HRV: plot Garmin overnight avg (weeklyAvg) not last-night; dedupe chart tooltips
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 8s

This commit is contained in:
2026-06-16 01:01:33 +01:00
parent d01f66223b
commit 16144d60b4
6 changed files with 33 additions and 7 deletions
+26 -5
View File
@@ -547,7 +547,7 @@ function DailySnapshot({ day, snapshotWeight, avg30, intradayHr, bodyBattery, bb
<p className="text-xs text-gray-500 mb-0.5">HRV</p>
<div className="flex items-baseline gap-1.5 flex-wrap">
<span className="text-3xl font-bold text-violet-400">
{day.hrv_nightly_avg ? Math.round(day.hrv_nightly_avg) : '--'}
{(day.hrv_weekly_avg ?? day.hrv_nightly_avg) ? Math.round(day.hrv_weekly_avg ?? day.hrv_nightly_avg) : '--'}
</span>
<span className="text-sm text-gray-500">ms</span>
</div>
@@ -677,6 +677,28 @@ const statusDot = (statusKey) => (props) => {
return <circle cx={cx} cy={cy} r={3.5} fill={color} stroke="#111827" strokeWidth={1} />
}
// Custom tooltip: collapses the dashed gap-bridging Line and the solid Area
// (same dataKey) into one row, and never shows the helper baseline band.
function ChartTooltip({ active, payload, label, formatter }) {
if (!active || !payload?.length) return null
const seen = new Set()
const rows = []
for (const p of payload) {
if (p.dataKey === '__band' || p.value == null || seen.has(p.dataKey)) continue
seen.add(p.dataKey)
rows.push(p)
}
if (!rows.length) return null
return (
<div style={{ ...tooltipStyle, padding: '6px 10px' }}>
<div style={{ color: '#9ca3af', marginBottom: 2 }}>{format(new Date(label), 'MMM d, yyyy')}</div>
{rows.map(p => (
<div key={p.dataKey} style={{ color: '#fff' }}>{formatter ? formatter(p.value) : p.value?.toFixed(1)}</div>
))}
</div>
)
}
function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDate, onDayClick, connectNulls = false, showDots = false, domain, referenceLines, statusDotKey, bandLowKey, bandHighKey, bandColor = '#9ca3af' }) {
const vals = data.filter(d => d[dataKey] != null)
if (!vals.length) return (
@@ -713,8 +735,7 @@ function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDa
tickFormatter={d => format(new Date(d), 'MMM d')} interval="preserveStartEnd" />
<YAxis tick={{ fontSize: 10, fill: '#6b7280' }} axisLine={false} tickLine={false} width={36}
tickFormatter={formatter} domain={domain} />
<Tooltip contentStyle={tooltipStyle} labelFormatter={d => format(new Date(d), 'MMM d, yyyy')}
formatter={(v, name) => name === '__band' ? null : [formatter ? formatter(v) : v?.toFixed(1)]} />
<Tooltip content={<ChartTooltip formatter={formatter} />} />
{hasBand && (
<Area type="monotone" dataKey="__band" stroke="none" fill={bandColor} fillOpacity={0.18}
connectNulls isAnimationActive={false} legendType="none" activeDot={false} />
@@ -1063,14 +1084,14 @@ export default function HealthPage() {
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-300">HRV (nightly avg)</h3>
<h3 className="text-sm font-medium text-gray-300">HRV (overnight avg)</h3>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} /> Balanced</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#f97316' }} /> Unbalanced</span>
<span className="flex items-center gap-1"><span className="w-3 h-2 rounded-sm" style={{ background: '#9ca3af', opacity: 0.5 }} /> Baseline</span>
</div>
</div>
<MetricChart data={metrics} dataKey="hrv_nightly_avg" color="#8b5cf6"
<MetricChart data={metrics} dataKey="hrv_weekly_avg" color="#8b5cf6"
formatter={v => `${Math.round(v)} ms`}
selectedDate={selDateForCharts} onDayClick={handleDayClick}
statusDotKey="hrv_status"