[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.

fix: plug locale to large chart config (#691)

authored by

Alec Lloyd Probert and committed by
GitHub
e46df532 cc017d56

+128 -61
+87 -46
app/components/Package/DownloadAnalytics.vue
··· 12 12 createdIso: string | null 13 13 }>() 14 14 15 + const { locale } = useI18n() 15 16 const { accentColors, selectedAccentColor } = useAccentColor() 16 17 const colorMode = useColorMode() 17 18 const resolvedMode = shallowRef<'light' | 'dark'>('light') 18 19 const rootEl = shallowRef<HTMLElement | null>(null) 19 20 20 21 const { width } = useElementSize(rootEl) 22 + 23 + const chartKey = ref(0) 24 + 25 + let chartRemountTimeoutId: ReturnType<typeof setTimeout> | null = null 21 26 22 27 onMounted(() => { 23 28 rootEl.value = document.documentElement 24 29 resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light' 30 + 31 + // If the chart is painted too early, built-in auto-sizing does not adapt to the final container size 32 + chartRemountTimeoutId = setTimeout(() => { 33 + chartKey.value += 1 34 + chartRemountTimeoutId = null 35 + }, 1) 36 + }) 37 + 38 + onBeforeUnmount(() => { 39 + if (chartRemountTimeoutId !== null) { 40 + clearTimeout(chartRemountTimeoutId) 41 + chartRemountTimeoutId = null 42 + } 25 43 }) 26 44 27 45 const { colors } = useCssVariables( 28 - ['--bg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], 46 + ['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], 29 47 { 30 48 element: rootEl, 31 49 watchHtmlAttributes: true, ··· 121 139 function formatXyDataset( 122 140 selectedGranularity: ChartTimeGranularity, 123 141 dataset: EvolutionData, 124 - ): { dataset: VueUiXyDatasetItem[] | null; dates: string[] } { 142 + ): { dataset: VueUiXyDatasetItem[] | null; dates: number[] } { 125 143 if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) { 126 144 return { 127 145 dataset: [ ··· 132 150 color: accent.value, 133 151 }, 134 152 ], 135 - dates: dataset.map(d => 136 - $t('package.downloads.date_range_multiline', { 137 - start: d.weekStart, 138 - end: d.weekEnd, 139 - }), 140 - ), 153 + dates: dataset.map(d => d.timestampEnd), 141 154 } 142 155 } 143 156 if (selectedGranularity === 'daily' && isDailyDataset(dataset)) { ··· 150 163 color: accent.value, 151 164 }, 152 165 ], 153 - dates: dataset.map(d => d.day), 166 + dates: dataset.map(d => d.timestamp), 154 167 } 155 168 } 156 169 if (selectedGranularity === 'monthly' && isMonthlyDataset(dataset)) { ··· 163 176 color: accent.value, 164 177 }, 165 178 ], 166 - dates: dataset.map(d => d.month), 179 + dates: dataset.map(d => d.timestamp), 167 180 } 168 181 } 169 182 if (selectedGranularity === 'yearly' && isYearlyDataset(dataset)) { ··· 176 189 color: accent.value, 177 190 }, 178 191 ], 179 - dates: dataset.map(d => d.year), 192 + dates: dataset.map(d => d.timestamp), 180 193 } 181 194 } 182 195 return { dataset: null, dates: [] } ··· 196 209 197 210 function safeMax(a: string, b: string): string { 198 211 return a.localeCompare(b) >= 0 ? a : b 199 - } 200 - 201 - function extractDates(dateLabel: string): [string, string] | null { 202 - const matches = dateLabel.match(/\b(\d{4}(?:-\d{2}-\d{2})?)\b/g) // either yyyy or yyyy-mm-dd 203 - if (!matches) return null 204 - 205 - const first = matches.at(0) 206 - const last = matches.at(-1) 207 - 208 - if (!first || !last || first === last) return null 209 - 210 - return [first, last] 211 212 } 212 213 213 214 /** ··· 439 440 return evolution.value 440 441 }) 441 442 442 - const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: string[] }>(() => { 443 + const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number[] }>(() => { 443 444 return formatXyDataset(displayedGranularity.value, effectiveData.value) 444 445 }) 445 446 ··· 453 454 a.remove() 454 455 } 455 456 457 + const datetimeFormatterOptions = computed(() => { 458 + return { 459 + daily: { 460 + year: 'yyyy-MM-dd', 461 + month: 'yyyy-MM-dd', 462 + day: 'yyyy-MM-dd', 463 + }, 464 + weekly: { 465 + year: 'yyyy-MM-dd', 466 + month: 'yyyy-MM-dd', 467 + day: 'yyyy-MM-dd', 468 + }, 469 + monthly: { 470 + year: 'MMM yyyy', 471 + month: 'MMM yyyy', 472 + day: 'MMM yyyy', 473 + }, 474 + yearly: { 475 + year: 'yyyy', 476 + month: 'yyyy', 477 + day: 'yyyy', 478 + }, 479 + }[selectedGranularity.value] 480 + }) 481 + 456 482 const config = computed(() => { 457 483 return { 458 484 theme: isDarkMode.value ? 'dark' : 'default', 459 485 chart: { 460 486 height: isMobile.value ? 950 : 600, 487 + padding: { 488 + bottom: 36, 489 + }, 461 490 userOptions: { 462 491 buttons: { 463 492 pdf: false, ··· 525 554 fontSize: isMobile.value ? 32 : 24, 526 555 }, 527 556 xAxisLabels: { 528 - show: !isMobile.value, 557 + show: false, 529 558 values: chartData.value?.dates, 530 - showOnlyAtModulo: true, 531 - modulo: 12, 559 + datetimeFormatter: { 560 + enable: true, 561 + locale: locale.value, 562 + useUTC: true, 563 + options: datetimeFormatterOptions.value, 564 + }, 532 565 }, 533 566 yAxis: { 534 567 formatter, ··· 536 569 }, 537 570 }, 538 571 }, 572 + timeTag: { 573 + show: true, 574 + backgroundColor: colors.value.bgElevated, 575 + color: colors.value.fg, 576 + fontSize: 16, 577 + circleMarker: { 578 + radius: 3, 579 + color: colors.value.border, 580 + }, 581 + useDefaultFormat: true, 582 + timeFormat: 'yyyy-MM-dd HH:mm:ss', 583 + }, 539 584 highlighter: { 540 585 useLine: true, 541 586 }, ··· 547 592 borderColor: 'transparent', 548 593 backdropFilter: false, 549 594 backgroundColor: 'transparent', 550 - customFormat: ({ 551 - absoluteIndex, 552 - datapoint, 553 - }: { 554 - absoluteIndex: number 555 - datapoint: Record<string, any> 556 - }) => { 595 + customFormat: ({ datapoint }: { datapoint: Record<string, any> }) => { 557 596 if (!datapoint) return '' 558 597 const displayValue = formatter({ value: datapoint[0]?.value ?? 0 }) 559 598 return `<div class="flex flex-col font-mono text-xs p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md"> 560 - <span class="text-fg-subtle">${chartData.value?.dates[absoluteIndex]}</span> 561 - <span class="text-xl">${displayValue}</span> 599 + <span class="text-xl text-[var(--fg)]">${displayValue}</span> 562 600 </div> 563 601 ` 564 602 }, 565 603 }, 566 604 zoom: { 567 605 maxWidth: isMobile.value ? 350 : 500, 568 - customFormat: 569 - displayedGranularity.value !== 'weekly' 570 - ? undefined 571 - : ({ absoluteIndex, side }: { absoluteIndex: number; side: 'left' | 'right' }) => { 572 - const parts = extractDates(chartData.value.dates[absoluteIndex] ?? '') 573 - if (!parts) return '' 574 - return side === 'left' ? parts[0] : parts[1] 575 - }, 576 606 highlightColor: colors.value.bgElevated, 577 607 minimap: { 578 608 show: true, ··· 594 624 </script> 595 625 596 626 <template> 597 - <div class="w-full relative"> 627 + <div class="w-full relative" id="download-analytics"> 598 628 <div class="w-full mb-4 flex flex-col gap-3"> 599 629 <!-- Mobile: stack vertically, Desktop: horizontal --> 600 630 <div class="flex flex-col sm:flex-row gap-3 sm:gap-2 sm:items-end"> ··· 688 718 </div> 689 719 690 720 <ClientOnly v-if="inModal && chartData.dataset"> 691 - <VueUiXy :dataset="chartData.dataset" :config="config" class="[direction:ltr]"> 721 + <VueUiXy 722 + :dataset="chartData.dataset" 723 + :config="config" 724 + class="[direction:ltr]" 725 + :key="chartKey" 726 + > 692 727 <template #menuIcon="{ isOpen }"> 693 728 <span v-if="isOpen" class="i-carbon:close w-6 h-6" aria-hidden="true" /> 694 729 <span v-else class="i-carbon:overflow-menu-vertical w-6 h-6" aria-hidden="true" /> ··· 798 833 .vue-ui-pen-and-paper-action:hover { 799 834 background: var(--bg-elevated) !important; 800 835 box-shadow: none !important; 836 + } 837 + 838 + /* Override default placement of the refresh button to have it to the minimap's side */ 839 + #download-analytics .vue-data-ui-refresh-button { 840 + top: -0.6rem !important; 841 + left: calc(100% + 2rem) !important; 801 842 } 802 843 </style>
+29 -9
app/composables/useCharts.ts
··· 5 5 time?: Record<string, string> 6 6 } 7 7 8 - export type DailyDownloadPoint = { downloads: number; day: string } 8 + export type DailyDownloadPoint = { downloads: number; day: string; timestamp: number } 9 9 export type WeeklyDownloadPoint = { 10 10 downloads: number 11 11 weekKey: string 12 12 weekStart: string 13 13 weekEnd: string 14 + timestampStart: number 15 + timestampEnd: number 14 16 } 15 - export type MonthlyDownloadPoint = { downloads: number; month: string } 16 - export type YearlyDownloadPoint = { downloads: number; year: string } 17 + export type MonthlyDownloadPoint = { downloads: number; month: string; timestamp: number } 18 + export type YearlyDownloadPoint = { downloads: number; year: string; timestamp: number } 17 19 18 20 type PackageDownloadEvolutionOptionsBase = { 19 21 startDate?: string ··· 124 126 125 127 function buildDailyEvolutionFromDaily( 126 128 daily: Array<{ day: string; downloads: number }>, 127 - ): DailyDownloadPoint[] { 129 + ): Array<{ day: string; downloads: number; timestamp: number }> { 128 130 return daily 129 131 .slice() 130 132 .sort((a, b) => a.day.localeCompare(b.day)) 131 - .map(item => ({ day: item.day, downloads: item.downloads })) 133 + .map(item => { 134 + const dayDate = parseIsoDateOnly(item.day) 135 + const timestamp = dayDate.getTime() 136 + 137 + return { day: item.day, downloads: item.downloads, timestamp } 138 + }) 132 139 } 133 140 134 141 function buildRollingWeeklyEvolutionFromDaily( ··· 164 171 const weekStartIso = toIsoDateString(weekStartDate) 165 172 const weekEndIso = toIsoDateString(clampedWeekEndDate) 166 173 174 + const timestampStart = weekStartDate.getTime() 175 + const timestampEnd = clampedWeekEndDate.getTime() 176 + 167 177 return { 168 178 downloads, 169 179 weekKey: `${weekStartIso}_${weekEndIso}`, 170 180 weekStart: weekStartIso, 171 181 weekEnd: weekEndIso, 182 + timestampStart, 183 + timestampEnd, 172 184 } 173 185 }) 174 186 } 175 187 176 188 function buildMonthlyEvolutionFromDaily( 177 189 daily: Array<{ day: string; downloads: number }>, 178 - ): MonthlyDownloadPoint[] { 190 + ): Array<{ month: string; downloads: number; timestamp: number }> { 179 191 const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day)) 180 192 const downloadsByMonth = new Map<string, number>() 181 193 ··· 186 198 187 199 return Array.from(downloadsByMonth.entries()) 188 200 .sort(([a], [b]) => a.localeCompare(b)) 189 - .map(([month, downloads]) => ({ month, downloads })) 201 + .map(([month, downloads]) => { 202 + const monthStartDate = parseIsoDateOnly(`${month}-01`) 203 + const timestamp = monthStartDate.getTime() 204 + return { month, downloads, timestamp } 205 + }) 190 206 } 191 207 192 208 function buildYearlyEvolutionFromDaily( 193 209 daily: Array<{ day: string; downloads: number }>, 194 - ): YearlyDownloadPoint[] { 210 + ): Array<{ year: string; downloads: number; timestamp: number }> { 195 211 const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day)) 196 212 const downloadsByYear = new Map<string, number>() 197 213 ··· 202 218 203 219 return Array.from(downloadsByYear.entries()) 204 220 .sort(([a], [b]) => a.localeCompare(b)) 205 - .map(([year, downloads]) => ({ year, downloads })) 221 + .map(([year, downloads]) => { 222 + const yearStartDate = parseIsoDateOnly(`${year}-01-01`) 223 + const timestamp = yearStartDate.getTime() 224 + return { year, downloads, timestamp } 225 + }) 206 226 } 207 227 208 228 function getClientDailyRangePromiseCache() {
+1 -1
package.json
··· 91 91 "vite-plugin-pwa": "1.2.0", 92 92 "vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", 93 93 "vue": "3.5.27", 94 - "vue-data-ui": "3.14.0" 94 + "vue-data-ui": "3.14.1" 95 95 }, 96 96 "devDependencies": { 97 97 "@npm/types": "2.1.0",
+5 -5
pnpm-lock.yaml
··· 186 186 specifier: 3.5.27 187 187 version: 3.5.27(typescript@5.9.3) 188 188 vue-data-ui: 189 - specifier: 3.14.0 190 - version: 3.14.0(vue@3.5.27(typescript@5.9.3)) 189 + specifier: 3.14.1 190 + version: 3.14.1(vue@3.5.27(typescript@5.9.3)) 191 191 devDependencies: 192 192 '@npm/types': 193 193 specifier: 2.1.0 ··· 9247 9247 vue-component-type-helpers@3.2.4: 9248 9248 resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} 9249 9249 9250 - vue-data-ui@3.14.0: 9251 - resolution: {integrity: sha512-8r5HRb+bruVw4pEF8GMqe6tWKXS4Qby0erHtkfcDT0KB+maHAJ9H5AKXx/snUtDYrkTaDd0mOKryRBSuStZTSA==} 9250 + vue-data-ui@3.14.1: 9251 + resolution: {integrity: sha512-i7GNaNtw39Avy9VuDHGdLdDpH2sRsr2ZFRpiilOvQ0XA5887KCvd7J5IUQRpVNTQSaT7pFSSEMPwcQ7NfPHuVw==} 9252 9252 peerDependencies: 9253 9253 jspdf: '>=3.0.1' 9254 9254 vue: '>=3.3.0' ··· 20666 20666 20667 20667 vue-component-type-helpers@3.2.4: {} 20668 20668 20669 - vue-data-ui@3.14.0(vue@3.5.27(typescript@5.9.3)): 20669 + vue-data-ui@3.14.1(vue@3.5.27(typescript@5.9.3)): 20670 20670 dependencies: 20671 20671 vue: 3.5.27(typescript@5.9.3) 20672 20672
+6
test/nuxt/a11y.spec.ts
··· 366 366 weekKey: '2024-W01', 367 367 weekStart: '2024-01-01', 368 368 weekEnd: '2024-01-07', 369 + timestampStart: 1704067200, 370 + timestampEnd: 1704585600, 369 371 }, 370 372 { 371 373 downloads: 1200, 372 374 weekKey: '2024-W02', 373 375 weekStart: '2024-01-08', 374 376 weekEnd: '2024-01-14', 377 + timestampStart: 1704672000, 378 + timestampEnd: 1705190400, 375 379 }, 376 380 { 377 381 downloads: 1500, 378 382 weekKey: '2024-W03', 379 383 weekStart: '2024-01-15', 380 384 weekEnd: '2024-01-21', 385 + timestampStart: 1705276800, 386 + timestampEnd: 1705795200, 381 387 }, 382 388 ] 383 389