feat(frontend): add median statistic to wakeup and bedtime plots

This commit is contained in:
2026-06-03 13:37:17 +02:00
parent 7e3bd0a16b
commit 8eb5946108
2 changed files with 54 additions and 35 deletions
+27 -18
View File
@@ -51,8 +51,13 @@ const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_
const min = Math.min(...validMinutes); const min = Math.min(...validMinutes);
const max = Math.max(...validMinutes); const max = Math.max(...validMinutes);
const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length; const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length;
const sorted = [...validMinutes].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 !== 0
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
return { min, max, avg }; return { min, max, avg, median };
}, [wakeupBedtime]); }, [wakeupBedtime]);
if (isLoading) return <div className="text-center p-4">Loading plot...</div>; if (isLoading) return <div className="text-center p-4">Loading plot...</div>;
@@ -79,11 +84,11 @@ const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_
tickFormatter={(value) => formatMinutesAfter(Number(value), bedtime_ref)} tickFormatter={(value) => formatMinutesAfter(Number(value), bedtime_ref)}
domain={['dataMin - 30', 'dataMax + 30']} domain={['dataMin - 30', 'dataMax + 30']}
/> />
<Tooltip <Tooltip
formatter={(value: number) => [ formatter={(value: unknown) => [
formatMinutesAfter(value, bedtime_ref), typeof value === 'number' ? formatMinutesAfter(value, bedtime_ref) : String(value ?? ''),
'Bedtime', 'Bedtime',
]} ]}
labelFormatter={(label) => label} labelFormatter={(label) => label}
/> />
<Line <Line
@@ -97,20 +102,24 @@ const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
{stats && ( {stats && (
<div className="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-2 text-center"> <div className="mt-4 pt-4 border-t border-gray-100 grid grid-cols-4 gap-2 text-center">
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Earliest</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.min, bedtime_ref)}</p>
</div>
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Latest</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.max, bedtime_ref)}</p>
</div>
<div> <div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Average</p> <p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Earliest</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.avg), bedtime_ref)}</p> <p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.min, bedtime_ref)}</p>
</div> </div>
</div> <div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Latest</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.max, bedtime_ref)}</p>
</div>
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Average</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.avg), bedtime_ref)}</p>
</div>
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Median</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.median), bedtime_ref)}</p>
</div>
</div>
)} )}
</div> </div>
); );
+27 -17
View File
@@ -42,20 +42,26 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
}).reverse(); }).reverse();
}, [wakeupBedtime, wakeup_ref]); }, [wakeupBedtime, wakeup_ref]);
const stats = useMemo(() => { const stats = useMemo(() => {
if (!wakeupBedtime?.days) return null; if (!wakeupBedtime?.days) return null;
const validMinutes = wakeupBedtime.days const validMinutes = wakeupBedtime.days
.map((day) => day.wakeup_minutes) .map((day) => day.wakeup_minutes)
.filter((m): m is number => m !== null); .filter((m): m is number => m !== null);
if (validMinutes.length === 0) return null; if (validMinutes.length === 0) return null;
const min = Math.min(...validMinutes); const min = Math.min(...validMinutes);
const max = Math.max(...validMinutes); const max = Math.max(...validMinutes);
const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length; const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length;
return { min, max, avg }; const sorted = [...validMinutes].sort((a, b) => a - b);
}, [wakeupBedtime]); const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 !== 0
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
return { min, max, avg, median };
}, [wakeupBedtime]);
if (isLoading) return <div className="text-center p-4">Loading plot...</div>; if (isLoading) return <div className="text-center p-4">Loading plot...</div>;
if (!chartData.length) { if (!chartData.length) {
@@ -81,11 +87,11 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
tickFormatter={(value) => formatMinutesAfter(Number(value), wakeup_ref)} tickFormatter={(value) => formatMinutesAfter(Number(value), wakeup_ref)}
domain={['dataMin - 30', 'dataMax + 30']} domain={['dataMin - 30', 'dataMax + 30']}
/> />
<Tooltip <Tooltip
formatter={(value: number) => [ formatter={(value: unknown) => [
formatMinutesAfter(value, wakeup_ref), typeof value === 'number' ? formatMinutesAfter(value, wakeup_ref) : String(value ?? ''),
'Wakeup', 'Wakeup',
]} ]}
labelFormatter={(label) => label} labelFormatter={(label) => label}
/> />
<Line <Line
@@ -99,7 +105,7 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
{stats && ( {stats && (
<div className="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-2 text-center"> <div className="mt-4 pt-4 border-t border-gray-100 grid grid-cols-4 gap-2 text-center">
<div> <div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Earliest</p> <p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Earliest</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.min, wakeup_ref)}</p> <p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(stats.min, wakeup_ref)}</p>
@@ -112,6 +118,10 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Average</p> <p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Average</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.avg), wakeup_ref)}</p> <p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.avg), wakeup_ref)}</p>
</div> </div>
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Median</p>
<p className="text-sm font-semibold text-gray-800">{formatMinutesAfter(Math.round(stats.median), wakeup_ref)}</p>
</div>
</div> </div>
)} )}
</div> </div>