feat(frontend): add median statistic to wakeup and bedtime plots
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user