diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index c1454355f3..afa81d0637 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -59,6 +59,13 @@ export interface AppSettings { } } +export const DEFAULT_CHART_FILTER: AppSettings['chartFilter'] = { + averageWindow: 0, + smoothingTau: 0, + anomaliesFixed: true, + predictionPoints: 4, +} + const DEFAULT_SETTINGS: AppSettings = { relativeDates: false, includeTypesInInstall: true, @@ -79,12 +86,7 @@ const DEFAULT_SETTINGS: AppSettings = { sidebar: { collapsed: [], }, - chartFilter: { - averageWindow: 0, - smoothingTau: 0, - anomaliesFixed: true, - predictionPoints: 4, - }, + chartFilter: { ...DEFAULT_CHART_FILTER }, timelineChart: { isZeroBased: false, showZoom: false, @@ -255,6 +257,27 @@ export function useBackgroundTheme() { } } +export function useChartFilter() { + const { settings } = useSettings() + + const isDefault = computed( + () => + settings.value.chartFilter.averageWindow === DEFAULT_CHART_FILTER.averageWindow && + settings.value.chartFilter.smoothingTau === DEFAULT_CHART_FILTER.smoothingTau && + settings.value.chartFilter.anomaliesFixed === DEFAULT_CHART_FILTER.anomaliesFixed && + settings.value.chartFilter.predictionPoints === DEFAULT_CHART_FILTER.predictionPoints, + ) + + function reset() { + settings.value.chartFilter = { ...DEFAULT_CHART_FILTER } + } + + return { + isDefault, + reset, + } +} + export function useCodeContainer() { const { settings } = useSettings() diff --git a/app/pages/settings.vue b/app/pages/settings.vue index e79f778846..edd9a6d78c 100644 --- a/app/pages/settings.vue +++ b/app/pages/settings.vue @@ -8,6 +8,7 @@ const colorMode = useColorMode() const { currentLocaleStatus, isSourceLocale } = useI18nStatus() const keyboardShortcutsEnabled = useKeyboardShortcuts() const { toggleCodeLigatures } = useCodeLigatures() +const { isDefault: isDefaultChartFilter, reset: resetChartFilter } = useChartFilter() // Create a computed property to handle locale binding properly const localeCodes = computed(() => @@ -325,6 +326,97 @@ useSeoMeta({ /> + + +
+

+ {{ $t('settings.sections.data_correction') }} +

+
+

+ {{ $t('settings.data_correction_description') }} +

+ +
+
+
+ {{ $t('package.trends.average_window') }} + + +
+ {{ settings.chartFilter.averageWindow }} + {{ $t('settings.data_correction_points') }} +
+
+
+ {{ $t('package.trends.smoothing') }} + + +
+ {{ settings.chartFilter.smoothingTau }} +
+
+
+ {{ $t('package.trends.prediction') }} + + +
+ {{ settings.chartFilter.predictionPoints }} + {{ $t('settings.data_correction_points') }} +
+
+
+ {{ $t('package.trends.known_anomalies') }} + + +
+ {{ + settings.chartFilter.anomaliesFixed + ? $t('settings.data_correction_enabled') + : $t('settings.data_correction_disabled') + }} +
+
+ +
+ + {{ $t('settings.data_correction_reset') }} + +
+
+
diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 672047fc30..9c2d27c4d0 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -273,8 +273,11 @@ "display": "Display", "search": "Search features", "language": "Language", - "keyboard_shortcuts": "Keyboard shortcuts" + "keyboard_shortcuts": "Keyboard shortcuts", + "data_correction": "Data correction" }, + "data_correction_reset": "Reset to defaults", + "data_correction_description": "Download chart correction parameters are configured on the stats and compare pages. Reset them here if needed.", "data_source": { "label": "Data source", "description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.", @@ -323,7 +326,14 @@ "keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts", "enable_code_ligatures": "Enable ligatures in code", "enable_changelog_autoscroll": "Auto scroll to requested version", - "enable_changelog_autoscroll_description": "Auto scroll to or close to the requested version at package changelog" + "enable_changelog_autoscroll_description": "Auto scroll to or close to the requested version at package changelog", + "data_correction_average_window_tooltip": "Number of data points on each side used for bidirectional moving average smoothing.", + "data_correction_smoothing_tooltip": "Time constant for forward-backward exponential smoothing. Higher values produce smoother curves.", + "data_correction_prediction_tooltip": "Number of recent data points used to estimate the current incomplete period.", + "data_correction_anomalies_tooltip": "Interpolates over known download spikes caused by bots or CI issues.", + "data_correction_enabled": "Enabled", + "data_correction_disabled": "Disabled", + "data_correction_points": "points" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", diff --git a/i18n/schema.json b/i18n/schema.json index 63dccc9991..e071478625 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -825,10 +825,19 @@ }, "keyboard_shortcuts": { "type": "string" + }, + "data_correction": { + "type": "string" } }, "additionalProperties": false }, + "data_correction_reset": { + "type": "string" + }, + "data_correction_description": { + "type": "string" + }, "data_source": { "type": "object", "properties": { @@ -975,6 +984,27 @@ }, "enable_changelog_autoscroll_description": { "type": "string" + }, + "data_correction_average_window_tooltip": { + "type": "string" + }, + "data_correction_smoothing_tooltip": { + "type": "string" + }, + "data_correction_prediction_tooltip": { + "type": "string" + }, + "data_correction_anomalies_tooltip": { + "type": "string" + }, + "data_correction_enabled": { + "type": "string" + }, + "data_correction_disabled": { + "type": "string" + }, + "data_correction_points": { + "type": "string" } }, "additionalProperties": false diff --git a/test/nuxt/composables/use-settings.spec.ts b/test/nuxt/composables/use-settings.spec.ts index 74eb043450..1829c27bff 100644 --- a/test/nuxt/composables/use-settings.spec.ts +++ b/test/nuxt/composables/use-settings.spec.ts @@ -47,6 +47,54 @@ describe('useSettings - keyboardShortcuts', () => { }) }) +describe('useSettings - chartFilter', () => { + beforeEach(() => { + vi.resetModules() + }) + + it('should have correct default values', async () => { + const { DEFAULT_CHART_FILTER } = await import('~/composables/useSettings') + expect(DEFAULT_CHART_FILTER).toEqual({ + averageWindow: 0, + smoothingTau: 0, + anomaliesFixed: true, + predictionPoints: 4, + }) + }) + + it('should report isDefault as true when all values match defaults', async () => { + const { useChartFilter } = await import('~/composables/useSettings') + const { isDefault } = useChartFilter() + expect(isDefault.value).toBe(true) + }) + + it('should report isDefault as false when any value differs', async () => { + const { useSettings, useChartFilter } = await import('~/composables/useSettings') + const { settings } = useSettings() + const { isDefault } = useChartFilter() + + settings.value.chartFilter.averageWindow = 5 + expect(isDefault.value).toBe(false) + }) + + it('should reset all values to defaults', async () => { + const { useSettings, useChartFilter, DEFAULT_CHART_FILTER } = + await import('~/composables/useSettings') + const { settings } = useSettings() + const { isDefault, reset } = useChartFilter() + + settings.value.chartFilter.averageWindow = 5 + settings.value.chartFilter.smoothingTau = 3 + settings.value.chartFilter.anomaliesFixed = false + settings.value.chartFilter.predictionPoints = 10 + expect(isDefault.value).toBe(false) + + reset() + expect(isDefault.value).toBe(true) + expect(settings.value.chartFilter).toEqual(DEFAULT_CHART_FILTER) + }) +}) + describe('useSettings - codeLigatures', () => { beforeEach(() => { vi.resetModules()