From 3231fdb4b7826329ff21cbd3c12f5f2d65827718 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Tue, 12 May 2026 23:24:33 +0200 Subject: [PATCH] ActivityCharts: add avg/P20/P80 reference lines to speed, cadence, and power line charts --- site/src/components/ActivityCharts.svelte | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/site/src/components/ActivityCharts.svelte b/site/src/components/ActivityCharts.svelte index d7541ac..3a1114d 100644 --- a/site/src/components/ActivityCharts.svelte +++ b/site/src/components/ActivityCharts.svelte @@ -147,6 +147,17 @@ }; } + // ── Reference lines (avg, P20, P80) — speed, cadence, and power ──────── + function refStats(tab: Tab, vals: number[]) { + if (tab !== 'speed' && tab !== 'cadence' && tab !== 'power') return null; + const moving = vals.filter(v => v > 0); + if (moving.length < 5) return null; + const avg = moving.reduce((a, b) => a + b, 0) / moving.length; + const sorted = [...moving].sort((a, b) => a - b); + const q = (p: number) => sorted[Math.max(0, Math.floor((sorted.length - 1) * p))]; + return { avg, p20: q(0.2), p80: q(0.8) }; + } + // ── Rendering ──────────────────────────────────────────────────────────── let themeObserver: MutationObserver | null = null; @@ -212,6 +223,24 @@ ); } + // ── Reference lines (avg, P20, P80) ───────────────────────────────────── + const ref = refStats(activeTab, metricValues); + if (ref) { + const xEnd = (data[data.length - 1] as any)?.[x]; + marks.push( + Plot.ruleY([ref.avg], { stroke: color, strokeWidth: 1.5, strokeDasharray: '6,4', strokeOpacity: 0.65 }), + Plot.ruleY([ref.p20, ref.p80], { stroke: tc.axis, strokeWidth: 1, strokeDasharray: '3,4', strokeOpacity: 0.55 }), + Plot.text( + [ + { xv: xEnd, yv: ref.avg, label: `avg ${Math.round(ref.avg)}` }, + { xv: xEnd, yv: ref.p20, label: `P20 ${Math.round(ref.p20)}` }, + { xv: xEnd, yv: ref.p80, label: `P80 ${Math.round(ref.p80)}` }, + ], + { x: 'xv', y: 'yv', text: 'label', textAnchor: 'end', dx: -4, dy: -6, fill: tc.axis, fontSize: 10 }, + ), + ); + } + 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 })),