import { useState, useRef } from 'react' // Proper sleep hypnogram: 4 horizontal lanes (Awake/REM/Light/Deep), time on X axis. const SLEEP_LANE_ORDER = [1, 4, 2, 3] // top→bottom: awake, rem, light, deep const SLEEP_STAGE_COLOR = { 0: '#6b7280', 1: '#eab308', 2: '#a78bfa', 3: '#6366f1', 4: '#7c3aed' } const SLEEP_STAGE_LABEL = { 1: 'Awake', 2: 'Light', 3: 'Deep', 4: 'REM' } const LANE_H = 15 const fmtClock = (ms) => new Date(ms).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) export default function SleepHypnogram({ sleepStart, sleepEnd, stages }) { const wrapRef = useRef(null) const [tip, setTip] = useState(null) if (!sleepStart || !sleepEnd || !stages?.length) return null const startMs = new Date(sleepStart).getTime() const endMs = new Date(sleepEnd).getTime() const windowMs = endMs - startMs if (windowMs <= 0) return null // Build segments per lane (keep each segment's real start/end for the tooltip) const segsByLane = {} SLEEP_LANE_ORDER.forEach(lv => { segsByLane[lv] = [] }) stages.forEach(([tsMs, level], i) => { if (!(level in segsByLane)) return const nextTs = i + 1 < stages.length ? stages[i + 1][0] : endMs const left = Math.max(0, (tsMs - startMs) / windowMs * 100) const right = Math.min(100, (nextTs - startMs) / windowMs * 100) const w = right - left if (w > 0) segsByLane[level].push({ left, w, level, startMs: tsMs, endMs: nextTs }) }) const showTip = (seg, e) => { const rect = wrapRef.current?.getBoundingClientRect() if (!rect) return setTip({ x: e.clientX - rect.left, y: e.clientY - rect.top, level: seg.level, range: `${fmtClock(seg.startMs)}–${fmtClock(seg.endMs)}`, mins: Math.max(1, Math.round((seg.endMs - seg.startMs) / 60000)), }) } // Hour ticks const sh = new Date(startMs); sh.setMinutes(0, 0, 0); sh.setHours(sh.getHours() + 1) const ticks = [] for (let t = sh.getTime(); t < endMs; t += 3600000) { const pct = (t - startMs) / windowMs * 100 if (pct >= 0 && pct <= 100) ticks.push({ pct, label: new Date(t).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) }) } return (
setTip(null)}>
{SLEEP_LANE_ORDER.map(level => (
{SLEEP_STAGE_LABEL[level]}
{segsByLane[level].map((seg, i) => (
showTip(seg, e)} onMouseMove={(e) => showTip(seg, e)} /> ))} {ticks.map((t, i) => (
))}
))}
{tip && (
{SLEEP_STAGE_LABEL[tip.level]} {tip.range} · {tip.mins}m
)}
{new Date(startMs).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })} {ticks.map((t, i) => ( {t.label} ))} {new Date(endMs).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
) }