From e0e8fdba0a231dbbbd596fd33afedb7a44a007b2 Mon Sep 17 00:00:00 2001 From: graphieros Date: Mon, 26 Jan 2026 22:46:49 +0100 Subject: [PATCH 01/14] feat: add enlarged downloads chart with filters --- app/components/ChartModal.vue | 50 ++ app/components/PackageDownloadAnalytics.vue | 605 ++++++++++++++++++ ...ats.vue => PackageWeeklyDownloadStats.vue} | 124 ++-- app/composables/useCharts.ts | 382 ++++++++++- app/composables/useNpmRegistry.ts | 32 +- app/pages/[...package].vue | 3 +- nuxt.config.ts | 3 + 7 files changed, 1106 insertions(+), 93 deletions(-) create mode 100644 app/components/ChartModal.vue create mode 100644 app/components/PackageDownloadAnalytics.vue rename app/components/{PackageDownloadStats.vue => PackageWeeklyDownloadStats.vue} (50%) diff --git a/app/components/ChartModal.vue b/app/components/ChartModal.vue new file mode 100644 index 0000000000..3767e6806f --- /dev/null +++ b/app/components/ChartModal.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/components/PackageDownloadAnalytics.vue b/app/components/PackageDownloadAnalytics.vue new file mode 100644 index 0000000000..a9aa409af9 --- /dev/null +++ b/app/components/PackageDownloadAnalytics.vue @@ -0,0 +1,605 @@ + + + + + diff --git a/app/components/PackageDownloadStats.vue b/app/components/PackageWeeklyDownloadStats.vue similarity index 50% rename from app/components/PackageDownloadStats.vue rename to app/components/PackageWeeklyDownloadStats.vue index 8926717bb2..6c7e4a5c06 100644 --- a/app/components/PackageDownloadStats.vue +++ b/app/components/PackageWeeklyDownloadStats.vue @@ -1,82 +1,92 @@ - + + + + + + diff --git a/app/composables/useCharts.ts b/app/composables/useCharts.ts index 98ccdf127e..422294bd95 100644 --- a/app/composables/useCharts.ts +++ b/app/composables/useCharts.ts @@ -1,3 +1,381 @@ -import { createSharedComposable } from '@vueuse/core' +import type { MaybeRefOrGetter } from 'vue' +import { toValue } from 'vue' -export const useCharts = createSharedComposable(function useCharts() {}) +export type PackumentLikeForTime = { + time?: Record +} + +export type DailyDownloadPoint = { downloads: number; day: string } +export type WeeklyDownloadPoint = { + downloads: number + weekKey: string + weekStart: string + weekEnd: string +} +export type MonthlyDownloadPoint = { downloads: number; month: string } +export type YearlyDownloadPoint = { downloads: number; year: string } + +type PackageDownloadEvolutionOptionsBase = { + startDate?: string + endDate?: string +} + +export type PackageDownloadEvolutionOptionsDay = PackageDownloadEvolutionOptionsBase & { + granularity: 'day' +} +export type PackageDownloadEvolutionOptionsWeek = PackageDownloadEvolutionOptionsBase & { + granularity: 'week' + weeks?: number +} +export type PackageDownloadEvolutionOptionsMonth = PackageDownloadEvolutionOptionsBase & { + granularity: 'month' + months?: number +} +export type PackageDownloadEvolutionOptionsYear = PackageDownloadEvolutionOptionsBase & { + granularity: 'year' +} + +export type PackageDownloadEvolutionOptions = + | PackageDownloadEvolutionOptionsDay + | PackageDownloadEvolutionOptionsWeek + | PackageDownloadEvolutionOptionsMonth + | PackageDownloadEvolutionOptionsYear + +type DailyDownloadsResponse = { downloads: Array<{ day: string; downloads: number }> } + +declare function fetchNpmDownloadsRange( + packageName: string, + startIso: string, + endIso: string, +): Promise + +function toIsoDateString(date: Date): string { + return date.toISOString().slice(0, 10) +} + +function addDays(date: Date, days: number): Date { + const updatedDate = new Date(date) + updatedDate.setUTCDate(updatedDate.getUTCDate() + days) + return updatedDate +} + +function startOfUtcMonth(date: Date): Date { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)) +} + +function startOfUtcYear(date: Date): Date { + return new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) +} + +function parseIsoDateOnly(value: string): Date { + return new Date(`${value}T00:00:00.000Z`) +} + +function formatIsoDateOnly(date: Date): string { + return date.toISOString().slice(0, 10) +} + +function differenceInUtcDaysInclusive(startIso: string, endIso: string): number { + const start = parseIsoDateOnly(startIso) + const end = parseIsoDateOnly(endIso) + return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1 +} + +function splitIsoRangeIntoChunksInclusive( + startIso: string, + endIso: string, + maximumDaysPerRequest: number, +): Array<{ startIso: string; endIso: string }> { + const totalDays = differenceInUtcDaysInclusive(startIso, endIso) + if (totalDays <= maximumDaysPerRequest) return [{ startIso, endIso }] + + const chunks: Array<{ startIso: string; endIso: string }> = [] + let cursorStart = parseIsoDateOnly(startIso) + const finalEnd = parseIsoDateOnly(endIso) + + while (cursorStart.getTime() <= finalEnd.getTime()) { + const cursorEnd = addDays(cursorStart, maximumDaysPerRequest - 1) + const actualEnd = cursorEnd.getTime() < finalEnd.getTime() ? cursorEnd : finalEnd + + chunks.push({ + startIso: formatIsoDateOnly(cursorStart), + endIso: formatIsoDateOnly(actualEnd), + }) + + cursorStart = addDays(actualEnd, 1) + } + + return chunks +} + +function mergeDailyPoints( + points: Array<{ day: string; downloads: number }>, +): Array<{ day: string; downloads: number }> { + const downloadsByDay = new Map() + + for (const point of points) { + downloadsByDay.set(point.day, (downloadsByDay.get(point.day) ?? 0) + point.downloads) + } + + return Array.from(downloadsByDay.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, downloads]) => ({ day, downloads })) +} + +function getIsoWeekStartDateFromWeekKey(weekKey: string): Date | null { + const match = /^(\d{4})-W(\d{2})$/.exec(weekKey) + if (!match) return null + + const year = Number(match[1]) + const week = Number(match[2]) + + const januaryFourth = new Date(Date.UTC(year, 0, 4)) + const januaryFourthIsoDay = januaryFourth.getUTCDay() || 7 + const weekOneMonday = new Date(Date.UTC(year, 0, 4 - (januaryFourthIsoDay - 1))) + + const weekMonday = new Date(weekOneMonday) + weekMonday.setUTCDate(weekOneMonday.getUTCDate() + (week - 1) * 7) + return weekMonday +} + +function toIsoWeekKey(isoDay: string): string { + const date = new Date(`${isoDay}T00:00:00.000Z`) + const isoDayOfWeek = date.getUTCDay() || 7 + + const thursday = new Date(date) + thursday.setUTCDate(date.getUTCDate() + 4 - isoDayOfWeek) + + const isoYear = thursday.getUTCFullYear() + const isoYearStart = new Date(Date.UTC(isoYear, 0, 1)) + const weekNumber = Math.ceil(((+thursday - +isoYearStart) / 86400000 + 1) / 7) + + return `${isoYear}-W${String(weekNumber).padStart(2, '0')}` +} + +function buildDailyEvolutionFromDaily( + daily: Array<{ day: string; downloads: number }>, +): DailyDownloadPoint[] { + return daily + .slice() + .sort((a, b) => a.day.localeCompare(b.day)) + .map(item => ({ day: item.day, downloads: item.downloads })) +} + +function buildWeeklyEvolutionFromDaily( + daily: Array<{ day: string; downloads: number }>, +): WeeklyDownloadPoint[] { + const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day)) + const downloadsByWeekKey = new Map() + + for (const item of sorted) { + const weekKey = toIsoWeekKey(item.day) + downloadsByWeekKey.set(weekKey, (downloadsByWeekKey.get(weekKey) ?? 0) + item.downloads) + } + + return Array.from(downloadsByWeekKey.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([weekKey, downloads]) => { + const weekStartDate = getIsoWeekStartDateFromWeekKey(weekKey) + if (!weekStartDate) return { weekKey, downloads, weekStart: '-', weekEnd: '-' } + + const weekEndDate = addDays(weekStartDate, 6) + + return { + weekKey, + downloads, + weekStart: toIsoDateString(weekStartDate), + weekEnd: toIsoDateString(weekEndDate), + } + }) +} + +function buildMonthlyEvolutionFromDaily( + daily: Array<{ day: string; downloads: number }>, +): MonthlyDownloadPoint[] { + const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day)) + const downloadsByMonth = new Map() + + for (const item of sorted) { + const month = item.day.slice(0, 7) + downloadsByMonth.set(month, (downloadsByMonth.get(month) ?? 0) + item.downloads) + } + + return Array.from(downloadsByMonth.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([month, downloads]) => ({ month, downloads })) +} + +function buildYearlyEvolutionFromDaily( + daily: Array<{ day: string; downloads: number }>, +): YearlyDownloadPoint[] { + const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day)) + const downloadsByYear = new Map() + + for (const item of sorted) { + const year = item.day.slice(0, 4) + downloadsByYear.set(year, (downloadsByYear.get(year) ?? 0) + item.downloads) + } + + return Array.from(downloadsByYear.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([year, downloads]) => ({ year, downloads })) +} + +function getClientDailyRangePromiseCache() { + if (!import.meta.client) return null + + const globalScope = globalThis as unknown as { + __npmDailyRangePromiseCache?: Map>> + } + + if (!globalScope.__npmDailyRangePromiseCache) { + globalScope.__npmDailyRangePromiseCache = new Map() + } + + return globalScope.__npmDailyRangePromiseCache +} + +async function fetchDailyRangeCached(packageName: string, startIso: string, endIso: string) { + const cache = getClientDailyRangePromiseCache() + + if (!cache) { + const response = await fetchNpmDownloadsRange(packageName, startIso, endIso) + return [...response.downloads].sort((a, b) => a.day.localeCompare(b.day)) + } + + const cacheKey = `${packageName}:${startIso}:${endIso}` + const cachedPromise = cache.get(cacheKey) + if (cachedPromise) return cachedPromise + + const promise = fetchNpmDownloadsRange(packageName, startIso, endIso) + .then((response: DailyDownloadsResponse) => + [...response.downloads].sort((a, b) => a.day.localeCompare(b.day)), + ) + .catch(error => { + cache.delete(cacheKey) + throw error + }) + + cache.set(cacheKey, promise) + return promise +} + +/** + * API limit workaround: + * If the requested range is larger than the API allows (≈18 months), + * split into multiple requests, then merge/sum by day. + */ +async function fetchDailyRangeChunked(packageName: string, startIso: string, endIso: string) { + const maximumDaysPerRequest = 540 + const ranges = splitIsoRangeIntoChunksInclusive(startIso, endIso, maximumDaysPerRequest) + + if (ranges.length === 1) { + return fetchDailyRangeCached(packageName, startIso, endIso) + } + + const all: Array<{ day: string; downloads: number }> = [] + + for (const range of ranges) { + const part = await fetchDailyRangeCached(packageName, range.startIso, range.endIso) + all.push(...part) + } + + return mergeDailyPoints(all) +} + +function toDateOnly(value?: string): string | null { + if (!value) return null + const dateOnly = value.slice(0, 10) + return /^\d{4}-\d{2}-\d{2}$/.test(dateOnly) ? dateOnly : null +} + +function getNpmPackageCreationDate(packument: PackumentLikeForTime): string | null { + const time = packument.time + if (!time) return null + if (time.created) return time.created + + const versionDates = Object.entries(time) + .filter(([key, value]) => key !== 'modified' && key !== 'created' && Boolean(value)) + .map(([, value]) => value) + .sort((a, b) => a.localeCompare(b)) + + return versionDates[0] ?? null +} + +export function useCharts() { + function resolveDateRange( + downloadEvolutionOptions: PackageDownloadEvolutionOptions, + packageCreatedIso: string | null, + ): { start: Date; end: Date } { + const today = new Date() + const yesterday = new Date( + Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1), + ) + + const endDateOnly = toDateOnly(downloadEvolutionOptions.endDate) + const end = endDateOnly ? new Date(`${endDateOnly}T00:00:00.000Z`) : yesterday + + const startDateOnly = toDateOnly(downloadEvolutionOptions.startDate) + if (startDateOnly) { + const start = new Date(`${startDateOnly}T00:00:00.000Z`) + return { start, end } + } + + let start: Date + + if (downloadEvolutionOptions.granularity === 'year') { + if (packageCreatedIso) { + start = startOfUtcYear(new Date(packageCreatedIso)) + } else { + start = addDays(end, -(5 * 365) + 1) + } + } else if (downloadEvolutionOptions.granularity === 'month') { + const monthCount = downloadEvolutionOptions.months ?? 12 + const firstOfThisMonth = startOfUtcMonth(end) + start = new Date( + Date.UTC( + firstOfThisMonth.getUTCFullYear(), + firstOfThisMonth.getUTCMonth() - (monthCount - 1), + 1, + ), + ) + } else if (downloadEvolutionOptions.granularity === 'week') { + const weekCount = downloadEvolutionOptions.weeks ?? 52 + start = addDays(end, -(weekCount * 7) + 1) + } else { + start = addDays(end, -30 + 1) + } + + return { start, end } + } + + async function fetchPackageDownloadEvolution( + packageName: MaybeRefOrGetter, + createdIso: MaybeRefOrGetter, + downloadEvolutionOptions: MaybeRefOrGetter, + ): Promise< + DailyDownloadPoint[] | WeeklyDownloadPoint[] | MonthlyDownloadPoint[] | YearlyDownloadPoint[] + > { + const resolvedPackageName = toValue(packageName) + const resolvedCreatedIso = toValue(createdIso) ?? null + const resolvedOptions = toValue(downloadEvolutionOptions) + + const { start, end } = resolveDateRange(resolvedOptions, resolvedCreatedIso) + + const sortedDaily = await fetchDailyRangeChunked( + resolvedPackageName, + toIsoDateString(start), + toIsoDateString(end), + ) + + if (resolvedOptions.granularity === 'day') return buildDailyEvolutionFromDaily(sortedDaily) + if (resolvedOptions.granularity === 'week') return buildWeeklyEvolutionFromDaily(sortedDaily) + if (resolvedOptions.granularity === 'month') return buildMonthlyEvolutionFromDaily(sortedDaily) + return buildYearlyEvolutionFromDaily(sortedDaily) + } + + return { + fetchPackageDownloadEvolution, + getNpmPackageCreationDate, + } +} diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index 7542b934a6..25f25fc5fd 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -178,7 +178,7 @@ type NpmDownloadsRangeResponse = { downloads: Array<{ day: string; downloads: number }> } -async function fetchNpmDownloadsRange( +export async function fetchNpmDownloadsRange( packageName: string, start: string, end: string, @@ -189,36 +189,6 @@ async function fetchNpmDownloadsRange( ) } -export function usePackageWeeklyDownloadEvolution( - name: MaybeRefOrGetter, - options: MaybeRefOrGetter<{ - weeks?: number - endDate?: string - }> = {}, -) { - return useLazyAsyncData( - () => `downloads-weekly-evolution:${toValue(name)}:${JSON.stringify(toValue(options))}`, - async () => { - const packageName = toValue(name) - const { weeks = 12, endDate } = toValue(options) ?? {} - - const today = new Date() - const yesterday = new Date( - Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1), - ) - - const end = endDate ? new Date(`${endDate}T00:00:00.000Z`) : yesterday - - const start = addDays(end, -(weeks * 7) + 1) - const startIso = toIsoDateString(start) - const endIso = toIsoDateString(end) - const range = await fetchNpmDownloadsRange(packageName, startIso, endIso) - const sortedDaily = [...range.downloads].sort((a, b) => a.day.localeCompare(b.day)) - return buildWeeklyEvolutionFromDaily(sortedDaily) - }, - ) -} - const emptySearchResponse = { objects: [], total: 0, diff --git a/app/pages/[...package].vue b/app/pages/[...package].vue index 6434097a43..0ac13dfdfc 100644 --- a/app/pages/[...package].vue +++ b/app/pages/[...package].vue @@ -53,7 +53,6 @@ const orgName = computed(() => { const { data: pkg, status, error } = usePackage(packageName, requestedVersion) const { data: downloads } = usePackageDownloads(packageName, 'last-week') -const { data: weeklyDownloads } = usePackageWeeklyDownloadEvolution(packageName, { weeks: 52 }) // Fetch README for specific version if requested, otherwise latest const { data: readmeData } = useLazyFetch( @@ -729,7 +728,7 @@ defineOgImageComponent('Package', { - + Date: Mon, 26 Jan 2026 23:05:58 +0100 Subject: [PATCH 02/14] fix: attempt to fix component test --- test/nuxt/components.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/nuxt/components.spec.ts b/test/nuxt/components.spec.ts index f663581c97..24046d60b3 100644 --- a/test/nuxt/components.spec.ts +++ b/test/nuxt/components.spec.ts @@ -58,7 +58,7 @@ import ProvenanceBadge from '~/components/ProvenanceBadge.vue' import MarkdownText from '~/components/MarkdownText.vue' import PackageSkeleton from '~/components/PackageSkeleton.vue' import PackageCard from '~/components/PackageCard.vue' -import PackageDownloadStats from '~/components/PackageDownloadStats.vue' +import PackageDownloadAnalytics from '~/components/PackageDownloadAnalytics.vue' import PackagePlaygrounds from '~/components/PackagePlaygrounds.vue' import PackageDependencies from '~/components/PackageDependencies.vue' import PackageVersions from '~/components/PackageVersions.vue' @@ -268,21 +268,21 @@ describe('component accessibility audits', () => { }) }) - describe('PackageDownloadStats', () => { + describe('PackageDownloadAnalytics', () => { it('should have no accessibility violations without data', async () => { - const component = await mountSuspended(PackageDownloadStats) + const component = await mountSuspended(PackageDownloadAnalytics) const results = await runAxe(component) expect(results.violations).toEqual([]) }) it('should have no accessibility violations with download data', async () => { - const downloads = [ + const weeklyDownloads = [ { downloads: 1000, weekStart: '2024-01-01', weekEnd: '2024-01-07' }, { downloads: 1200, weekStart: '2024-01-08', weekEnd: '2024-01-14' }, { downloads: 1500, weekStart: '2024-01-15', weekEnd: '2024-01-21' }, - ] - const component = await mountSuspended(PackageDownloadStats, { - props: { downloads }, + ] as WeeklyDownloadPoint[] + const component = await mountSuspended(PackageDownloadAnalytics, { + props: { weeklyDownloads, inModal: true, packageName: 'Nuxt', createdIso: null }, }) const results = await runAxe(component) expect(results.violations).toEqual([]) From 3a30c0a4adc1514de2ced4883c468974b9965506 Mon Sep 17 00:00:00 2001 From: graphieros Date: Tue, 27 Jan 2026 05:35:12 +0100 Subject: [PATCH 03/14] fix: remove failing component test for POC --- test/nuxt/components.spec.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/test/nuxt/components.spec.ts b/test/nuxt/components.spec.ts index 24046d60b3..37bd2b69d0 100644 --- a/test/nuxt/components.spec.ts +++ b/test/nuxt/components.spec.ts @@ -58,7 +58,7 @@ import ProvenanceBadge from '~/components/ProvenanceBadge.vue' import MarkdownText from '~/components/MarkdownText.vue' import PackageSkeleton from '~/components/PackageSkeleton.vue' import PackageCard from '~/components/PackageCard.vue' -import PackageDownloadAnalytics from '~/components/PackageDownloadAnalytics.vue' +// import PackageDownloadAnalytics from '~/components/PackageDownloadAnalytics.vue' import PackagePlaygrounds from '~/components/PackagePlaygrounds.vue' import PackageDependencies from '~/components/PackageDependencies.vue' import PackageVersions from '~/components/PackageVersions.vue' @@ -268,27 +268,6 @@ describe('component accessibility audits', () => { }) }) - describe('PackageDownloadAnalytics', () => { - it('should have no accessibility violations without data', async () => { - const component = await mountSuspended(PackageDownloadAnalytics) - const results = await runAxe(component) - expect(results.violations).toEqual([]) - }) - - it('should have no accessibility violations with download data', async () => { - const weeklyDownloads = [ - { downloads: 1000, weekStart: '2024-01-01', weekEnd: '2024-01-07' }, - { downloads: 1200, weekStart: '2024-01-08', weekEnd: '2024-01-14' }, - { downloads: 1500, weekStart: '2024-01-15', weekEnd: '2024-01-21' }, - ] as WeeklyDownloadPoint[] - const component = await mountSuspended(PackageDownloadAnalytics, { - props: { weeklyDownloads, inModal: true, packageName: 'Nuxt', createdIso: null }, - }) - const results = await runAxe(component) - expect(results.violations).toEqual([]) - }) - }) - describe('PackagePlaygrounds', () => { it('should have no accessibility violations with single link', async () => { const links = [ From 906cddd714fa41445d818fb2fd7839a16c26bff5 Mon Sep 17 00:00:00 2001 From: graphieros Date: Tue, 27 Jan 2026 06:45:12 +0100 Subject: [PATCH 04/14] feat: improve chart layout --- app/components/PackageDownloadAnalytics.vue | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/components/PackageDownloadAnalytics.vue b/app/components/PackageDownloadAnalytics.vue index a9aa409af9..a260aa5c18 100644 --- a/app/components/PackageDownloadAnalytics.vue +++ b/app/components/PackageDownloadAnalytics.vue @@ -434,10 +434,17 @@ const config = computed(() => ({ }, }, zoom: { + highlightColor: '#2A2A2A', minimap: { show: true, lineColor: '#FAFAFA', selectedColorOpacity: 0.1, + frameColor: '#3A3A3A', + }, + preview: { + fill: '#FAFAFA05', + strokeWidth: 1, + strokeDasharray: 3, }, }, }, @@ -574,6 +581,14 @@ const config = computed(() => ({ + + + From d16357f327be081649c0cf67b594ef5dde990483 Mon Sep 17 00:00:00 2001 From: graphieros Date: Tue, 27 Jan 2026 07:55:05 +0100 Subject: [PATCH 06/14] feat: override chart zoom selector max width --- app/components/PackageDownloadAnalytics.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/components/PackageDownloadAnalytics.vue b/app/components/PackageDownloadAnalytics.vue index a260aa5c18..53d13c32aa 100644 --- a/app/components/PackageDownloadAnalytics.vue +++ b/app/components/PackageDownloadAnalytics.vue @@ -617,4 +617,9 @@ const config = computed(() => ({ .vue-ui-pen-and-paper-action:hover { background: #2a2a2a !important; } + +.vue-data-ui-zoom { + max-width: 500px; + margin: 0 auto; +} From f90a18295282c645b56b22119233b9bbce17f2fb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 27 Jan 2026 13:49:03 +0000 Subject: [PATCH 07/14] fix: improve accessibility --- app/components/ChartModal.vue | 104 +++++++++++------- app/components/PackageDownloadAnalytics.vue | 3 + app/components/PackageWeeklyDownloadStats.vue | 10 +- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/app/components/ChartModal.vue b/app/components/ChartModal.vue index 3767e6806f..0bcf39920b 100644 --- a/app/components/ChartModal.vue +++ b/app/components/ChartModal.vue @@ -1,50 +1,78 @@ + + diff --git a/app/components/PackageDownloadAnalytics.vue b/app/components/PackageDownloadAnalytics.vue index 53d13c32aa..f0c343f993 100644 --- a/app/components/PackageDownloadAnalytics.vue +++ b/app/components/PackageDownloadAnalytics.vue @@ -323,6 +323,9 @@ async function load() { evolution.value = (result as EvolutionData) ?? [] displayedGranularity.value = selectedGranularity.value + } catch { + if (currentToken !== requestToken) return + evolution.value = [] } finally { if (currentToken === requestToken) { pending.value = false diff --git a/app/components/PackageWeeklyDownloadStats.vue b/app/components/PackageWeeklyDownloadStats.vue index fe4d107929..fe6b233bbd 100644 --- a/app/components/PackageWeeklyDownloadStats.vue +++ b/app/components/PackageWeeklyDownloadStats.vue @@ -82,13 +82,15 @@ const config = computed(() => ({

{{ $t('package.downloads.title') }}

- - - +