diff --git a/site/src/components/ActivityCharts.svelte b/site/src/components/ActivityCharts.svelte index 44608a6..00a5318 100644 --- a/site/src/components/ActivityCharts.svelte +++ b/site/src/components/ActivityCharts.svelte @@ -147,6 +147,20 @@ }; } + // ── Rolling mean (display only — raw data used for stats/histogram) ──── + function rollingMean(vals: (number | null)[], halfWin: number): (number | null)[] { + return vals.map((_, i) => { + const lo = Math.max(0, i - halfWin); + const hi = Math.min(vals.length - 1, i + halfWin); + let sum = 0, count = 0; + for (let j = lo; j <= hi; j++) { + const v = vals[j]; + if (v != null) { sum += v; count++; } + } + return count > 0 ? sum / count : null; + }); + } + // ── Reference lines (avg, P20, P80) — speed, cadence, power, hr ──────── function refStats(tab: Tab, vals: number[]) { if (tab !== 'speed' && tab !== 'cadence' && tab !== 'power' && tab !== 'hr') return null; @@ -214,12 +228,20 @@ // 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; + if (activeTab === 'cadence') { - marks.push(Plot.lineY(data, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve })); + marks.push(Plot.lineY(lineData, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve })); } else { marks.push( - Plot.areaY(data, { x, y: yKey, fill: color, fillOpacity: 0.15, curve }), - Plot.lineY(data, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve }), + Plot.areaY(lineData, { x, y: yKey, fill: color, fillOpacity: 0.15, curve }), + Plot.lineY(lineData, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve }), ); }