All tweaks added
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
ComposedChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer, ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { formatDuration, formatPace } from '../../utils/format'
|
||||
import { formatPace, formatCadence } from '../../utils/format'
|
||||
|
||||
function downsample(points, maxPoints = 500) {
|
||||
if (points.length <= maxPoints) return points
|
||||
@@ -17,7 +17,7 @@ function buildChartData(dataPoints, activeMetrics) {
|
||||
.map(p => {
|
||||
const row = { distance_m: p.distance_m ?? 0 }
|
||||
for (const key of activeMetrics) {
|
||||
row[key] = p[key] ?? null
|
||||
row[key] = (p[key] != null && p[key] !== 0) ? p[key] : null
|
||||
}
|
||||
return row
|
||||
})
|
||||
@@ -25,9 +25,7 @@ function buildChartData(dataPoints, activeMetrics) {
|
||||
|
||||
const CustomTooltip = ({ active, payload, label, metrics, sportType, onHover }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
if (onHover) onHover(label)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg p-3 text-xs shadow-xl">
|
||||
<p className="text-gray-400 mb-1">{(label / 1000).toFixed(2)} km</p>
|
||||
@@ -37,7 +35,7 @@ const CustomTooltip = ({ active, payload, label, metrics, sportType, onHover })
|
||||
let display = entry.value.toFixed(1)
|
||||
if (entry.dataKey === 'speed_ms') display = formatPace(entry.value, sportType)
|
||||
else if (entry.dataKey === 'heart_rate') display = `${Math.round(entry.value)} bpm`
|
||||
else if (entry.dataKey === 'cadence') display = `${Math.round(entry.value)} rpm`
|
||||
else if (entry.dataKey === 'cadence') display = formatCadence(entry.value, sportType)
|
||||
else if (entry.dataKey === 'power') display = `${Math.round(entry.value)} W`
|
||||
else if (entry.dataKey === 'temperature_c') display = `${entry.value.toFixed(1)} °C`
|
||||
else if (entry.dataKey === 'altitude_m') display = `${entry.value.toFixed(0)} m`
|
||||
@@ -61,7 +59,6 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
|
||||
const activeMetricConfigs = metrics.filter(m => activeMetrics.includes(m.key))
|
||||
|
||||
// Build per-metric Y-axis domains
|
||||
const domains = useMemo(() => {
|
||||
const result = {}
|
||||
for (const m of activeMetricConfigs) {
|
||||
@@ -70,6 +67,7 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
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
|
||||
@@ -87,18 +85,14 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
<div className="space-y-4">
|
||||
{activeMetricConfigs.map((metric, idx) => {
|
||||
const domain = domains[metric.key] || ['auto', 'auto']
|
||||
const data = chartData.filter(p => p[metric.key] != null)
|
||||
if (!data.length) return null
|
||||
const hasData = chartData.some(p => p[metric.key] != null)
|
||||
if (!hasData) return null
|
||||
|
||||
return (
|
||||
<div key={metric.key}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span style={{ color: metric.color }} className="text-xs font-medium">
|
||||
{metric.label}
|
||||
</span>
|
||||
{metric.unit && (
|
||||
<span className="text-xs text-gray-600">({metric.unit})</span>
|
||||
)}
|
||||
<span style={{ color: metric.color }} className="text-xs font-medium">{metric.label}</span>
|
||||
{metric.unit && <span className="text-xs text-gray-600">({metric.unit})</span>}
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={100}>
|
||||
<ComposedChart data={chartData} margin={{ top: 2, right: 8, bottom: 2, left: 8 }}>
|
||||
@@ -118,20 +112,19 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
tick={{ fontSize: 10, fill: '#6b7280' }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
width={40}
|
||||
tickFormatter={v => {
|
||||
if (metric.key === 'speed_ms') return `${(v * 3.6).toFixed(0)}`
|
||||
if (metric.key === 'speed_ms') {
|
||||
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')}`
|
||||
}
|
||||
if (metric.key === 'cadence') return Math.round(v * (sportType === 'running' ? 2 : 1))
|
||||
return Math.round(v)
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
<CustomTooltip
|
||||
metrics={metrics}
|
||||
sportType={sportType}
|
||||
onHover={onHoverDistance}
|
||||
/>
|
||||
}
|
||||
content={<CustomTooltip metrics={metrics} sportType={sportType} onHover={onHoverDistance} />}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
@@ -148,8 +141,6 @@ export default function MetricTimeline({ dataPoints, activeMetrics, metrics, onH
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Shared distance axis label */}
|
||||
<p className="text-xs text-gray-600 text-center">Distance (km)</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user