ActivityCharts: switch slope tab to distance-weighted smoothing
Replace time-based rolling mean with a 400 m distance-weighted sliding- window average (O(n), two advancing pointers) matching the spec in SLOPE_COLORING.md. Slope values are now spatially consistent regardless of riding speed. Smoothing buttons are hidden when the slope tab is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,21 +151,33 @@
|
|||||||
return '#a855f7'; // wall
|
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<number>(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 = (() => {
|
$: slopeData = (() => {
|
||||||
if (!dist_km) return null;
|
if (!dist_km) return null;
|
||||||
const d = dist_km;
|
const raw = timeseries.elevation_m;
|
||||||
const halfWin = SMOOTH_HALF[smoothMode];
|
const pts = dist_km.map((d, i) => ({ d: d * 1000, e: raw[i] ?? 0 }));
|
||||||
const elev = halfWin > 0 ? rollingMean(timeseries.elevation_m, halfWin) : timeseries.elevation_m;
|
const slopes = computeSmoothedSlopes(pts);
|
||||||
return data.map((pt, i) => {
|
return data.map((pt, i) => ({ ...pt, slope: slopes[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 };
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function injectSlopeGradient(svg: SVGElement, pts: { dist_km: number | null; slope: number }[], w: number) {
|
function injectSlopeGradient(svg: SVGElement, pts: { dist_km: number | null; slope: number }[], w: number) {
|
||||||
@@ -526,7 +538,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if chartType === 'line'}
|
{#if chartType === 'line' && activeTab !== 'slope'}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
{#each (['raw', '10s', '20s'] as SmoothMode[]) as sm}
|
{#each (['raw', '10s', '20s'] as SmoothMode[]) as sm}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user