Activity map: extend track coloring to HR, power, elevation, cadence
Refactor the metric gradient code into shared _computeProgress + _buildGradient helpers. Hovering any of speed/HR/power/elevation/cadence stats switches the track to a blue→green→yellow→red gradient for that metric. A small legend (min ·gradient bar· max + label) appears in the bottom-left corner of the map while active. Absolute elevation used (not slope), so blue=valleys, red=peaks.
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
export let initialCoords: [number, number][] | null = null;
|
||||
export let accentColor: string = '#00c8ff';
|
||||
export let hoveredIdx: number | null = null;
|
||||
/** When 'speed', colors the track by speed using a blue→green→yellow→red gradient. */
|
||||
export let colorMode: 'default' | 'speed' = 'default';
|
||||
/** Colors the track by a metric value using a blue→green→yellow→red gradient. */
|
||||
export let colorMode: 'default' | 'speed' | 'hr' | 'power' | 'elevation' | 'cadence' = 'default';
|
||||
|
||||
let mapEl: HTMLDivElement;
|
||||
let map: any;
|
||||
@@ -32,13 +32,13 @@
|
||||
1, accentColor,
|
||||
];
|
||||
|
||||
// Blue → green → yellow → red scale, t ∈ [0, 1]
|
||||
function _speedColor(t: number): string {
|
||||
// Shared blue → green → yellow → red scale, t ∈ [0, 1]
|
||||
function _linearColor(t: number): string {
|
||||
const stops: [number, [number, number, number]][] = [
|
||||
[0, [59, 130, 246]], // blue-500
|
||||
[0, [59, 130, 246]], // blue-500 (low)
|
||||
[0.33, [74, 222, 128]], // green-400
|
||||
[0.66, [250, 204, 21 ]], // yellow-400
|
||||
[1, [239, 68, 68 ]], // red-500
|
||||
[1, [239, 68, 68 ]], // red-500 (high)
|
||||
];
|
||||
for (let i = 0; i < stops.length - 1; i++) {
|
||||
const [t0, c0] = stops[i], [t1, c1] = stops[i + 1];
|
||||
@@ -50,49 +50,66 @@
|
||||
return '#60a5fa';
|
||||
}
|
||||
|
||||
function buildSpeedGradient(ts: Timeseries): any[] | null {
|
||||
const t = ts.t, speeds = ts.speed_kmh;
|
||||
if (!t?.length || !speeds?.length) return null;
|
||||
const n = Math.min(t.length, speeds.length);
|
||||
|
||||
// Compute cumulative distance via speed integration so fast sections
|
||||
// map to proportionally longer segments on the track (vs time-based).
|
||||
const cumDist: number[] = [0];
|
||||
let lastSpd = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const spd = speeds[i] != null ? speeds[i]! : lastSpd;
|
||||
if (speeds[i] != null) lastSpd = spd;
|
||||
cumDist.push(cumDist[i - 1] + (spd / 3.6) * (t[i] - t[i - 1]));
|
||||
// Cumulative-distance progress array from speed integration.
|
||||
// Fast sections occupy proportionally more of [0,1] than slow ones,
|
||||
// matching their visual length on the track.
|
||||
function _computeProgress(ts: Timeseries): number[] | null {
|
||||
const { t, speed_kmh } = ts;
|
||||
if (!t?.length) return null;
|
||||
const n = t.length;
|
||||
if (speed_kmh?.length) {
|
||||
const cum: number[] = [0];
|
||||
let last = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const s = speed_kmh[i] != null ? speed_kmh[i]! : last;
|
||||
if (speed_kmh[i] != null) last = s;
|
||||
cum.push(cum[i - 1] + (s / 3.6) * (t[i] - t[i - 1]));
|
||||
}
|
||||
const total = cum[n - 1];
|
||||
if (total > 0) return cum.map(d => Math.min(d / total, 1));
|
||||
}
|
||||
const total = cumDist[n - 1];
|
||||
if (!total) return null;
|
||||
// Fallback: time-based progress
|
||||
const t0 = t[0], span = t[n - 1] - t[0];
|
||||
return span > 0 ? t.map(ti => (ti - t0) / span) : null;
|
||||
}
|
||||
|
||||
const valid = speeds.filter((s): s is number => s != null);
|
||||
if (!valid.length) return null;
|
||||
const minSpd = Math.min(...valid);
|
||||
const maxSpd = Math.max(...valid);
|
||||
const range = maxSpd - minSpd;
|
||||
// Generic gradient builder: progress array + any nullable metric array → MapLibre expression.
|
||||
function _buildGradient(ts: Timeseries, values: (number | null)[]): any[] | null {
|
||||
const progress = _computeProgress(ts);
|
||||
if (!progress) return null;
|
||||
const n = Math.min(progress.length, values.length);
|
||||
|
||||
let minV = Infinity, maxV = -Infinity;
|
||||
for (const v of values) { if (v != null) { if (v < minV) minV = v; if (v > maxV) maxV = v; } }
|
||||
if (!isFinite(minV)) return null;
|
||||
const range = maxV - minV;
|
||||
|
||||
const expr: any[] = ['interpolate', ['linear'], ['line-progress']];
|
||||
let prev = -1;
|
||||
let lastFill = minSpd;
|
||||
let prev = -1, lastFill = minV;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const progress = Math.min(cumDist[i] / total, 1);
|
||||
if (progress <= prev + 0.001) continue;
|
||||
prev = progress;
|
||||
const spd = speeds[i] != null ? speeds[i]! : lastFill;
|
||||
if (speeds[i] != null) lastFill = spd;
|
||||
expr.push(progress, _speedColor(range > 0 ? (spd - minSpd) / range : 0.5));
|
||||
const p = Math.min(progress[i], 1);
|
||||
if (p <= prev + 0.001) continue;
|
||||
prev = p;
|
||||
const v = values[i] != null ? values[i]! : lastFill;
|
||||
if (values[i] != null) lastFill = v;
|
||||
expr.push(p, _linearColor(range > 0 ? (v - minV) / range : 0.5));
|
||||
}
|
||||
if (prev < 1) expr.push(1, expr[expr.length - 1]); // close at 1.0
|
||||
return expr.length >= 6 ? expr : null; // need ≥ 2 stops
|
||||
if (prev < 1) expr.push(1, expr[expr.length - 1]);
|
||||
return expr.length >= 6 ? expr : null;
|
||||
}
|
||||
|
||||
function _applyGradient() {
|
||||
if (!map?.getLayer('track-line')) return;
|
||||
const gradient = colorMode === 'speed' && timeseries
|
||||
? (buildSpeedGradient(timeseries) ?? defaultGradient)
|
||||
: defaultGradient;
|
||||
let gradient = defaultGradient;
|
||||
if (timeseries && colorMode !== 'default') {
|
||||
const field: (number | null)[] | null =
|
||||
colorMode === 'speed' ? timeseries.speed_kmh :
|
||||
colorMode === 'hr' ? timeseries.hr_bpm :
|
||||
colorMode === 'power' ? timeseries.power_w :
|
||||
colorMode === 'elevation' ? timeseries.elevation_m :
|
||||
colorMode === 'cadence' ? timeseries.cadence_rpm : null;
|
||||
if (field) gradient = _buildGradient(timeseries, field) ?? defaultGradient;
|
||||
}
|
||||
map.setPaintProperty('track-line', 'line-gradient', gradient);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user