From 50f253f2a616ed9d1e90acdbffd3d159e59bd4ac Mon Sep 17 00:00:00 2001 From: graphieros Date: Mon, 9 Feb 2026 12:33:04 +0100 Subject: [PATCH 1/4] chore: bump vue-data-ui from 3.14.9 to 3.14.10 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 77e6dd29e..ec1d6f82c 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "vite-plugin-pwa": "1.2.0", "vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", "vue": "3.5.27", - "vue-data-ui": "3.14.9" + "vue-data-ui": "3.14.10" }, "devDependencies": { "@e18e/eslint-plugin": "0.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4903d2079..1f79506fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,8 +207,8 @@ importers: specifier: 3.5.27 version: 3.5.27(typescript@5.9.3) vue-data-ui: - specifier: 3.14.9 - version: 3.14.9(vue@3.5.27(typescript@5.9.3)) + specifier: 3.14.10 + version: 3.14.10(vue@3.5.27(typescript@5.9.3)) devDependencies: '@e18e/eslint-plugin': specifier: 0.1.4 @@ -9414,8 +9414,8 @@ packages: vue-component-type-helpers@3.2.4: resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} - vue-data-ui@3.14.9: - resolution: {integrity: sha512-ITq2xDK1LC2JrlDw0V17j/KsgVs/TXQEkdC3gPl6dkB4AvX88FsaNU1abGR1D5nXyCxaluPqIOiqSa/qDPDFSg==} + vue-data-ui@3.14.10: + resolution: {integrity: sha512-2mzt/5InMFWpE1458gm1h26ILpQxotQ9cOM1xcS8boWRZnjEw1ficfay+g/HNQZL0k4AzMSZKnWWBJ/PaKgclA==} peerDependencies: jspdf: '>=3.0.1' vue: '>=3.3.0' @@ -21025,7 +21025,7 @@ snapshots: vue-component-type-helpers@3.2.4: {} - vue-data-ui@3.14.9(vue@3.5.27(typescript@5.9.3)): + vue-data-ui@3.14.10(vue@3.5.27(typescript@5.9.3)): dependencies: vue: 3.5.27(typescript@5.9.3) From 5b188de6506fd51d983b6e09ce99cf5358b74dda Mon Sep 17 00:00:00 2001 From: graphieros Date: Mon, 9 Feb 2026 12:33:44 +0100 Subject: [PATCH 2/4] feat: add chart watermark for PNG and SVG prints --- app/components/Package/DownloadAnalytics.vue | 166 +++++++++++++++++-- 1 file changed, 156 insertions(+), 10 deletions(-) diff --git a/app/components/Package/DownloadAnalytics.vue b/app/components/Package/DownloadAnalytics.vue index 5856a4794..e4cd86007 100644 --- a/app/components/Package/DownloadAnalytics.vue +++ b/app/components/Package/DownloadAnalytics.vue @@ -55,7 +55,16 @@ onMounted(async () => { }) const { colors } = useCssVariables( - ['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], + [ + '--bg', + '--fg', + '--bg-subtle', + '--bg-elevated', + '--fg-subtle', + '--fg-muted', + '--border', + '--border-subtle', + ], { element: rootEl, watchHtmlAttributes: true, @@ -784,6 +793,10 @@ const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number return { dataset, dates } }) +const maxDatapoints = computed(() => + Math.max(0, ...(chartData.value.dataset ?? []).map(d => d.series.length)), +) + /** * Maximum estimated value across all series when the chart is * displaying a partially completed time bucket (monthly or yearly). @@ -1310,6 +1323,104 @@ function drawLastDatapointLabel(svg: Record) { return dataLabels.join('\n') } +/** + * Build and return a legend to be injected during the SVG export only, since the custom legend is + * displayed as an independant div, content has to be injected within the chart's viewBox. + * + * Legend items are displayed in a column, on the top left of the chart. + */ +function drawSvgPrintLegend(svg: Record) { + const data = Array.isArray(svg?.data) ? svg.data : [] + if (!data.length) return '' + + const seriesNames: string[] = [] + + data.forEach((serie, index) => { + seriesNames.push(` + + + ${serie.name} + + `) + }) + + // Inject the estimation legend item when necessary + if ( + ['monthly', 'yearly'].includes(displayedGranularity.value) && + !isEndDateOnPeriodEnd.value && + !isZoomed.value + ) { + seriesNames.push(` + + + ${$t('package.trends.legend_estimation')} + + `) + } + + return seriesNames.join('\n') +} + +/** + * Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports + */ +function drawNpmxLogoAndTaglineWatermark(svg: Record) { + if (!svg?.drawingArea) return '' + const npmxLogoWidthToHeight = 2.64 + const npmxLogoWidth = 100 + const npmxLogoHeight = npmxLogoWidth / npmxLogoWidthToHeight + + return ` + + + + + ${$t('tagline')} + + ` +} + // VueUiXy chart component configuration const chartConfig = computed(() => { return { @@ -1317,7 +1428,7 @@ const chartConfig = computed(() => { chart: { height: isMobile.value ? 950 : 600, backgroundColor: colors.value.bg, - padding: { bottom: 36, right: 100 }, // padding right is set to leave space of last datapoint label(s) + padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 100 }, // padding right is set to leave space of last datapoint label(s) userOptions: { buttons: { pdf: false, labels: false, fullscreen: false, table: false, tooltip: false }, buttonTitles: { @@ -1356,6 +1467,7 @@ const chartConfig = computed(() => { }, grid: { stroke: colors.value.border, + showHorizontalLines: true, labels: { fontSize: isMobile.value ? 24 : 16, color: pending.value ? colors.value.border : colors.value.fgSubtle, @@ -1364,12 +1476,13 @@ const chartConfig = computed(() => { granularity: getGranularityLabel(selectedGranularity.value), facet: $t('package.trends.items.downloads'), }), - xLabel: isMultiPackageMode.value ? '' : xAxisLabel.value, // for multiple series, names are displayed in the chart's legend yLabelOffsetX: 12, fontSize: isMobile.value ? 32 : 24, }, xAxisLabels: { - show: false, + show: true, + showOnlyAtModulo: true, + modulo: 12, values: chartData.value?.dates, datetimeFormatter: { enable: true, @@ -1570,7 +1683,7 @@ const chartConfig = computed(() => {
-
+
{ v-html="drawLastDatapointLabel(svg)" /> + + + + + + @@ -1623,10 +1745,7 @@ const chartConfig = computed(() => { -