From ab0e51108c0801ef46f184f1e4c051cbfc8bd072 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Wed, 15 Jul 2026 10:36:22 +0000 Subject: [PATCH] Project today's end-of-day sleep total on the Sleep trend chart A dashed tail continues today's line from now to midnight by adding the increments the N-day average curve makes over the same stretch, so the projection follows the usual daily rhythm instead of extrapolating the current rate (which overshoots right after a long night). The legend shows the projected total; with no history there's no average and no projection. --- src/app.js | 31 +++++++++++++++++++++++++++++-- src/changelog.json | 1 + src/index.html | 5 +++-- src/style.css | 12 ++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index 97674c2..6be6634 100644 --- a/src/app.js +++ b/src/app.js @@ -1318,7 +1318,27 @@ } } - return { today, yesterday, avg, avgDays }; + // Where today is likely to end up: continue today's line by adding what + // the average day typically adds between now and midnight. That respects + // the time-of-day rhythm (night sleep, nap clusters), unlike a linear + // rate extrapolation, which overshoots wildly just after a long night. + // No history → no average → no projection. + let projected = null; + if (avg) { + const nowPt = today[today.length - 1]; + const avgAt = (x) => { + const lo = Math.floor(x); + const hi = Math.min(24, lo + 1); + return avg[lo].y + (avg[hi].y - avg[lo].y) * (x - lo); + }; + projected = [{ x: nowPt.x, y: nowPt.y }]; + for (let h = Math.ceil(nowPt.x); h <= 24; h++) { + if (h <= nowPt.x) continue; // now landing exactly on an hour boundary + projected.push({ x: h, y: nowPt.y + avgAt(h) - avgAt(nowPt.x) }); + } + } + + return { today, yesterday, avg, avgDays, projected }; } function drawSleepTrendChart(curves) { @@ -1330,9 +1350,10 @@ const innerH = H - MT - MB; const series = [ - // Reference lines first so today draws on top of them. + // Reference lines first so today (and its projected tail) draw on top. { pts: curves.avg, cls: "trend-avg", label: `${curves.avgDays}-day average` }, { pts: curves.yesterday, cls: "trend-yesterday", label: "Yesterday" }, + { pts: curves.projected, cls: "trend-projected", label: "Projected end of day" }, { pts: curves.today, cls: "trend-today", label: "Today" }, ].filter(s => s.pts && s.pts.length > 1); @@ -1377,9 +1398,15 @@ const yLegend = document.getElementById("legend-trend-yesterday"); const aLegend = document.getElementById("legend-trend-avg"); const aText = document.getElementById("legend-trend-avg-text"); + const pLegend = document.getElementById("legend-trend-projected"); + const pText = document.getElementById("legend-trend-projected-text"); if (yLegend) yLegend.hidden = !curves.yesterday; if (aLegend) aLegend.hidden = !curves.avg; if (aText) aText.textContent = `${curves.avgDays}-day avg`; + if (pLegend) pLegend.hidden = !curves.projected; + if (pText && curves.projected) { + pText.textContent = `Projected ~${curves.projected[curves.projected.length - 1].y.toFixed(1)}h`; + } } // ---------- weight ---------- diff --git a/src/changelog.json b/src/changelog.json index 957c980..525eeff 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-07-15", "text": "The Sleep trend chart projects where today will land by midnight — a dashed tail continues today's line following the average day's rhythm" }, { "date": "2026-07-15", "text": "The charts highlight the selected day, so it's easy to spot which bars, rows and cells you're looking at" }, { "date": "2026-07-15", "text": "New Sleep trend chart: today's running sleep total through the day, against yesterday and the average over your chart window (7/14/30 days)" }, { "date": "2026-07-15", "text": "Quick actions that don't fit right now are dimmed (asleep → everything but Sleep end; awake → Sleep end) — still tappable for corrections" }, diff --git a/src/index.html b/src/index.html index 9fb2ba1..f5021c7 100644 --- a/src/index.html +++ b/src/index.html @@ -222,13 +222,14 @@

Sleep trend

- +
Today + Yesterday 7-day avg
-

Hours slept so far at each point of the day, against yesterday and the average over the picked chart window.

+

Hours slept so far at each point of the day, against yesterday and the average over the picked chart window. The dashed tail continues today's line the way the average day usually plays out.

diff --git a/src/style.css b/src/style.css index c68f044..659f9f8 100644 --- a/src/style.css +++ b/src/style.css @@ -939,7 +939,19 @@ input.switch:checked::after { transform: translateX(18px); } fill: none; opacity: 0.7; } +/* Today's projected tail: same weight/color as today so it reads as its + continuation, dashed + faded because it hasn't happened yet. */ +.chart-svg .trend-projected { + stroke: var(--sleep); + stroke-width: 2.5; + stroke-dasharray: 2 4; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + opacity: 0.55; +} .lg.trend-today .sw { background: var(--sleep); } +.lg.trend-projected .sw { background: color-mix(in srgb, var(--sleep) 40%, var(--surface)); } .lg.trend-yesterday .sw { background: var(--muted); } .lg.trend-avg .sw { background: color-mix(in srgb, var(--sleep) 55%, var(--surface)); } .lg[hidden] { display: none; }