From 8eb5946108c88bbd2c496cb11e14700797a537b2 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Wed, 3 Jun 2026 13:37:17 +0200 Subject: [PATCH] feat(frontend): add median statistic to wakeup and bedtime plots --- frontend/src/components/stats/BedtimePlot.tsx | 45 +++++++++++-------- frontend/src/components/stats/WakeupPlot.tsx | 44 +++++++++++------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/stats/BedtimePlot.tsx b/frontend/src/components/stats/BedtimePlot.tsx index a5db27a..87c60f5 100644 --- a/frontend/src/components/stats/BedtimePlot.tsx +++ b/frontend/src/components/stats/BedtimePlot.tsx @@ -51,8 +51,13 @@ const BedtimePlot: React.FC = ({ from_date, to_date, wakeup_ const min = Math.min(...validMinutes); const max = Math.max(...validMinutes); 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]); if (isLoading) return
Loading plot...
; @@ -79,11 +84,11 @@ const BedtimePlot: React.FC = ({ from_date, to_date, wakeup_ tickFormatter={(value) => formatMinutesAfter(Number(value), bedtime_ref)} domain={['dataMin - 30', 'dataMax + 30']} /> - [ - formatMinutesAfter(value, bedtime_ref), - 'Bedtime', - ]} + [ + typeof value === 'number' ? formatMinutesAfter(value, bedtime_ref) : String(value ?? ''), + 'Bedtime', + ]} labelFormatter={(label) => label} /> = ({ from_date, to_date, wakeup_ {stats && ( -
-
-

Earliest

-

{formatMinutesAfter(stats.min, bedtime_ref)}

-
-
-

Latest

-

{formatMinutesAfter(stats.max, bedtime_ref)}

-
+
-

Average

-

{formatMinutesAfter(Math.round(stats.avg), bedtime_ref)}

+

Earliest

+

{formatMinutesAfter(stats.min, bedtime_ref)}

-
+
+

Latest

+

{formatMinutesAfter(stats.max, bedtime_ref)}

+
+
+

Average

+

{formatMinutesAfter(Math.round(stats.avg), bedtime_ref)}

+
+
+

Median

+

{formatMinutesAfter(Math.round(stats.median), bedtime_ref)}

+
+
)} ); diff --git a/frontend/src/components/stats/WakeupPlot.tsx b/frontend/src/components/stats/WakeupPlot.tsx index bd22a09..9eff866 100644 --- a/frontend/src/components/stats/WakeupPlot.tsx +++ b/frontend/src/components/stats/WakeupPlot.tsx @@ -42,20 +42,26 @@ const WakeupPlot: React.FC = ({ from_date, to_date, wakeup_r }).reverse(); }, [wakeupBedtime, wakeup_ref]); - const stats = useMemo(() => { - if (!wakeupBedtime?.days) return null; - const validMinutes = wakeupBedtime.days - .map((day) => day.wakeup_minutes) - .filter((m): m is number => m !== null); + const stats = useMemo(() => { + if (!wakeupBedtime?.days) return null; + const validMinutes = wakeupBedtime.days + .map((day) => day.wakeup_minutes) + .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 max = Math.max(...validMinutes); - const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length; + const min = Math.min(...validMinutes); + const max = Math.max(...validMinutes); + const avg = validMinutes.reduce((sum, m) => sum + m, 0) / validMinutes.length; - return { min, max, avg }; - }, [wakeupBedtime]); + 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, median }; + }, [wakeupBedtime]); if (isLoading) return
Loading plot...
; if (!chartData.length) { @@ -81,11 +87,11 @@ const WakeupPlot: React.FC = ({ from_date, to_date, wakeup_r tickFormatter={(value) => formatMinutesAfter(Number(value), wakeup_ref)} domain={['dataMin - 30', 'dataMax + 30']} /> - [ - formatMinutesAfter(value, wakeup_ref), - 'Wakeup', - ]} + [ + typeof value === 'number' ? formatMinutesAfter(value, wakeup_ref) : String(value ?? ''), + 'Wakeup', + ]} labelFormatter={(label) => label} /> = ({ from_date, to_date, wakeup_r {stats && ( -
+

Earliest

{formatMinutesAfter(stats.min, wakeup_ref)}

@@ -112,6 +118,10 @@ const WakeupPlot: React.FC = ({ from_date, to_date, wakeup_r

Average

{formatMinutesAfter(Math.round(stats.avg), wakeup_ref)}

+
+

Median

+

{formatMinutesAfter(Math.round(stats.median), wakeup_ref)}

+
)}