Fix follow-ups: lap bests, segments, charts, dashboard health
- Lap bests: compare against OTHER activities on the route (exclude self), so single-activity routes no longer show every lap as "best" - Segment create: POST to trailing-slash URL (was a 307 that dropped the body); surface errors in the UI - PR splits: scale GPS distance stream to the activity's official distance so over-measured GPS no longer yields bogus split PRs - Speed route colours: red->orange->green->blue->purple (slow->fast) with smooth interpolation + a Slow/Fast gradient key under the map - Health body battery: snap activity highlight to the categorical axis; white tooltip text + % suffix - Health weight: y-min = lowest weight - 20kg; st/lb hover shows total lbs too - Health sleep: move 8h/avg reference labels into the right margin - Dashboard: Health-today pulls latest non-null values (sleep score, VO2 max); body battery tile renders a condensed colour-graded intraday graph Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,13 +28,27 @@ const TILE_LAYERS = {
|
||||
// buffer of tiles and don't defer loads until the map is idle.
|
||||
const TILE_OPTS = { maxZoom: 19, keepBuffer: 6, updateWhenIdle: false, updateWhenZooming: false }
|
||||
|
||||
// Slow → fast colour ramp for speed-coloured routes.
|
||||
const SPEED_STOPS = ['#3b82f6', '#22c55e', '#eab308', '#f97316', '#ef4444']
|
||||
// Slow → fast colour ramp for speed-coloured routes (red → purple).
|
||||
export const SPEED_STOPS = ['#ef4444', '#f97316', '#22c55e', '#3b82f6', '#a855f7']
|
||||
|
||||
function speedColorIndex(speed, min, max) {
|
||||
if (!(max > min)) return 1
|
||||
const t = (speed - min) / (max - min)
|
||||
return Math.min(SPEED_STOPS.length - 1, Math.max(0, Math.floor(t * SPEED_STOPS.length)))
|
||||
// CSS gradient string for the speed legend.
|
||||
export const SPEED_GRADIENT = `linear-gradient(to right, ${SPEED_STOPS.join(', ')})`
|
||||
|
||||
const SPEED_LEVELS = 24 // quantisation steps → smooth gradient while limiting layer count
|
||||
|
||||
function lerpColor(c1, c2, t) {
|
||||
const a = parseInt(c1.slice(1), 16), b = parseInt(c2.slice(1), 16)
|
||||
const r = Math.round(((a >> 16) & 255) + (((b >> 16) & 255) - ((a >> 16) & 255)) * t)
|
||||
const g = Math.round(((a >> 8) & 255) + (((b >> 8) & 255) - ((a >> 8) & 255)) * t)
|
||||
const bl = Math.round((a & 255) + ((b & 255) - (a & 255)) * t)
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1)}`
|
||||
}
|
||||
|
||||
function rampColor(t) {
|
||||
t = Math.max(0, Math.min(1, t))
|
||||
const seg = t * (SPEED_STOPS.length - 1)
|
||||
const i = Math.min(SPEED_STOPS.length - 2, Math.floor(seg))
|
||||
return lerpColor(SPEED_STOPS[i], SPEED_STOPS[i + 1], seg - i)
|
||||
}
|
||||
|
||||
function decodePolyline(encoded) {
|
||||
@@ -77,21 +91,26 @@ function drawRoute(map, { polyline, dataPoints, sportType, colorMode }, trackRef
|
||||
const lo = speeds[Math.floor(speeds.length * 0.05)] ?? 0
|
||||
const hi = speeds[Math.floor(speeds.length * 0.95)] ?? lo + 1
|
||||
|
||||
// Group consecutive points into runs of the same colour bucket → one polyline per run.
|
||||
const levelOf = (s) => {
|
||||
const t = (hi > lo) ? (((s ?? lo) - lo) / (hi - lo)) : 0.5
|
||||
return Math.round(Math.max(0, Math.min(1, t)) * SPEED_LEVELS)
|
||||
}
|
||||
|
||||
// Group consecutive points into runs of the same colour level → one polyline per run.
|
||||
let runStart = 0
|
||||
let runIdx = speedColorIndex(speedPts[0].speed_ms ?? lo, lo, hi)
|
||||
let runLevel = levelOf(speedPts[0].speed_ms)
|
||||
const flush = (end) => {
|
||||
const coords = speedPts.slice(runStart, end + 1).map(p => [p.latitude, p.longitude])
|
||||
if (coords.length >= 2) {
|
||||
L.polyline(coords, { color: SPEED_STOPS[runIdx], weight: 3, opacity: 0.95 }).addTo(group)
|
||||
L.polyline(coords, { color: rampColor(runLevel / SPEED_LEVELS), weight: 3, opacity: 0.95 }).addTo(group)
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < speedPts.length; i++) {
|
||||
const idx = speedColorIndex(speedPts[i].speed_ms ?? lo, lo, hi)
|
||||
if (idx !== runIdx) {
|
||||
const level = levelOf(speedPts[i].speed_ms)
|
||||
if (level !== runLevel) {
|
||||
flush(i) // include current point so runs join up
|
||||
runStart = i
|
||||
runIdx = idx
|
||||
runLevel = level
|
||||
}
|
||||
}
|
||||
flush(speedPts.length - 1)
|
||||
|
||||
Reference in New Issue
Block a user