132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useStats } from '../../hooks/useStats';
|
|
import { formatMinutesAfter, getToday } from '../../utils/time';
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from 'recharts';
|
|
|
|
interface WakeupBedtimeProps {
|
|
from_date: string;
|
|
to_date: string;
|
|
wakeup_ref: string;
|
|
bedtime_ref: string;
|
|
}
|
|
|
|
const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_ref = '07:00', bedtime_ref = '20:00' }) => {
|
|
const { wakeupBedtime, isLoading } = useStats(from_date, to_date, '07:00', '20:00', wakeup_ref, bedtime_ref);
|
|
|
|
const chartData = useMemo(() => {
|
|
if (!wakeupBedtime?.days) return [];
|
|
|
|
return wakeupBedtime.days.map((day) => {
|
|
const daysAgo = Math.floor(
|
|
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
|
|
const offsetFromRef = day.wakeup_minutes ?? 0;
|
|
|
|
return {
|
|
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
|
minutes: offsetFromRef,
|
|
time:
|
|
day.wakeup_minutes !== null
|
|
? formatMinutesAfter(offsetFromRef, wakeup_ref)
|
|
: 'N/A',
|
|
};
|
|
}).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);
|
|
|
|
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 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 <div className="text-center p-4">Loading plot...</div>;
|
|
if (!chartData.length) {
|
|
return <div className="text-center p-4 text-gray-500">No data available for this period.</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
|
|
<h3 className="text-lg font-semibold mb-4 text-gray-800">Wakeup Times</h3>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<LineChart data={chartData} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
|
<XAxis
|
|
dataKey="date"
|
|
tick={{ fontSize: 12 }}
|
|
tickLine={false}
|
|
axisLine={{ stroke: '#e5e7eb' }}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12 }}
|
|
tickLine={false}
|
|
axisLine={{ stroke: '#e5e7eb' }}
|
|
tickFormatter={(value) => formatMinutesAfter(Number(value), wakeup_ref)}
|
|
domain={['dataMin - 30', 'dataMax + 30']}
|
|
/>
|
|
<Tooltip
|
|
formatter={(value: unknown) => [
|
|
typeof value === 'number' ? formatMinutesAfter(value, wakeup_ref) : String(value ?? ''),
|
|
'Wakeup',
|
|
]}
|
|
labelFormatter={(label) => label}
|
|
/>
|
|
<Line
|
|
type="monotone"
|
|
dataKey="minutes"
|
|
stroke="#3b82f6"
|
|
strokeWidth={2}
|
|
dot={{ fill: '#3b82f6', r: 4 }}
|
|
connectNulls={false}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
{stats && (
|
|
<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, wakeup_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, wakeup_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), wakeup_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), wakeup_ref)}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WakeupPlot;
|