@@ -12,20 +12,38 @@ const props = defineProps<{
1212 createdIso: string | null
1313}>()
1414
15+ const { locale } = useI18n ()
1516const { accentColors, selectedAccentColor } = useAccentColor ()
1617const colorMode = useColorMode ()
1718const resolvedMode = shallowRef <' light' | ' dark' >(' light' )
1819const rootEl = shallowRef <HTMLElement | null >(null )
1920
2021const { width } = useElementSize (rootEl )
2122
23+ const chartKey = ref (0 )
24+
25+ let chartRemountTimeoutId: ReturnType <typeof setTimeout > | null = null
26+
2227onMounted (() => {
2328 rootEl .value = document .documentElement
2429 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+ }
2543})
2644
2745const { 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' ],
2947 {
3048 element: rootEl ,
3149 watchHtmlAttributes: true ,
@@ -121,7 +139,7 @@ function isYearlyDataset(data: unknown): data is YearlyDownloadPoint[] {
121139function formatXyDataset(
122140 selectedGranularity : ChartTimeGranularity ,
123141 dataset : EvolutionData ,
124- ): { dataset: VueUiXyDatasetItem [] | null ; dates: string [] } {
142+ ): { dataset: VueUiXyDatasetItem [] | null ; dates: number [] } {
125143 if (selectedGranularity === ' weekly' && isWeeklyDataset (dataset )) {
126144 return {
127145 dataset: [
@@ -132,12 +150,7 @@ function formatXyDataset(
132150 color: accent .value ,
133151 },
134152 ],
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 ),
141154 }
142155 }
143156 if (selectedGranularity === ' daily' && isDailyDataset (dataset )) {
@@ -150,7 +163,7 @@ function formatXyDataset(
150163 color: accent .value ,
151164 },
152165 ],
153- dates: dataset .map (d => d .day ),
166+ dates: dataset .map (d => d .timestamp ),
154167 }
155168 }
156169 if (selectedGranularity === ' monthly' && isMonthlyDataset (dataset )) {
@@ -163,7 +176,7 @@ function formatXyDataset(
163176 color: accent .value ,
164177 },
165178 ],
166- dates: dataset .map (d => d .month ),
179+ dates: dataset .map (d => d .timestamp ),
167180 }
168181 }
169182 if (selectedGranularity === ' yearly' && isYearlyDataset (dataset )) {
@@ -176,7 +189,7 @@ function formatXyDataset(
176189 color: accent .value ,
177190 },
178191 ],
179- dates: dataset .map (d => d .year ),
192+ dates: dataset .map (d => d .timestamp ),
180193 }
181194 }
182195 return { dataset: null , dates: [] }
@@ -198,18 +211,6 @@ function safeMax(a: string, b: string): string {
198211 return a .localeCompare (b ) >= 0 ? a : b
199212}
200213
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-
213214/**
214215 * Two-phase state:
215216 * - selectedGranularity: immediate UI
@@ -439,7 +440,7 @@ const effectiveData = computed<EvolutionData>(() => {
439440 return evolution .value
440441})
441442
442- const chartData = computed <{ dataset: VueUiXyDatasetItem [] | null ; dates: string [] }>(() => {
443+ const chartData = computed <{ dataset: VueUiXyDatasetItem [] | null ; dates: number [] }>(() => {
443444 return formatXyDataset (displayedGranularity .value , effectiveData .value )
444445})
445446
@@ -453,11 +454,39 @@ const loadFile = (link: string, filename: string) => {
453454 a .remove ()
454455}
455456
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+
456482const config = computed (() => {
457483 return {
458484 theme: isDarkMode .value ? ' dark' : ' default' ,
459485 chart: {
460486 height: isMobile .value ? 950 : 600 ,
487+ padding: {
488+ bottom: 36 ,
489+ },
461490 userOptions: {
462491 buttons: {
463492 pdf: false ,
@@ -525,17 +554,33 @@ const config = computed(() => {
525554 fontSize: isMobile .value ? 32 : 24 ,
526555 },
527556 xAxisLabels: {
528- show: ! isMobile . value ,
557+ show: false ,
529558 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+ },
532565 },
533566 yAxis: {
534567 formatter ,
535568 useNiceScale: true ,
536569 },
537570 },
538571 },
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+ },
539584 highlighter: {
540585 useLine: true ,
541586 },
@@ -547,32 +592,17 @@ const config = computed(() => {
547592 borderColor: ' transparent' ,
548593 backdropFilter: false ,
549594 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 > }) => {
557596 if (! datapoint ) return ' '
558597 const displayValue = formatter ({ value: datapoint [0 ]?.value ?? 0 })
559598 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>
562600 </div>
563601 `
564602 },
565603 },
566604 zoom: {
567605 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- },
576606 highlightColor: colors .value .bgElevated ,
577607 minimap: {
578608 show: true ,
@@ -594,7 +624,7 @@ const config = computed(() => {
594624 </script >
595625
596626<template >
597- <div class =" w-full relative" >
627+ <div class =" w-full relative" id = " download-analytics " >
598628 <div class =" w-full mb-4 flex flex-col gap-3" >
599629 <!-- Mobile: stack vertically, Desktop: horizontal -->
600630 <div class =" flex flex-col sm:flex-row gap-3 sm:gap-2 sm:items-end" >
@@ -688,7 +718,12 @@ const config = computed(() => {
688718 </div >
689719
690720 <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+ >
692727 <template #menuIcon =" { isOpen } " >
693728 <span v-if =" isOpen" class =" i-carbon:close w-6 h-6" aria-hidden =" true" />
694729 <span v-else class =" i-carbon:overflow-menu-vertical w-6 h-6" aria-hidden =" true" />
@@ -799,4 +834,10 @@ const config = computed(() => {
799834 background : var (--bg-elevated ) !important ;
800835 box-shadow : none !important ;
801836}
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 ;
842+ }
802843 </style >
0 commit comments