Skip to content

Commit 88334f4

Browse files
committed
Add tweakable Hampel filter sliders to chart UI
Expose halfWindow and threshold as sliders on a second row in the data correction panel so users can tune the filter interactively.
1 parent 7dc8c0b commit 88334f4

6 files changed

Lines changed: 56 additions & 5 deletions

File tree

app/components/Package/TrendsChart.vue

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,10 @@ const effectiveDataSingle = computed<EvolutionData>(() => {
977977
978978
if (isDownloadsMetric.value && data.length) {
979979
if (settings.value.chartFilter.anomaliesFixed) {
980-
data = applyHampelCorrection(data)
980+
data = applyHampelCorrection(data, {
981+
halfWindow: settings.value.chartFilter.hampelWindow,
982+
threshold: settings.value.chartFilter.hampelThreshold,
983+
})
981984
}
982985
}
983986
@@ -1021,7 +1024,10 @@ const chartData = computed<{
10211024
let data = state.evolutionsByPackage[pkg] ?? []
10221025
if (isDownloadsMetric.value && data.length) {
10231026
if (settings.value.chartFilter.anomaliesFixed) {
1024-
data = applyHampelCorrection(data)
1027+
data = applyHampelCorrection(data, {
1028+
halfWindow: settings.value.chartFilter.hampelWindow,
1029+
threshold: settings.value.chartFilter.hampelThreshold,
1030+
})
10251031
}
10261032
}
10271033
const points = extractSeriesPoints(granularity, data)
@@ -1698,6 +1704,36 @@ watch(selectedMetric, value => {
16981704
class="accent-[var(--accent-color,var(--fg-subtle))]"
16991705
/>
17001706
</label>
1707+
</div>
1708+
<div v-if="showCorrectionControls" class="grid grid-cols-2 sm:flex items-end gap-3">
1709+
<label class="flex flex-col gap-1 flex-1">
1710+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1711+
{{ $t('package.trends.hampel_window') }}
1712+
<span class="text-fg-muted">({{ settings.chartFilter.hampelWindow }})</span>
1713+
</span>
1714+
<input
1715+
v-model.number="settings.chartFilter.hampelWindow"
1716+
type="range"
1717+
min="1"
1718+
max="10"
1719+
step="1"
1720+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1721+
/>
1722+
</label>
1723+
<label class="flex flex-col gap-1 flex-1">
1724+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1725+
{{ $t('package.trends.hampel_threshold') }}
1726+
<span class="text-fg-muted">({{ settings.chartFilter.hampelThreshold }})</span>
1727+
</span>
1728+
<input
1729+
v-model.number="settings.chartFilter.hampelThreshold"
1730+
type="range"
1731+
min="1"
1732+
max="10"
1733+
step="0.5"
1734+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1735+
/>
1736+
</label>
17011737
<div class="flex flex-col gap-1 shrink-0">
17021738
<span
17031739
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ const correctedDownloads = computed<WeeklyDataPoint[]>(() => {
186186
let data = weeklyDownloads.value as WeeklyDataPoint[]
187187
if (!data.length) return data
188188
if (settings.value.chartFilter.anomaliesFixed) {
189-
data = applyHampelCorrection(data) as WeeklyDataPoint[]
189+
data = applyHampelCorrection(data, {
190+
halfWindow: settings.value.chartFilter.hampelWindow,
191+
threshold: settings.value.chartFilter.hampelThreshold,
192+
}) as WeeklyDataPoint[]
190193
}
191194
data = applyDataCorrection(data, settings.value.chartFilter) as WeeklyDataPoint[]
192195
return data

app/composables/useSettings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export interface AppSettings {
4545
averageWindow: number
4646
smoothingTau: number
4747
anomaliesFixed: boolean
48+
hampelWindow: number
49+
hampelThreshold: number
4850
predictionPoints: number
4951
}
5052
}
@@ -69,6 +71,8 @@ const DEFAULT_SETTINGS: AppSettings = {
6971
averageWindow: 0,
7072
smoothingTau: 1,
7173
anomaliesFixed: true,
74+
hampelWindow: 3,
75+
hampelThreshold: 3,
7276
predictionPoints: 4,
7377
},
7478
}

app/utils/download-anomalies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ export function applyHampelCorrection(
3131
): EvolutionData {
3232
// halfWindow controls how many neighbors on each side to consider.
3333
// A window of 3 means we look at 7 points total (3 left + current + 3 right).
34-
const halfWindow = opts?.halfWindow ?? DEFAULT_HALF_WINDOW
34+
const halfWindow = opts?.halfWindow || DEFAULT_HALF_WINDOW
3535

3636
// threshold controls sensitivity. A value of 3 means a point must deviate
3737
// more than 3 scaled MADs from the local median to be flagged.
3838
// Higher = less sensitive, lower = more aggressive filtering.
39-
const threshold = opts?.threshold ?? DEFAULT_THRESHOLD
39+
const threshold = opts?.threshold || DEFAULT_THRESHOLD
4040

4141
// Not enough data to form a full window — return as-is.
4242
if (data.length < halfWindow * 2 + 1) return data

i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,8 @@
470470
"average_window": "Average window",
471471
"smoothing": "Smoothing",
472472
"prediction": "Prediction",
473+
"hampel_window": "Hampel window",
474+
"hampel_threshold": "Hampel threshold",
473475
"known_anomalies": "Known anomalies",
474476
"known_anomalies_description": "Interpolates over known download spikes caused by bots or CI issues.",
475477
"apply_correction": "Apply correction",

i18n/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,12 @@
14141414
"prediction": {
14151415
"type": "string"
14161416
},
1417+
"hampel_window": {
1418+
"type": "string"
1419+
},
1420+
"hampel_threshold": {
1421+
"type": "string"
1422+
},
14171423
"known_anomalies": {
14181424
"type": "string"
14191425
},

0 commit comments

Comments
 (0)