Add segments, YTD stats, route matching fixes, body battery layout, pace fix
- Segments page: new /segments route with auto-generate (1km splits, turn detection, hill detection), manual segment creation, per-segment performance times across matched activities; fixed auth on existing segment endpoints - YTD distance: new /activities/stats/ytd endpoint; Dashboard replaces 'Total distance' with 'Running this year' + 'Cycling this year'; Activities page shows YTD stats row - Weekly chart click: clicking a Dashboard bar navigates to Activities filtered to that week; Activities reads from/to query params with dismissable chip - Route matching: add ±2.5% distance gate + 3% relative DTW threshold (was flat 80m); tighten candidate pre-filter from 80/120% to 95/105% - Body battery layout: HR chart and body battery now side-by-side at same height on large screens instead of stacked full-width - Pace display fix: MetricTimeline clamps GPS speed outliers before computing Y-axis domain; tick formatter guards against v<=0 or v>25 m/s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,16 +62,21 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
const domains = useMemo(() => {
|
||||
const result = {}
|
||||
for (const m of activeMetricConfigs) {
|
||||
const vals = chartData.map(p => p[m.key]).filter(v => v != null)
|
||||
let vals = chartData.map(p => p[m.key]).filter(v => v != null)
|
||||
if (!vals.length) continue
|
||||
// Clamp GPS speed outliers (spikes cause absurd pace labels like 0:01/km)
|
||||
if (m.key === 'speed_ms') {
|
||||
const speedCap = sportType === 'cycling' ? 25 : 12
|
||||
vals = vals.filter(v => v > 0 && v <= speedCap)
|
||||
if (!vals.length) continue
|
||||
}
|
||||
const min = Math.min(...vals)
|
||||
const max = Math.max(...vals)
|
||||
const pad = (max - min) * 0.1 || 1
|
||||
// For elevation, don't start from 0 - show actual range
|
||||
result[m.key] = [min - pad, max + pad]
|
||||
}
|
||||
return result
|
||||
}, [chartData, activeMetricConfigs])
|
||||
}, [chartData, activeMetricConfigs, sportType])
|
||||
|
||||
if (!chartData.length) {
|
||||
return (
|
||||
@@ -115,6 +120,7 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
width={40}
|
||||
tickFormatter={v => {
|
||||
if (metric.key === 'speed_ms') {
|
||||
if (v <= 0 || v > 25) return ''
|
||||
if (sportType === 'cycling') return `${(v * 3.6).toFixed(0)}`
|
||||
const spm = 1000 / v
|
||||
return `${Math.floor(spm/60)}:${String(Math.floor(spm%60)).padStart(2,'0')}`
|
||||
|
||||
Reference in New Issue
Block a user