From f1fec6d8257221c17b1d5ba546174b2aa1f6c85e Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Tue, 12 May 2026 23:37:41 +0200 Subject: [PATCH] ActivityCharts: smoothing toggle (Raw/10s/20s) for all line chart metrics --- site/src/components/ActivityCharts.svelte | 41 +++++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/site/src/components/ActivityCharts.svelte b/site/src/components/ActivityCharts.svelte index 00a5318..2f09c31 100644 --- a/site/src/components/ActivityCharts.svelte +++ b/site/src/components/ActivityCharts.svelte @@ -14,10 +14,13 @@ type Tab = 'elevation' | 'speed' | 'hr' | 'cadence' | 'power'; type XMode = 'time' | 'distance'; type ChartType = 'line' | 'histogram'; + type SmoothMode = 'raw' | '10s' | '20s'; + const SMOOTH_HALF: Record = { raw: 0, '10s': 5, '20s': 10 }; let activeTab: Tab = 'elevation'; let xMode: XMode = 'time'; let chartType: ChartType = 'line'; + let smoothMode: SmoothMode = 'raw'; let chartEl: HTMLDivElement; let chart: SVGElement | null = null; @@ -183,7 +186,7 @@ onDestroy(() => { chart?.remove(); chart = null; themeObserver?.disconnect(); }); $: if (chartEl) { - activeTab; xMode; chartType; histData; histThresholds; alignZones; + activeTab; xMode; chartType; histData; histThresholds; alignZones; smoothMode; renderChart(); } @@ -228,13 +231,14 @@ // control points and visual artifacts — use linear instead. const curve = xMode === 'distance' ? 'linear' : 'monotone-x'; - // Smooth cadence and power for visual rendering only (±5 s centred window = 11 s). - // Raw data is still used for the histogram, reference-line stats, and tooltip. - const needsSmooth = activeTab === 'cadence' || activeTab === 'power'; - const smoothed = needsSmooth - ? rollingMean(data.map(d => (d as any)[yKey] as number | null), 5) - : null; - const lineData = smoothed ? data.map((d, i) => ({ ...d, [yKey]: smoothed[i] })) : data; + // Apply smoothing for visual rendering only — raw data still used for stats/histogram. + const halfWin = SMOOTH_HALF[smoothMode]; + const lineData = halfWin > 0 + ? (() => { + const s = rollingMean(data.map(d => (d as any)[yKey] as number | null), halfWin); + return data.map((d, i) => ({ ...d, [yKey]: s[i] })); + })() + : data; if (activeTab === 'cadence') { marks.push(Plot.lineY(lineData, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve })); @@ -265,9 +269,9 @@ } marks.push( - Plot.ruleX(data, Plot.pointerX({ x, stroke: tc.rule, strokeWidth: 1, strokeDasharray: '4,4' })), - Plot.dot(data, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: tc.tooltipBg, strokeWidth: 1.5 })), - Plot.text(data, Plot.pointerX({ + Plot.ruleX(lineData, Plot.pointerX({ x, stroke: tc.rule, strokeWidth: 1, strokeDasharray: '4,4' })), + Plot.dot(lineData, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: tc.tooltipBg, strokeWidth: 1.5 })), + Plot.text(lineData, Plot.pointerX({ x, y: yKey, text: (d: any) => d[yKey] != null ? `${Math.round(d[yKey])}` : '', dy: -12, @@ -426,6 +430,21 @@ {/if} + {#if chartType === 'line'} +
+ {#each (['raw', '10s', '20s'] as SmoothMode[]) as sm} + + {/each} +
+ {/if} +
{#each (['line', 'histogram'] as ChartType[]) as type}