From e5fe32cb247cf6ae38a7c2ab17d19b7848207103 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 10:46:38 +0100 Subject: [PATCH 1/6] feat: implement alt copy for versions chart --- .../Package/VersionDistribution.vue | 34 +++++++-- app/utils/charts.ts | 74 ++++++++++++++++++- i18n/locales/en.json | 6 +- i18n/locales/fr-FR.json | 6 +- i18n/schema.json | 12 +++ lunaria/files/en-GB.json | 6 +- lunaria/files/en-US.json | 6 +- lunaria/files/fr-FR.json | 6 +- 8 files changed, 138 insertions(+), 12 deletions(-) diff --git a/app/components/Package/VersionDistribution.vue b/app/components/Package/VersionDistribution.vue index ae76102b4b..bb6e7e0295 100644 --- a/app/components/Package/VersionDistribution.vue +++ b/app/components/Package/VersionDistribution.vue @@ -9,6 +9,7 @@ import { drawNpmxLogoAndTaglineWatermark, } from '~/composables/useChartWatermark' import TooltipApp from '~/components/Tooltip/App.vue' +import { copyAltTextForVersionsBarChart } from '~/utils/charts' const props = defineProps<{ packageName: string @@ -16,6 +17,8 @@ const props = defineProps<{ }>() const { accentColors, selectedAccentColor } = useAccentColor() +const { copy, copied } = useClipboard() + const colorMode = useColorMode() const resolvedMode = shallowRef<'light' | 'dark'>('light') const rootEl = shallowRef(null) @@ -190,14 +193,14 @@ const chartConfig = computed(() => { fullscreen: false, table: false, tooltip: false, - altCopy: false, // TODO: set to true to enable the alt copy feature + altCopy: true, }, buttonTitles: { csv: $t('package.trends.download_file', { fileType: 'CSV' }), img: $t('package.trends.download_file', { fileType: 'PNG' }), svg: $t('package.trends.download_file', { fileType: 'SVG' }), annotator: $t('package.trends.toggle_annotator'), - altCopy: undefined, // TODO: set to proper translation key + altCopy: $t('package.trends.copy_alt.button_label'), // Do not make this text dependant on the `copied` variable, since this would re-render the component, which is undesirable if the minimap was used to select a time frame. }, callbacks: { img: args => { @@ -230,10 +233,19 @@ const chartConfig = computed(() => { loadFile(url, buildExportFilename('svg')) URL.revokeObjectURL(url) }, - // altCopy: ({ dataset: dst, config: cfg }: { dataset: Array; config: VueUiXyConfig}) => { - // // TODO: implement a reusable copy-alt-text-to-clipboard feature based on the dataset & configuration - // console.log({ dst, cfg}) - // } + altCopy: ({ dataset: dst, config: cfg }) => + copyAltTextForVersionsBarChart({ + dataset: dst, + config: { + ...cfg, + datapointLabels: xAxisLabels.value, + dateRangeLabel: dateRangeLabel.value, + semverGroupingMode: groupingMode.value, + copy, + $t, + numberFormatter: compactNumberFormatter.value.format, + }, + }), }, }, grid: { @@ -575,6 +587,16 @@ const chartConfig = computed(() => { aria-hidden="true" /> + diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 84e071ede0..ebf35086b2 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -1,4 +1,9 @@ -import type { AltCopyArgs, VueUiXyConfig, VueUiXyDatasetLineItem } from 'vue-data-ui' +import type { + AltCopyArgs, + VueUiXyConfig, + VueUiXyDatasetBarItem, + VueUiXyDatasetLineItem, +} from 'vue-data-ui' import type { ChartTimeGranularity } from '~/types/chart' export function sum(numbers: number[]): number { @@ -413,6 +418,11 @@ export type TrendLineDataset = { [key: string]: unknown } | null +export type VersionsBarDataset = { + bars: VueUiXyDatasetBarItem[] + [key: string]: unknown +} | null + export type TrendTranslateKey = number | 'package.trends.y_axis_label' | (string & {}) export type TrendTranslateFunction = { @@ -431,6 +441,12 @@ export type TrendLineConfig = VueUiXyConfig & { numberFormatter: (value: number) => string } +export type VersionsBarConfig = Omit< + TrendLineConfig, + 'formattedDates' | 'hasEstimation' | 'formattedDatasetValues' | 'granularity' +> & { datapointLabels: string[]; dateRangeLabel: string; semverGroupingMode: string } + +// Used for TrendsChart.vue export function createAltTextForTrendLineChart({ dataset, config, @@ -519,3 +535,59 @@ export async function copyAltTextForTrendLineChart({ const altText = createAltTextForTrendLineChart({ dataset, config }) await config.copy(altText) } + +// Used for VersionDistribution.vue +export function createAltTextForVersionsBarChart({ + dataset, + config, +}: AltCopyArgs) { + if (!dataset) return '' + + const versions = dataset.bars[0]?.series.map((value, i) => ({ + name: config.datapointLabels[i], + downloads: config.numberFormatter(value), + })) + + const versionWithMaxDownloads = versions?.reduce((max, current) => { + return current.downloads > max.downloads ? current : max + }) + + const per_version_analysis = versions + ?.toReversed() + .filter(v => v.name !== versionWithMaxDownloads?.name) + .map(v => + config.$t(`package.versions.copy_alt.per_version_analysis`, { + version: v?.name ?? '-', + downloads: v?.downloads ?? '-', + }), + ) + .join(', ') + + const semver_grouping_mode = + config.semverGroupingMode === 'major' + ? config.$t('package.versions.grouping_major') + : config.$t('package.versions.grouping_minor') + + const altText = `${config.$t('package.versions.copy_alt.general_description', { + package_name: dataset?.bars[0]?.name ?? '-', + versions_count: versions?.length ?? '0', + semver_grouping_mode: semver_grouping_mode.toLocaleLowerCase(), + first_version: versions?.[0]?.name ?? '-', + last_version: versions?.at(-1)?.name ?? '-', + date_range_label: config.dateRangeLabel ?? '-', + max_downloaded_version: versionWithMaxDownloads?.name ?? '-', + max_version_downloads: versionWithMaxDownloads?.downloads ?? '-', + per_version_analysis, + watermark: config.$t('package.trends.copy_alt.watermark'), + })}` + + return altText +} + +export async function copyAltTextForVersionsBarChart({ + dataset, + config, +}: AltCopyArgs) { + const altText = createAltTextForVersionsBarChart({ dataset, config }) + await config.copy(altText) +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index b7a14fe3ec..3e4354e485 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -312,7 +312,11 @@ "filter_help": "Semver range filter help", "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.", "filter_tooltip_link": "semver range", - "no_matches": "No versions match this range" + "no_matches": "No versions match this range", + "copy_alt": { + "per_version_analysis": "{version} version was downloaded {downloads} times", + "general_description": "Bar chart showing per-version downloads for {versions_count} {semver_grouping_mode} versions of the {package_name} package, {date_range_label} from the {first_version} version to the {last_version} version. The most downloaded version is {max_downloaded_version} with {max_version_downloads} downloads. {per_version_analysis} {watermark}." + } }, "dependencies": { "title": "Dependency ({count}) | Dependencies ({count})", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 1a62055023..01cfefe21e 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -312,7 +312,11 @@ "filter_help": "Infos sur le filtre de plage semver", "filter_tooltip": "Filtrer les versions avec une {link}. Par exemple, ^3.0.0 affiche toutes les versions 3.x.", "filter_tooltip_link": "plage semver", - "no_matches": "Aucune version ne correspond à cette plage" + "no_matches": "Aucune version ne correspond à cette plage", + "copy_alt": { + "per_version_analysis": "La version {version} a été téléchargée {downloads} fois", + "general_description": "Graphique en barres montrant les téléchargements par version pour {versions_count} versions {semver_grouping_mode} du paquet {package_name}, {date_range_label} de la version {first_version} à la version {last_version}. La version la plus téléchargée est {max_downloaded_version} avec {max_version_downloads} téléchargements. {per_version_analysis} {watermark}." + } }, "dependencies": { "title": "Dépendances ({count})", diff --git a/i18n/schema.json b/i18n/schema.json index ce915ed5ec..aa7681bc31 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -942,6 +942,18 @@ }, "no_matches": { "type": "string" + }, + "copy_alt": { + "type": "object", + "properties": { + "per_version_analysis": { + "type": "string" + }, + "general_description": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index 12789651b4..4ca9ba59df 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -311,7 +311,11 @@ "filter_help": "Semver range filter help", "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.", "filter_tooltip_link": "semver range", - "no_matches": "No versions match this range" + "no_matches": "No versions match this range", + "copy_alt": { + "per_version_analysis": "{version} version was downloaded {downloads} times", + "general_description": "Bar chart showing per-version downloads for {versions_count} {semver_grouping_mode} versions of the {package_name} package, {date_range_label} from the {first_version} version to the {last_version} version. The most downloaded version is {max_downloaded_version} with {max_version_downloads} downloads. {per_version_analysis} {watermark}." + } }, "dependencies": { "title": "Dependency ({count}) | Dependencies ({count})", diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index 7b621707ec..69a66c3f7f 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -311,7 +311,11 @@ "filter_help": "Semver range filter help", "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.", "filter_tooltip_link": "semver range", - "no_matches": "No versions match this range" + "no_matches": "No versions match this range", + "copy_alt": { + "per_version_analysis": "{version} version was downloaded {downloads} times", + "general_description": "Bar chart showing per-version downloads for {versions_count} {semver_grouping_mode} versions of the {package_name} package, {date_range_label} from the {first_version} version to the {last_version} version. The most downloaded version is {max_downloaded_version} with {max_version_downloads} downloads. {per_version_analysis} {watermark}." + } }, "dependencies": { "title": "Dependency ({count}) | Dependencies ({count})", diff --git a/lunaria/files/fr-FR.json b/lunaria/files/fr-FR.json index 4ecef381b0..c1ed3272c5 100644 --- a/lunaria/files/fr-FR.json +++ b/lunaria/files/fr-FR.json @@ -311,7 +311,11 @@ "filter_help": "Infos sur le filtre de plage semver", "filter_tooltip": "Filtrer les versions avec une {link}. Par exemple, ^3.0.0 affiche toutes les versions 3.x.", "filter_tooltip_link": "plage semver", - "no_matches": "Aucune version ne correspond à cette plage" + "no_matches": "Aucune version ne correspond à cette plage", + "copy_alt": { + "per_version_analysis": "La version {version} a été téléchargée {downloads} fois", + "general_description": "Graphique en barres montrant les téléchargements par version pour {versions_count} versions {semver_grouping_mode} du paquet {package_name}, {date_range_label} de la version {first_version} à la version {last_version}. La version la plus téléchargée est {max_downloaded_version} avec {max_version_downloads} téléchargements. {per_version_analysis} {watermark}." + } }, "dependencies": { "title": "Dépendances ({count})", From aa5f370330d93c9e60fde0c665418001204d0222 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 10:58:15 +0100 Subject: [PATCH 2/6] fix: apply coderabbit suggestion --- app/utils/charts.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/utils/charts.ts b/app/utils/charts.ts index ebf35086b2..0784cdee28 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -543,13 +543,15 @@ export function createAltTextForVersionsBarChart({ }: AltCopyArgs) { if (!dataset) return '' - const versions = dataset.bars[0]?.series.map((value, i) => ({ - name: config.datapointLabels[i], + const series = dataset.bars[0]?.series + const versions = series?.map((value, i) => ({ + name: config.datapointLabels[i] ?? '-', + rawDownloads: value ?? 0, downloads: config.numberFormatter(value), })) const versionWithMaxDownloads = versions?.reduce((max, current) => { - return current.downloads > max.downloads ? current : max + return current.rawDownloads > max.rawDownloads ? current : max }) const per_version_analysis = versions From 07935568791b57b1d7826999be8e1b6124396372 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 10:59:49 +0100 Subject: [PATCH 3/6] fix: apply coderabbit suggestion --- app/utils/charts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 0784cdee28..0a732b5bce 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -540,14 +540,14 @@ export async function copyAltTextForTrendLineChart({ export function createAltTextForVersionsBarChart({ dataset, config, -}: AltCopyArgs) { +}: AltCopyArgs) { if (!dataset) return '' const series = dataset.bars[0]?.series const versions = series?.map((value, i) => ({ name: config.datapointLabels[i] ?? '-', rawDownloads: value ?? 0, - downloads: config.numberFormatter(value), + downloads: config.numberFormatter(value ?? 0), })) const versionWithMaxDownloads = versions?.reduce((max, current) => { From 02c9802011096028047b53e0eef1dfef5e76ad06 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 11:09:02 +0100 Subject: [PATCH 4/6] fix: apply coderabbit suggestion --- app/utils/charts.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 0a732b5bce..2dd459abdc 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -543,16 +543,17 @@ export function createAltTextForVersionsBarChart({ }: AltCopyArgs) { if (!dataset) return '' - const series = dataset.bars[0]?.series - const versions = series?.map((value, i) => ({ + const series = dataset.bars[0]?.series ?? [] + const versions = series.map((value, i) => ({ name: config.datapointLabels[i] ?? '-', rawDownloads: value ?? 0, downloads: config.numberFormatter(value ?? 0), })) - const versionWithMaxDownloads = versions?.reduce((max, current) => { - return current.rawDownloads > max.rawDownloads ? current : max - }) + const versionWithMaxDownloads = + versions.length > 0 + ? versions.reduce((max, current) => (current.rawDownloads > max.rawDownloads ? current : max)) + : undefined const per_version_analysis = versions ?.toReversed() From bf9ab398081a7ac9959fa7ea7043d0f335416c17 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 11:15:03 +0100 Subject: [PATCH 5/6] fix: apply coderabbit suggestion --- app/utils/charts.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 2dd459abdc..96deefbe72 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -544,8 +544,9 @@ export function createAltTextForVersionsBarChart({ if (!dataset) return '' const series = dataset.bars[0]?.series ?? [] - const versions = series.map((value, i) => ({ - name: config.datapointLabels[i] ?? '-', + const versions = series.map((value, index) => ({ + index, + name: config.datapointLabels[index] ?? '-', rawDownloads: value ?? 0, downloads: config.numberFormatter(value ?? 0), })) @@ -557,7 +558,7 @@ export function createAltTextForVersionsBarChart({ const per_version_analysis = versions ?.toReversed() - .filter(v => v.name !== versionWithMaxDownloads?.name) + .filter(v => v.index !== versionWithMaxDownloads?.index) .map(v => config.$t(`package.versions.copy_alt.per_version_analysis`, { version: v?.name ?? '-', From fd6e16c9a7bca8d7b99ed00e598446b6e2b238c7 Mon Sep 17 00:00:00 2001 From: graphieros Date: Thu, 26 Feb 2026 11:21:05 +0100 Subject: [PATCH 6/6] fix: apply coderabbit nitpicks --- app/utils/charts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 96deefbe72..bb253632d5 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -557,7 +557,7 @@ export function createAltTextForVersionsBarChart({ : undefined const per_version_analysis = versions - ?.toReversed() + .toReversed() .filter(v => v.index !== versionWithMaxDownloads?.index) .map(v => config.$t(`package.versions.copy_alt.per_version_analysis`, { @@ -574,10 +574,10 @@ export function createAltTextForVersionsBarChart({ const altText = `${config.$t('package.versions.copy_alt.general_description', { package_name: dataset?.bars[0]?.name ?? '-', - versions_count: versions?.length ?? '0', + versions_count: versions?.length, semver_grouping_mode: semver_grouping_mode.toLocaleLowerCase(), - first_version: versions?.[0]?.name ?? '-', - last_version: versions?.at(-1)?.name ?? '-', + first_version: versions[0]?.name ?? '-', + last_version: versions.at(-1)?.name ?? '-', date_range_label: config.dateRangeLabel ?? '-', max_downloaded_version: versionWithMaxDownloads?.name ?? '-', max_version_downloads: versionWithMaxDownloads?.downloads ?? '-',