ActivityCharts: smoothing toggle (Raw/10s/20s) for all line chart metrics
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user