[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: clean up and document (#950)

authored by

Alec Lloyd Probert and committed by
GitHub
8ddcecc3 704987bb

+179 -9
+179 -9
app/components/Package/DownloadAnalytics.vue
··· 25 25 createdIso?: string | null 26 26 }>() 27 27 28 - const shouldFetch = computed(() => true) 29 - 30 28 const { locale } = useI18n() 31 29 const { accentColors, selectedAccentColor } = useAccentColor() 32 30 const colorMode = useColorMode() ··· 140 138 ) 141 139 } 142 140 141 + /** 142 + * Formats a single evolution dataset into the structure expected by `VueUiXy` 143 + * for single-series charts. 144 + * 145 + * The dataset is interpreted based on the selected time granularity: 146 + * - **daily** → uses `timestamp` 147 + * - **weekly** → uses `timestampEnd` 148 + * - **monthly** → uses `timestamp` 149 + * - **yearly** → uses `timestamp` 150 + * 151 + * Only datasets matching the expected shape for the given granularity are 152 + * accepted. If the dataset does not match, an empty result is returned. 153 + * 154 + * The returned structure includes: 155 + * - a single line-series dataset with a consistent color 156 + * - a list of timestamps used as the x-axis values 157 + * 158 + * @param selectedGranularity - Active chart time granularity 159 + * @param dataset - Raw evolution dataset to format 160 + * @param seriesName - Display name for the resulting series 161 + * @returns An object containing a formatted dataset and its associated dates, 162 + * or `{ dataset: null, dates: [] }` when the input is incompatible 163 + */ 143 164 function formatXyDataset( 144 165 selectedGranularity: ChartTimeGranularity, 145 166 dataset: EvolutionData, ··· 200 221 return { dataset: null, dates: [] } 201 222 } 202 223 224 + /** 225 + * Extracts normalized time-series points from an evolution dataset based on 226 + * the selected time granularity. 227 + * 228 + * Each returned point contains: 229 + * - `timestamp`: the numeric time value used for x-axis alignment 230 + * - `downloads`: the corresponding value at that time 231 + * 232 + * The timestamp field is selected according to granularity: 233 + * - **daily** → `timestamp` 234 + * - **weekly** → `timestampEnd` 235 + * - **monthly** → `timestamp` 236 + * - **yearly** → `timestamp` 237 + * 238 + * If the dataset does not match the expected shape for the given granularity, 239 + * an empty array is returned. 240 + * 241 + * This helper is primarily used in multi-package mode to align multiple 242 + * datasets on a shared time axis. 243 + * 244 + * @param selectedGranularity - Active chart time granularity 245 + * @param dataset - Raw evolution dataset to extract points from 246 + * @returns An array of normalized `{ timestamp, downloads }` points 247 + */ 203 248 function extractSeriesPoints( 204 249 selectedGranularity: ChartTimeGranularity, 205 250 dataset: EvolutionData, ··· 263 308 const endDate = shallowRef<string>('') // YYYY-MM-DD 264 309 const hasUserEditedDates = shallowRef(false) 265 310 311 + /** 312 + * Initializes the date range from the provided weeklyDownloads dataset. 313 + * 314 + * The range is inferred directly from the dataset boundaries: 315 + * - `startDate` is set from the `weekStart` of the first entry 316 + * - `endDate` is set from the `weekEnd` of the last entry 317 + * 318 + * Dates are normalized to `YYYY-MM-DD` and validated before assignment. 319 + * 320 + * This function is a no-op when: 321 + * - the user has already edited the date range 322 + * - no weekly download data is available 323 + * 324 + * The inferred range takes precedence over client-side fallbacks but does not 325 + * override user-defined dates. 326 + */ 266 327 function initDateRangeFromWeekly() { 267 328 if (hasUserEditedDates.value) return 268 329 if (!props.weeklyDownloads?.length) return ··· 275 336 if (isValidIsoDateOnly(end)) endDate.value = end 276 337 } 277 338 339 + /** 340 + * Initializes a default date range on the client when no explicit dates 341 + * have been provided and the user has not manually edited the range, typically 342 + * when weeklyDownloads is not provided. 343 + * 344 + * The range is computed in UTC to avoid timezone-related off-by-one errors: 345 + * - `endDate` is set to yesterday (UTC) 346 + * - `startDate` is set to 29 days before yesterday (UTC), yielding a 30-day range 347 + * 348 + * This function is a no-op when: 349 + * - the user has already edited the date range 350 + * - the code is running on the server 351 + * - both `startDate` and `endDate` are already defined 352 + */ 278 353 function initDateRangeFallbackClient() { 279 354 if (hasUserEditedDates.value) return 280 355 if (!import.meta.client) return ··· 297 372 function toUtcDateOnly(date: Date): string { 298 373 return date.toISOString().slice(0, 10) 299 374 } 375 + 300 376 function addUtcDays(date: Date, days: number): Date { 301 377 const next = new Date(date) 302 378 next.setUTCDate(next.getUTCDate() + days) 303 379 return next 304 380 } 381 + 382 + /** 383 + * Initializes a default date range for multi-package mode using a fixed 384 + * 52-week rolling window. 385 + * 386 + * The range is computed in UTC to ensure consistent boundaries across 387 + * timezones: 388 + * - `endDate` is set to yesterday (UTC) 389 + * - `startDate` is set to the first day of the 52-week window ending yesterday 390 + * 391 + * This function is intended for multi-package comparisons where no explicit 392 + * date range or dataset-derived range is available. 393 + * 394 + * This function is a no-op when: 395 + * - the user has already edited the date range 396 + * - the code is running on the server 397 + * - the component is not in multi-package mode 398 + * - both `startDate` and `endDate` are already defined 399 + */ 305 400 function initDateRangeForMultiPackageWeekly52() { 306 401 if (hasUserEditedDates.value) return 307 402 if (!import.meta.client) return ··· 364 459 | { granularity: 'year'; startDate?: string; endDate?: string } 365 460 >({ granularity: 'week', weeks: 52 }) 366 461 462 + /** 463 + * Applies the current date range (`startDate` / `endDate`) to a base options 464 + * object, returning a new object augmented with validated date fields. 465 + * 466 + * Dates are normalized to `YYYY-MM-DD`, validated, and ordered to ensure 467 + * logical consistency: 468 + * - When both dates are valid, the earliest is assigned to `startDate` and 469 + * the latest to `endDate` 470 + * - When only one valid date is present, only that boundary is applied 471 + * - Invalid or empty dates are omitted from the result 472 + * 473 + * The input object is not mutated. 474 + * 475 + * @typeParam T - Base options type to extend with date range fields 476 + * @param base - Base options object to which the date range should be applied 477 + * @returns A new options object including the applicable `startDate` and/or 478 + * `endDate` fields 479 + */ 367 480 function applyDateRange<T extends Record<string, unknown>>(base: T): T & DateRangeFields { 368 481 const next: T & DateRangeFields = { ...base } 369 482 ··· 396 509 const isMounted = shallowRef(false) 397 510 let requestToken = 0 398 511 512 + // Watches granularity and date inputs to keep request options in sync and 513 + // manage the loading state. 514 + // 515 + // This watcher does NOT perform the fetch itself. Its responsibilities are: 516 + // - derive the correct API options from the selected granularity 517 + // - apply the current validated date range to those options 518 + // - determine whether a loading indicator should be shown 519 + // 520 + // Fetching is debounced separately to avoid excessive 521 + // network requests while the user is interacting with controls. 399 522 watch( 400 523 [selectedGranularity, startDate, endDate], 401 524 ([granularityValue]) => { ··· 410 533 if (!isMounted.value) return 411 534 412 535 const packageNames = effectivePackageNames.value 413 - if (!import.meta.client || !shouldFetch.value || !packageNames.length) { 536 + if (!import.meta.client || !packageNames.length) { 414 537 pending.value = false 415 538 return 416 539 } ··· 434 557 { immediate: true }, 435 558 ) 436 559 560 + /** 561 + * Fetches download evolution data based on the current granularity, 562 + * date range, and package selection. 563 + * 564 + * This function: 565 + * - runs only on the client 566 + * - supports both single-package and multi-package modes 567 + * - applies request de-duplication via a request token to avoid race conditions 568 + * - updates the appropriate reactive stores with fetched data 569 + * - manages the `pending` loading state 570 + * 571 + * Behavior details: 572 + * - In multi-package mode, all packages are fetched in parallel and partial 573 + * failures are tolerated using `Promise.allSettled` 574 + * - In single-package mode, weekly data is reused from `weeklyDownloads` 575 + * when available and no explicit date range is requested 576 + * - Outdated responses are discarded when a newer request supersedes them 577 + * 578 + */ 437 579 async function loadNow() { 438 580 if (!import.meta.client) return 439 - if (!shouldFetch.value) return 440 581 441 582 const packageNames = effectivePackageNames.value 442 583 if (!packageNames.length) return ··· 498 639 } 499 640 } 500 641 642 + // Debounced wrapper around `loadNow` to avoid triggering a network request 643 + // on every intermediate state change while the user is interacting with inputs 644 + // 645 + // This 'arbitrary' 1000 ms delay: 646 + // - gives enough time for the user to finish changing granularity or dates 647 + // - prevents unnecessary API load and visual flicker of the loading state 648 + // 501 649 const debouncedLoadNow = useDebounceFn(() => { 502 650 loadNow() 503 651 }, 1000) ··· 506 654 const names = effectivePackageNames.value.join(',') 507 655 const o = options.value as any 508 656 return [ 509 - shouldFetch.value ? '1' : '0', 510 657 isMultiPackageMode.value ? 'M' : 'S', 511 658 names, 512 659 String(props.createdIso ?? ''), ··· 536 683 return evolution.value 537 684 }) 538 685 686 + /** 687 + * Normalized chart data derived from the fetched evolution datasets. 688 + * 689 + * This computed value adapts its behavior based on the current mode: 690 + * 691 + * - **Single-package mode** 692 + * - Delegates formatting to `formatXyDataset` 693 + * - Produces a single series with its corresponding timestamps 694 + * 695 + * - **Multi-package mode** 696 + * - Merges multiple package datasets into a shared time axis 697 + * - Aligns all series on the same sorted list of timestamps 698 + * - Fills missing datapoints with `0` to keep series lengths consistent 699 + * - Assigns framework-specific colors when applicable 700 + * 701 + * The returned structure matches the expectations of `VueUiXy`: 702 + * - `dataset`: array of series definitions, or `null` when no data is available 703 + * - `dates`: sorted list of timestamps used as the x-axis reference 704 + * 705 + * Returning `dataset: null` explicitly signals the absence of data and allows 706 + * the template to handle empty states without ambiguity. 707 + */ 539 708 const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number[] }>(() => { 540 709 if (!isMultiPackageMode.value) { 541 710 const pkg = effectivePackageNames.value[0] ?? props.packageName ?? '' ··· 558 727 const dates = Array.from(timestampSet).sort((a, b) => a - b) 559 728 if (!dates.length) return { dataset: null, dates: [] } 560 729 561 - const dataset: VueUiXyDatasetItem[] = names.map((pkg, index) => { 730 + const dataset: VueUiXyDatasetItem[] = names.map(pkg => { 562 731 const points = pointsByPackage.get(pkg) ?? [] 563 732 const map = new Map<number, number>() 564 733 for (const p of points) map.set(p.timestamp, p.downloads) ··· 616 785 return `${sanitise(label ?? '')}-${g}_${range}.${extension}` 617 786 } 618 787 619 - const config = computed(() => { 788 + // VueUiXy chart component configuration 789 + const chartConfig = computed(() => { 620 790 return { 621 791 theme: isDarkMode.value ? 'dark' : 'default', 622 792 chart: { ··· 860 1030 <div role="region" aria-labelledby="download-analytics-title"> 861 1031 <ClientOnly v-if="chartData.dataset"> 862 1032 <div> 863 - <VueUiXy :dataset="chartData.dataset" :config="config" class="[direction:ltr]"> 1033 + <VueUiXy :dataset="chartData.dataset" :config="chartConfig" class="[direction:ltr]"> 864 1034 <!-- Custom legend for multiple series --> 865 1035 <template v-if="isMultiPackageMode" #legend="{ legend }"> 866 1036 <div class="flex gap-4 flex-wrap justify-center"> ··· 970 1140 </div> 971 1141 972 1142 <div 973 - v-if="shouldFetch && !chartData.dataset && !pending" 1143 + v-if="!chartData.dataset && !pending" 974 1144 class="min-h-[260px] flex items-center justify-center text-fg-subtle font-mono text-sm" 975 1145 > 976 1146 {{ $t('package.downloads.no_data') }}