@@ -18,6 +18,8 @@ import type {
1818 YearlyDataPoint ,
1919} from ' ~/types/chart'
2020import { DATE_INPUT_MAX } from ' ~/utils/input'
21+ import { applyDataCorrection } from ' ~/utils/chart-data-correction'
22+ import { applyBlocklistCorrection , getAnomaliesForPackages } from ' ~/utils/download-anomalies'
2123import { copyAltTextForTrendLineChart } from ' ~/utils/charts'
2224
2325const props = withDefaults (
@@ -51,6 +53,7 @@ const props = withDefaults(
5153
5254const { locale } = useI18n ()
5355const { accentColors, selectedAccentColor } = useAccentColor ()
56+ const { settings } = useSettings ()
5457const { copy, copied } = useClipboard ()
5558
5659const colorMode = useColorMode ()
@@ -929,15 +932,35 @@ watch(
929932
930933const effectiveDataSingle = computed <EvolutionData >(() => {
931934 const state = activeMetricState .value
935+ let data: EvolutionData
932936 if (
933937 selectedMetric .value === DEFAULT_METRIC_ID &&
934938 displayedGranularity .value === DEFAULT_GRANULARITY &&
935939 props .weeklyDownloads ?.length
936940 ) {
937- if (isWeeklyDataset (state .evolution ) && state .evolution .length ) return state .evolution
938- return props .weeklyDownloads
941+ data =
942+ isWeeklyDataset (state .evolution ) && state .evolution .length
943+ ? state .evolution
944+ : props .weeklyDownloads
945+ } else {
946+ data = state .evolution
939947 }
940- return state .evolution
948+
949+ if (isDownloadsMetric .value && data .length ) {
950+ const pkg = effectivePackageNames .value [0 ] ?? props .packageName ?? ' '
951+ if (settings .value .chartFilter .anomaliesFixed ) {
952+ data = applyBlocklistCorrection ({
953+ data ,
954+ packageName: pkg ,
955+ granularity: displayedGranularity .value ,
956+ })
957+ }
958+ return applyDataCorrection (
959+ data as Array <{ value: number }>,
960+ settings .value .chartFilter ,
961+ ) as EvolutionData
962+ }
963+ return data
941964})
942965
943966/**
@@ -971,7 +994,16 @@ const chartData = computed<{
971994 const pointsByPackage = new Map <string , Array <{ timestamp: number ; value: number }>>()
972995
973996 for (const pkg of names ) {
974- const data = state .evolutionsByPackage [pkg ] ?? []
997+ let data = state .evolutionsByPackage [pkg ] ?? []
998+ if (isDownloadsMetric .value && data .length ) {
999+ if (settings .value .chartFilter .anomaliesFixed ) {
1000+ data = applyBlocklistCorrection ({ data , packageName: pkg , granularity })
1001+ }
1002+ data = applyDataCorrection (
1003+ data as Array <{ value: number }>,
1004+ settings .value .chartFilter ,
1005+ ) as EvolutionData
1006+ }
9751007 const points = extractSeriesPoints (granularity , data )
9761008 pointsByPackage .set (pkg , points )
9771009 for (const p of points ) timestampSet .add (p .timestamp )
@@ -1428,6 +1460,7 @@ const chartConfig = computed<VueUiXyConfig>(() => {
14281460 img: $t (' package.trends.download_file' , { fileType: ' PNG' }),
14291461 svg: $t (' package.trends.download_file' , { fileType: ' SVG' }),
14301462 annotator: $t (' package.trends.toggle_annotator' ),
1463+ stack: $t (' package.trends.toggle_stack_mode' ),
14311464 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.
14321465 },
14331466 callbacks: {
@@ -1598,6 +1631,23 @@ const chartConfig = computed<VueUiXyConfig>(() => {
15981631 }
15991632})
16001633
1634+ const isDownloadsMetric = computed (() => selectedMetric .value === ' downloads' )
1635+ const showCorrectionControls = shallowRef (false )
1636+
1637+ const packageAnomalies = computed (() => getAnomaliesForPackages (effectivePackageNames .value ))
1638+ const hasAnomalies = computed (() => packageAnomalies .value .length > 0 )
1639+
1640+ function formatAnomalyDate(dateStr : string ) {
1641+ const [y, m, d] = dateStr .split (' -' ).map (Number )
1642+ if (! y || ! m || ! d ) return dateStr
1643+ return new Intl .DateTimeFormat (locale .value , {
1644+ year: ' numeric' ,
1645+ month: ' short' ,
1646+ day: ' numeric' ,
1647+ timeZone: ' UTC' ,
1648+ }).format (new Date (Date .UTC (y , m - 1 , d )))
1649+ }
1650+
16011651// Trigger data loading when the metric is switched
16021652watch (selectedMetric , value => {
16031653 if (! isMounted .value ) return
@@ -1686,6 +1736,117 @@ watch(selectedMetric, value => {
16861736 </button >
16871737 </div >
16881738
1739+ <!-- Download filter controls -->
1740+ <div v-if =" isDownloadsMetric" class =" flex flex-col gap-2" >
1741+ <button
1742+ type =" button"
1743+ class =" self-start flex items-center gap-1 text-2xs font-mono text-fg-subtle hover:text-fg transition-colors"
1744+ @click =" showCorrectionControls = !showCorrectionControls"
1745+ >
1746+ <span
1747+ class =" w-3.5 h-3.5 transition-transform"
1748+ :class =" showCorrectionControls ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
1749+ aria-hidden =" true"
1750+ />
1751+ {{ $t('package.trends.data_correction') }}
1752+ </button >
1753+ <div v-if =" showCorrectionControls" class =" flex items-end gap-3" >
1754+ <label class =" flex flex-col gap-1 flex-1" >
1755+ <span class =" text-2xs font-mono text-fg-subtle tracking-wide uppercase" >
1756+ {{ $t('package.trends.average_window') }}
1757+ <span class =" text-fg-muted" >({{ settings.chartFilter.averageWindow }})</span >
1758+ </span >
1759+ <input
1760+ v-model.number =" settings.chartFilter.averageWindow"
1761+ type =" range"
1762+ min =" 0"
1763+ max =" 20"
1764+ step =" 1"
1765+ class =" accent-[var(--accent-color,var(--fg-subtle))]"
1766+ />
1767+ </label >
1768+ <label class =" flex flex-col gap-1 flex-1" >
1769+ <span class =" text-2xs font-mono text-fg-subtle tracking-wide uppercase" >
1770+ {{ $t('package.trends.smoothing') }}
1771+ <span class =" text-fg-muted" >({{ settings.chartFilter.smoothingTau }})</span >
1772+ </span >
1773+ <input
1774+ v-model.number =" settings.chartFilter.smoothingTau"
1775+ type =" range"
1776+ min =" 0"
1777+ max =" 20"
1778+ step =" 1"
1779+ class =" accent-[var(--accent-color,var(--fg-subtle))]"
1780+ />
1781+ </label >
1782+ <div class =" flex flex-col gap-1 shrink-0" >
1783+ <span
1784+ class =" text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
1785+ >
1786+ {{ $t('package.trends.known_anomalies') }}
1787+ <TooltipApp interactive :to =" inModal ? '#chart-modal' : undefined" >
1788+ <button
1789+ type =" button"
1790+ class =" i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
1791+ :aria-label =" $t('package.trends.known_anomalies')"
1792+ />
1793+ <template #content >
1794+ <div class =" flex flex-col gap-3" >
1795+ <p class =" text-xs text-fg-muted" >
1796+ {{ $t('package.trends.known_anomalies_description') }}
1797+ </p >
1798+ <div v-if =" hasAnomalies" >
1799+ <p class =" text-xs text-fg-subtle font-medium" >
1800+ {{ $t('package.trends.known_anomalies_ranges') }}
1801+ </p >
1802+ <ul class =" text-xs text-fg-subtle list-disc list-inside" >
1803+ <li v-for =" a in packageAnomalies" :key =" `${a.packageName}-${a.start}`" >
1804+ {{
1805+ isMultiPackageMode
1806+ ? $t('package.trends.known_anomalies_range_named', {
1807+ packageName: a.packageName,
1808+ start: formatAnomalyDate(a.start),
1809+ end: formatAnomalyDate(a.end),
1810+ })
1811+ : $t('package.trends.known_anomalies_range', {
1812+ start: formatAnomalyDate(a.start),
1813+ end: formatAnomalyDate(a.end),
1814+ })
1815+ }}
1816+ </li >
1817+ </ul >
1818+ </div >
1819+ <p v-else class =" text-xs text-fg-muted" >
1820+ {{ $t('package.trends.known_anomalies_none', effectivePackageNames.length) }}
1821+ </p >
1822+ <div class =" flex justify-end" >
1823+ <LinkBase
1824+ to =" https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
1825+ class =" text-xs text-accent"
1826+ >
1827+ {{ $t('package.trends.known_anomalies_contribute') }}
1828+ </LinkBase >
1829+ </div >
1830+ </div >
1831+ </template >
1832+ </TooltipApp >
1833+ </span >
1834+ <label
1835+ class =" flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer"
1836+ :class =" { 'opacity-50 pointer-events-none': !hasAnomalies }"
1837+ >
1838+ <input
1839+ v-model =" settings.chartFilter.anomaliesFixed"
1840+ type =" checkbox"
1841+ :disabled =" !hasAnomalies"
1842+ class =" accent-[var(--accent-color,var(--fg-subtle))]"
1843+ />
1844+ {{ $t('package.trends.apply_correction') }}
1845+ </label >
1846+ </div >
1847+ </div >
1848+ </div >
1849+
16891850 <p v-if =" skippedPackagesWithoutGitHub.length > 0" class =" text-2xs font-mono text-fg-subtle" >
16901851 {{ $t('package.trends.contributors_skip', { count: skippedPackagesWithoutGitHub.length }) }}
16911852 {{ skippedPackagesWithoutGitHub.join(', ') }}
0 commit comments