ActivityCharts: smoothing toggle (Raw/10s/20s) for all line chart metrics

This commit is contained in:
Davide Scaini
2026-05-12 23:37:41 +02:00
parent a5db6142b3
commit f1fec6d825
+30 -11
View File
@@ -14,10 +14,13 @@
type Tab = 'elevation' | 'speed' | 'hr' | 'cadence' | 'power'; type Tab = 'elevation' | 'speed' | 'hr' | 'cadence' | 'power';
type XMode = 'time' | 'distance'; type XMode = 'time' | 'distance';
type ChartType = 'line' | 'histogram'; type ChartType = 'line' | 'histogram';
type SmoothMode = 'raw' | '10s' | '20s';
const SMOOTH_HALF: Record<SmoothMode, number> = { raw: 0, '10s': 5, '20s': 10 };
let activeTab: Tab = 'elevation'; let activeTab: Tab = 'elevation';
let xMode: XMode = 'time'; let xMode: XMode = 'time';
let chartType: ChartType = 'line'; let chartType: ChartType = 'line';
let smoothMode: SmoothMode = 'raw';
let chartEl: HTMLDivElement; let chartEl: HTMLDivElement;
let chart: SVGElement | null = null; let chart: SVGElement | null = null;
@@ -183,7 +186,7 @@
onDestroy(() => { chart?.remove(); chart = null; themeObserver?.disconnect(); }); onDestroy(() => { chart?.remove(); chart = null; themeObserver?.disconnect(); });
$: if (chartEl) { $: if (chartEl) {
activeTab; xMode; chartType; histData; histThresholds; alignZones; activeTab; xMode; chartType; histData; histThresholds; alignZones; smoothMode;
renderChart(); renderChart();
} }
@@ -228,13 +231,14 @@
// control points and visual artifacts — use linear instead. // control points and visual artifacts — use linear instead.
const curve = xMode === 'distance' ? 'linear' : 'monotone-x'; const curve = xMode === 'distance' ? 'linear' : 'monotone-x';
// Smooth cadence and power for visual rendering only (±5 s centred window = 11 s). // Apply smoothing for visual rendering only — raw data still used for stats/histogram.
// Raw data is still used for the histogram, reference-line stats, and tooltip. const halfWin = SMOOTH_HALF[smoothMode];
const needsSmooth = activeTab === 'cadence' || activeTab === 'power'; const lineData = halfWin > 0
const smoothed = needsSmooth ? (() => {
? rollingMean(data.map(d => (d as any)[yKey] as number | null), 5) const s = rollingMean(data.map(d => (d as any)[yKey] as number | null), halfWin);
: null; return data.map((d, i) => ({ ...d, [yKey]: s[i] }));
const lineData = smoothed ? data.map((d, i) => ({ ...d, [yKey]: smoothed[i] })) : data; })()
: data;
if (activeTab === 'cadence') { if (activeTab === 'cadence') {
marks.push(Plot.lineY(lineData, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve })); marks.push(Plot.lineY(lineData, { x, y: yKey, stroke: color, strokeWidth: 1.5, curve }));
@@ -265,9 +269,9 @@
} }
marks.push( marks.push(
Plot.ruleX(data, Plot.pointerX({ x, stroke: tc.rule, strokeWidth: 1, strokeDasharray: '4,4' })), Plot.ruleX(lineData, 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.dot(lineData, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: tc.tooltipBg, strokeWidth: 1.5 })),
Plot.text(data, Plot.pointerX({ Plot.text(lineData, Plot.pointerX({
x, y: yKey, x, y: yKey,
text: (d: any) => d[yKey] != null ? `${Math.round(d[yKey])}` : '', text: (d: any) => d[yKey] != null ? `${Math.round(d[yKey])}` : '',
dy: -12, dy: -12,
@@ -426,6 +430,21 @@
</div> </div>
{/if} {/if}
{#if chartType === 'line'}
<div class="flex items-center gap-1">
{#each (['raw', '10s', '20s'] as SmoothMode[]) as sm}
<button
class="px-2 py-1 rounded transition-colors"
class:bg-zinc-800={smoothMode === sm}
class:text-white={smoothMode === sm}
class:hover:text-zinc-300={smoothMode !== sm}
on:click={() => smoothMode = sm}
title="Smoothing window"
>{sm === 'raw' ? '~ Raw' : sm}</button>
{/each}
</div>
{/if}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#each (['line', 'histogram'] as ChartType[]) as type} {#each (['line', 'histogram'] as ChartType[]) as type}
<button <button