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.
This commit is contained in:
Alexander Heldt
2026-07-15 10:36:22 +00:00
parent cebe923d68
commit ab0e51108c
4 changed files with 45 additions and 4 deletions
+29 -2
View File
@@ -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 ----------
+1
View File
@@ -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" },
+3 -2
View File
@@ -222,13 +222,14 @@
<section class="patterns" data-panel="sleep-trend">
<h2>Sleep trend</h2>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Cumulative sleep hours through the day: today, yesterday and the recent average"></svg>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Cumulative sleep hours through the day: today, yesterday, the recent average and the projected end-of-day total"></svg>
<div class="legend">
<span class="lg trend-today"><span class="sw"></span>Today</span>
<span class="lg trend-projected" id="legend-trend-projected" hidden><span class="sw"></span><span id="legend-trend-projected-text">Projected</span></span>
<span class="lg trend-yesterday" id="legend-trend-yesterday"><span class="sw"></span>Yesterday</span>
<span class="lg trend-avg" id="legend-trend-avg"><span class="sw"></span><span id="legend-trend-avg-text">7-day avg</span></span>
</div>
<p class="muted-note">Hours slept so far at each point of the day, against yesterday and the average over the picked chart window.</p>
<p class="muted-note">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.</p>
</section>
<section class="patterns" data-panel="hour-heatmap">
+12
View File
@@ -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; }