Skip to content

Commit e46df53

Browse files
authored
fix: plug locale to large chart config (#691)
1 parent cc017d5 commit e46df53

File tree

5 files changed

+128
-61
lines changed

5 files changed

+128
-61
lines changed

app/components/Package/DownloadAnalytics.vue

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,38 @@ const props = defineProps<{
1212
createdIso: string | null
1313
}>()
1414
15+
const { locale } = useI18n()
1516
const { accentColors, selectedAccentColor } = useAccentColor()
1617
const colorMode = useColorMode()
1718
const resolvedMode = shallowRef<'light' | 'dark'>('light')
1819
const rootEl = shallowRef<HTMLElement | null>(null)
1920
2021
const { width } = useElementSize(rootEl)
2122
23+
const chartKey = ref(0)
24+
25+
let chartRemountTimeoutId: ReturnType<typeof setTimeout> | null = null
26+
2227
onMounted(() => {
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
2745
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'],
2947
{
3048
element: rootEl,
3149
watchHtmlAttributes: true,
@@ -121,7 +139,7 @@ function isYearlyDataset(data: unknown): data is YearlyDownloadPoint[] {
121139
function 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+
456482
const 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>

app/composables/useCharts.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ export type PackumentLikeForTime = {
55
time?: Record<string, string>
66
}
77

8-
export type DailyDownloadPoint = { downloads: number; day: string }
8+
export type DailyDownloadPoint = { downloads: number; day: string; timestamp: number }
99
export type WeeklyDownloadPoint = {
1010
downloads: number
1111
weekKey: string
1212
weekStart: string
1313
weekEnd: string
14+
timestampStart: number
15+
timestampEnd: number
1416
}
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 }
1719

1820
type PackageDownloadEvolutionOptionsBase = {
1921
startDate?: string
@@ -124,11 +126,16 @@ function mergeDailyPoints(
124126

125127
function buildDailyEvolutionFromDaily(
126128
daily: Array<{ day: string; downloads: number }>,
127-
): DailyDownloadPoint[] {
129+
): Array<{ day: string; downloads: number; timestamp: number }> {
128130
return daily
129131
.slice()
130132
.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+
})
132139
}
133140

134141
function buildRollingWeeklyEvolutionFromDaily(
@@ -164,18 +171,23 @@ function buildRollingWeeklyEvolutionFromDaily(
164171
const weekStartIso = toIsoDateString(weekStartDate)
165172
const weekEndIso = toIsoDateString(clampedWeekEndDate)
166173

174+
const timestampStart = weekStartDate.getTime()
175+
const timestampEnd = clampedWeekEndDate.getTime()
176+
167177
return {
168178
downloads,
169179
weekKey: `${weekStartIso}_${weekEndIso}`,
170180
weekStart: weekStartIso,
171181
weekEnd: weekEndIso,
182+
timestampStart,
183+
timestampEnd,
172184
}
173185
})
174186
}
175187

176188
function buildMonthlyEvolutionFromDaily(
177189
daily: Array<{ day: string; downloads: number }>,
178-
): MonthlyDownloadPoint[] {
190+
): Array<{ month: string; downloads: number; timestamp: number }> {
179191
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
180192
const downloadsByMonth = new Map<string, number>()
181193

@@ -186,12 +198,16 @@ function buildMonthlyEvolutionFromDaily(
186198

187199
return Array.from(downloadsByMonth.entries())
188200
.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+
})
190206
}
191207

192208
function buildYearlyEvolutionFromDaily(
193209
daily: Array<{ day: string; downloads: number }>,
194-
): YearlyDownloadPoint[] {
210+
): Array<{ year: string; downloads: number; timestamp: number }> {
195211
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
196212
const downloadsByYear = new Map<string, number>()
197213

@@ -202,7 +218,11 @@ function buildYearlyEvolutionFromDaily(
202218

203219
return Array.from(downloadsByYear.entries())
204220
.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+
})
206226
}
207227

208228
function getClientDailyRangePromiseCache() {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"vite-plugin-pwa": "1.2.0",
9292
"vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab",
9393
"vue": "3.5.27",
94-
"vue-data-ui": "3.14.0"
94+
"vue-data-ui": "3.14.1"
9595
},
9696
"devDependencies": {
9797
"@npm/types": "2.1.0",

0 commit comments

Comments
 (0)