diff --git a/src/app.js b/src/app.js
index 27d1335..2afebc1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1241,6 +1241,128 @@
svg.innerHTML = parts.join("");
}
+ // ---------- sleep trend ----------
+ // Cumulative hours slept as the day progresses: today's running total (up to
+ // now) against yesterday's full curve and the mean of the last N full days,
+ // where N follows the 7/14/30 chart-days picker.
+ // Each curve is a list of { x: hour-of-day 0..24, y: cumulative hours }.
+ function sleepTrendCurves(events) {
+ const HOUR = 3_600_000;
+ const windows = sleepWindows(events); // ongoing sleep already clipped to now
+ const sleptMs = (from, to) => {
+ let total = 0;
+ for (const w of windows) {
+ const s = Math.max(w.start, from);
+ const e = Math.min(w.end, to);
+ if (e > s) total += e - s;
+ }
+ return total;
+ };
+ const dayStartTs = (daysAgo) => {
+ const d = startOfDay(new Date());
+ d.setDate(d.getDate() - daysAgo);
+ return d.getTime();
+ };
+ // capTs (today only) truncates the curve at "now" with a final fractional
+ // point, so the line visibly ends where the day currently stands.
+ const curveFor = (start, capTs) => {
+ const pts = [];
+ for (let h = 0; h <= 24; h++) {
+ const to = start + h * HOUR;
+ if (capTs != null && to >= capTs) {
+ pts.push({ x: (capTs - start) / HOUR, y: sleptMs(start, capTs) / HOUR });
+ break;
+ }
+ pts.push({ x: h, y: sleptMs(start, to) / HOUR });
+ }
+ return pts;
+ };
+
+ const today = curveFor(dayStartTs(0), Date.now());
+
+ const yesterdayCurve = curveFor(dayStartTs(1));
+ const yesterday = yesterdayCurve[24].y > 0 ? yesterdayCurve : null;
+
+ // Mean of the last N full days, skipping days with no sleep at all so a
+ // young log (or a tracking gap) doesn't drag the average toward zero.
+ const avgDays = chartDays();
+ const dayCurves = [];
+ for (let i = 1; i <= avgDays; i++) {
+ const c = curveFor(dayStartTs(i));
+ if (c[24].y > 0) dayCurves.push(c);
+ }
+ let avg = null;
+ if (dayCurves.length > 0) {
+ avg = [];
+ for (let h = 0; h <= 24; h++) {
+ avg.push({ x: h, y: dayCurves.reduce((s, c) => s + c[h].y, 0) / dayCurves.length });
+ }
+ }
+
+ return { today, yesterday, avg, avgDays };
+ }
+
+ function drawSleepTrendChart(curves) {
+ const svg = document.getElementById("chart-sleep-trend");
+ if (!svg) return;
+ const W = 320, H = 160;
+ const ML = 26, MR = 8, MT = 10, MB = 22;
+ const innerW = W - ML - MR;
+ const innerH = H - MT - MB;
+
+ const series = [
+ // Reference lines first so today draws on top of them.
+ { pts: curves.avg, cls: "trend-avg", label: `${curves.avgDays}-day average` },
+ { pts: curves.yesterday, cls: "trend-yesterday", label: "Yesterday" },
+ { pts: curves.today, cls: "trend-today", label: "Today" },
+ ].filter(s => s.pts && s.pts.length > 1);
+
+ const rawMax = Math.max(...series.flatMap(s => s.pts.map(p => p.y)));
+ const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
+
+ const xOf = (hour) => ML + (hour / 24) * innerW;
+ const yOf = (v) => MT + innerH * (1 - v / yMax);
+
+ const parts = [];
+ for (let i = 0; i <= ySteps; i++) {
+ const y = MT + innerH * (1 - i / ySteps);
+ const v = Math.round(yMax * i / ySteps * 10) / 10;
+ const vText = v % 1 === 0 ? v : v.toFixed(1);
+ parts.push(`
Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.
+Hours slept so far at each point of the day, against yesterday and the average over the picked chart window.
+