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 XMode = 'time' | 'distance';
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 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 @@
</div>
{/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">
{#each (['line', 'histogram'] as ChartType[]) as type}
<button