diff --git a/site/src/components/ActivityCharts.svelte b/site/src/components/ActivityCharts.svelte index f6ff018..a251c5b 100644 --- a/site/src/components/ActivityCharts.svelte +++ b/site/src/components/ActivityCharts.svelte @@ -151,21 +151,33 @@ return '#a855f7'; // wall } + // Distance-weighted sliding-window slope smoothing (400 m window). + // O(n) via two advancing pointers. Matches SLOPE_COLORING.md spec. + function computeSmoothedSlopes(pts: { d: number; e: number }[], windowM = 400): number[] { + if (pts.length < 2) return pts.map(() => 0); + const half = windowM / 2; + const segs: { d: number; slope: number; len: number }[] = []; + for (let i = 0; i < pts.length - 1; i++) { + const len = pts[i + 1].d - pts[i].d; + segs.push({ d: (pts[i].d + pts[i + 1].d) / 2, slope: len > 0.5 ? (pts[i + 1].e - pts[i].e) / len * 100 : 0, len }); + } + const slopes = new Array(pts.length); + let lo = 0, hi = 0, sumW = 0, sumS = 0; + for (let i = 0; i < pts.length; i++) { + const center = pts[i].d; + while (hi < segs.length && segs[hi].d <= center + half) { sumW += segs[hi].len; sumS += segs[hi].slope * segs[hi].len; hi++; } + while (lo < hi && segs[lo].d < center - half) { sumW -= segs[lo].len; sumS -= segs[lo].slope * segs[lo].len; lo++; } + slopes[i] = sumW > 0.5 ? sumS / sumW : 0; + } + return slopes; + } + $: slopeData = (() => { if (!dist_km) return null; - const d = dist_km; - const halfWin = SMOOTH_HALF[smoothMode]; - const elev = halfWin > 0 ? rollingMean(timeseries.elevation_m, halfWin) : timeseries.elevation_m; - return data.map((pt, i) => { - const e0 = elev[i] ?? 0; - let slope = 0; - if (i < d.length - 1) { - const dDist = (d[i + 1] - d[i]) * 1000; - const e1 = elev[i + 1] ?? e0; - if (dDist > 0.5) slope = ((e1 - e0) / dDist) * 100; - } - return { ...pt, elevation: elev[i], slope }; - }); + const raw = timeseries.elevation_m; + const pts = dist_km.map((d, i) => ({ d: d * 1000, e: raw[i] ?? 0 })); + const slopes = computeSmoothedSlopes(pts); + return data.map((pt, i) => ({ ...pt, slope: slopes[i] })); })(); function injectSlopeGradient(svg: SVGElement, pts: { dist_km: number | null; slope: number }[], w: number) { @@ -526,7 +538,7 @@ {/if} - {#if chartType === 'line'} + {#if chartType === 'line' && activeTab !== 'slope'}
{#each (['raw', '10s', '20s'] as SmoothMode[]) as sm}