From 8723119bd093d92684c8f9ea8b4596328d02bf09 Mon Sep 17 00:00:00 2001 From: graphieros Date: Sun, 1 Feb 2026 23:59:02 +0100 Subject: [PATCH 1/4] chore: bump vue-data-ui from 3.14.0 to 3.14.1 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 131593fbbf..b984552326 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "vite-plugin-pwa": "1.2.0", "vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", "vue": "3.5.27", - "vue-data-ui": "3.14.0" + "vue-data-ui": "3.14.1" }, "devDependencies": { "@npm/types": "2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d531911c08..52a7700f0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,8 +186,8 @@ importers: specifier: 3.5.27 version: 3.5.27(typescript@5.9.3) vue-data-ui: - specifier: 3.14.0 - version: 3.14.0(vue@3.5.27(typescript@5.9.3)) + specifier: 3.14.1 + version: 3.14.1(vue@3.5.27(typescript@5.9.3)) devDependencies: '@npm/types': specifier: 2.1.0 @@ -9247,8 +9247,8 @@ packages: vue-component-type-helpers@3.2.4: resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} - vue-data-ui@3.14.0: - resolution: {integrity: sha512-8r5HRb+bruVw4pEF8GMqe6tWKXS4Qby0erHtkfcDT0KB+maHAJ9H5AKXx/snUtDYrkTaDd0mOKryRBSuStZTSA==} + vue-data-ui@3.14.1: + resolution: {integrity: sha512-i7GNaNtw39Avy9VuDHGdLdDpH2sRsr2ZFRpiilOvQ0XA5887KCvd7J5IUQRpVNTQSaT7pFSSEMPwcQ7NfPHuVw==} peerDependencies: jspdf: '>=3.0.1' vue: '>=3.3.0' @@ -20666,7 +20666,7 @@ snapshots: vue-component-type-helpers@3.2.4: {} - vue-data-ui@3.14.0(vue@3.5.27(typescript@5.9.3)): + vue-data-ui@3.14.1(vue@3.5.27(typescript@5.9.3)): dependencies: vue: 3.5.27(typescript@5.9.3) From 63b7d065fb2894ebe3d7df3235488eb98e3038c1 Mon Sep 17 00:00:00 2001 From: graphieros Date: Sun, 1 Feb 2026 23:59:39 +0100 Subject: [PATCH 2/4] fix: use locale in chart config --- app/components/Package/DownloadAnalytics.vue | 141 +++++++++++++------ app/composables/useCharts.ts | 38 +++-- 2 files changed, 124 insertions(+), 55 deletions(-) diff --git a/app/components/Package/DownloadAnalytics.vue b/app/components/Package/DownloadAnalytics.vue index 97fc4d3435..d0c4ad831b 100644 --- a/app/components/Package/DownloadAnalytics.vue +++ b/app/components/Package/DownloadAnalytics.vue @@ -12,6 +12,7 @@ const props = defineProps<{ createdIso: string | null }>() +const { locale } = useI18n() const { accentColors, selectedAccentColor } = useAccentColor() const colorMode = useColorMode() const resolvedMode = shallowRef<'light' | 'dark'>('light') @@ -19,13 +20,30 @@ const rootEl = shallowRef(null) const { width } = useElementSize(rootEl) +const chartKey = ref(0) + +let chartRemountTimeoutId: ReturnType | null = null + onMounted(() => { rootEl.value = document.documentElement resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light' + + // If the chart is painted too early, built-in auto-sizing does not adapt to the final container size + chartRemountTimeoutId = setTimeout(() => { + chartKey.value += 1 + chartRemountTimeoutId = null + }, 1) +}) + +onBeforeUnmount(() => { + if (chartRemountTimeoutId !== null) { + clearTimeout(chartRemountTimeoutId) + chartRemountTimeoutId = null + } }) const { colors } = useCssVariables( - ['--bg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], + ['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], { element: rootEl, watchHtmlAttributes: true, @@ -121,7 +139,7 @@ function isYearlyDataset(data: unknown): data is YearlyDownloadPoint[] { function formatXyDataset( selectedGranularity: ChartTimeGranularity, dataset: EvolutionData, -): { dataset: VueUiXyDatasetItem[] | null; dates: string[] } { +): { dataset: VueUiXyDatasetItem[] | null; dates: number[] } { if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) { return { dataset: [ @@ -132,12 +150,7 @@ function formatXyDataset( color: accent.value, }, ], - dates: dataset.map(d => - $t('package.downloads.date_range_multiline', { - start: d.weekStart, - end: d.weekEnd, - }), - ), + dates: dataset.map(d => d.timestampEnd), } } if (selectedGranularity === 'daily' && isDailyDataset(dataset)) { @@ -150,7 +163,7 @@ function formatXyDataset( color: accent.value, }, ], - dates: dataset.map(d => d.day), + dates: dataset.map(d => d.timestamp), } } if (selectedGranularity === 'monthly' && isMonthlyDataset(dataset)) { @@ -163,7 +176,7 @@ function formatXyDataset( color: accent.value, }, ], - dates: dataset.map(d => d.month), + dates: dataset.map(d => d.timestamp), } } if (selectedGranularity === 'yearly' && isYearlyDataset(dataset)) { @@ -176,7 +189,7 @@ function formatXyDataset( color: accent.value, }, ], - dates: dataset.map(d => d.year), + dates: dataset.map(d => d.timestamp), } } return { dataset: null, dates: [] } @@ -198,18 +211,6 @@ function safeMax(a: string, b: string): string { return a.localeCompare(b) >= 0 ? a : b } -function extractDates(dateLabel: string): [string, string] | null { - const matches = dateLabel.match(/\b(\d{4}(?:-\d{2}-\d{2})?)\b/g) // either yyyy or yyyy-mm-dd - if (!matches) return null - - const first = matches.at(0) - const last = matches.at(-1) - - if (!first || !last || first === last) return null - - return [first, last] -} - /** * Two-phase state: * - selectedGranularity: immediate UI @@ -439,7 +440,7 @@ const effectiveData = computed(() => { return evolution.value }) -const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: string[] }>(() => { +const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number[] }>(() => { return formatXyDataset(displayedGranularity.value, effectiveData.value) }) @@ -453,11 +454,39 @@ const loadFile = (link: string, filename: string) => { a.remove() } +const datetimeFormatterOptions = computed(() => { + return { + daily: { + year: 'yyyy-MM-dd', + month: 'yyyy-MM-dd', + day: 'yyyy-MM-dd', + }, + weekly: { + year: 'yyyy-MM-dd', + month: 'yyyy-MM-dd', + day: 'yyyy-MM-dd', + }, + monthly: { + year: 'MMM yyyy', + month: 'MMM yyyy', + day: 'MMM yyyy', + }, + yearly: { + year: 'yyyy', + month: 'yyyy', + day: 'yyyy', + }, + }[selectedGranularity.value] +}) + const config = computed(() => { return { theme: isDarkMode.value ? 'dark' : 'default', chart: { height: isMobile.value ? 950 : 600, + padding: { + bottom: 36, + }, userOptions: { buttons: { pdf: false, @@ -525,10 +554,14 @@ const config = computed(() => { fontSize: isMobile.value ? 32 : 24, }, xAxisLabels: { - show: !isMobile.value, + show: false, values: chartData.value?.dates, - showOnlyAtModulo: true, - modulo: 12, + datetimeFormatter: { + enable: true, + locale: locale.value, + useUTC: true, + options: datetimeFormatterOptions.value, + }, }, yAxis: { formatter, @@ -536,6 +569,18 @@ const config = computed(() => { }, }, }, + timeTag: { + show: true, + backgroundColor: colors.value.bgElevated, + color: colors.value.fg, + fontSize: 16, + circleMarker: { + radius: 3, + color: colors.value.border, + }, + useDefaultFormat: true, + timeFormat: 'yyyy-MM-dd HH:mm:ss', + }, highlighter: { useLine: true, }, @@ -547,32 +592,25 @@ const config = computed(() => { borderColor: 'transparent', backdropFilter: false, backgroundColor: 'transparent', - customFormat: ({ - absoluteIndex, - datapoint, - }: { - absoluteIndex: number - datapoint: Record - }) => { + customFormat: ({ datapoint }: { datapoint: Record }) => { if (!datapoint) return '' const displayValue = formatter({ value: datapoint[0]?.value ?? 0 }) return `
- ${chartData.value?.dates[absoluteIndex]} - ${displayValue} + ${displayValue}
` }, }, zoom: { maxWidth: isMobile.value ? 350 : 500, - customFormat: - displayedGranularity.value !== 'weekly' - ? undefined - : ({ absoluteIndex, side }: { absoluteIndex: number; side: 'left' | 'right' }) => { - const parts = extractDates(chartData.value.dates[absoluteIndex] ?? '') - if (!parts) return '' - return side === 'left' ? parts[0] : parts[1] - }, + // customFormat: + // displayedGranularity.value !== 'weekly' + // ? undefined + // : ({ absoluteIndex, side }: { absoluteIndex: number; side: 'left' | 'right' }) => { + // const parts = extractDates(chartData.value.dates[absoluteIndex] ?? '') + // if (!parts) return '' + // return side === 'left' ? parts[0] : parts[1] + // }, highlightColor: colors.value.bgElevated, minimap: { show: true, @@ -594,7 +632,7 @@ const config = computed(() => {