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; }