Show the age-based sleep goal as a band in the Sleep trend chart

The puppy's age (from the configured birthday) picks the daily goal —
0–8 weeks 20–22h, 8–16 weeks 18–20h, 16–18h to 6 months, 14–16h to 12
months — drawn as a shaded band behind the curves, with a legend chip
naming the range. The Projected chip gains a ✓/▽ marker for whether
today's projection reaches the goal's lower bound, and the y-axis
always extends to cover the band. No birthday (or an adult dog) means
no band, unchanged from before.
This commit is contained in:
Alexander Heldt
2026-07-15 17:35:55 +00:00
parent 8fe8f9d417
commit 897311465c
4 changed files with 54 additions and 5 deletions
+49 -4
View File
@@ -257,6 +257,19 @@
return `${wk} · ${mo} old`;
}
// Rough age-based daily sleep goal (hours), for the trend chart's goal band:
// 08 weeks 2022h, 816 weeks 1820h, then 1618h to 6 months and 1416h to
// 12 months. No birthday (or an adult dog) → no goal.
function sleepTargetFor(birthday) {
const a = ageParts(birthday);
if (!a) return null;
if (a.weeks < 8) return { lo: 20, hi: 22, label: "08 weeks" };
if (a.weeks < 16) return { lo: 18, hi: 20, label: "816 weeks" };
if (a.months < 6) return { lo: 16, hi: 18, label: "46 months" };
if (a.months < 12) return { lo: 14, hi: 16, label: "612 months" };
return null;
}
function addEvent(type, note, at, { photoId, weight, grams, exerciseId } = {}) {
const events = loadAll();
const now = Date.now();
@@ -1345,7 +1358,7 @@
return { today, yesterday, avg, avgDays, projected };
}
function drawSleepTrendChart(curves) {
function drawSleepTrendChart(curves, target) {
const svg = document.getElementById("chart-sleep-trend");
if (!svg) return;
const W = 320, H = 220;
@@ -1361,7 +1374,11 @@
{ 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)));
// The axis must reach the goal band's top even when the data doesn't yet.
const rawMax = Math.max(
target ? target.hi : 0,
...series.flatMap(s => s.pts.map(p => p.y)),
);
const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
const xOf = (hour) => ML + (hour / 24) * innerW;
@@ -1371,6 +1388,18 @@
// labels stay apart.
const yLabelCls = ySteps > 16 ? "y-dense" : "";
const parts = [];
// Age-based goal band, behind everything: today's projection should land
// inside it by midnight.
if (target) {
const yTop = yOf(target.hi);
const yBot = yOf(target.lo);
parts.push(
`<rect class="trend-goal" x="${ML}" y="${yTop.toFixed(1)}" ` +
`width="${innerW}" height="${(yBot - yTop).toFixed(1)}">` +
`<title>${escapeText(`Goal ${target.lo}${target.hi}h (${target.label})`)}</title></rect>`
);
}
for (let i = 0; i <= ySteps; i++) {
const y = MT + innerH * (1 - i / ySteps);
const v = Math.round(yMax * i / ySteps * 10) / 10;
@@ -1399,7 +1428,8 @@
function renderSleepTrend(events) {
const curves = sleepTrendCurves(events);
drawSleepTrendChart(curves);
const target = sleepTargetFor(loadConfig().birthday);
drawSleepTrendChart(curves, target);
// Drop legend chips for missing reference lines (no data for them yet),
// keep the average chip's label in step with the chart-days picker, and
// write each curve's slept-hours total into its chip.
@@ -1419,7 +1449,22 @@
const pLegend = chip("legend-trend-projected");
pLegend.hidden = !curves.projected;
if (curves.projected) {
chip("legend-trend-projected-text").textContent = `Projected ~${hrs(curves.projected)}`;
// ✓ when the projection reaches at least the goal's lower bound.
let mark = "";
if (target) {
const end = curves.projected[curves.projected.length - 1].y;
mark = end >= target.lo ? " ✓" : " ▽";
pLegend.title = end >= target.lo
? "On track for the sleep goal"
: "Projected below the sleep goal";
}
chip("legend-trend-projected-text").textContent = `Projected ~${hrs(curves.projected)}${mark}`;
}
const gLegend = chip("legend-trend-goal");
gLegend.hidden = !target;
if (target) {
chip("legend-trend-goal-text").textContent =
`Goal ${target.lo}${target.hi}h (${target.label})`;
}
}
+1
View File
@@ -1,4 +1,5 @@
[
{ "date": "2026-07-15", "text": "The Sleep trend chart shows the sleep goal for your puppy's age as a shaded band — the projection chip gets a ✓ when today is on track" },
{ "date": "2026-07-15", "text": "Logging a training session while viewing another day puts it on that day (the snackbar tells you where it went)" },
{ "date": "2026-07-15", "text": "The Sleep trend yesterday line is orange instead of gray, which was hard to see" },
{ "date": "2026-07-15", "text": "The Sleep trend average line is teal now, so it doesn't blend in with today's blue line and its projection" },
+2 -1
View File
@@ -222,12 +222,13 @@
<section class="patterns" data-panel="sleep-trend">
<h2>Sleep trend</h2>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 220" role="img" aria-label="Cumulative sleep hours through the day: today, yesterday, the recent average and the projected end-of-day total"></svg>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 220" role="img" aria-label="Cumulative sleep hours through the day: today, yesterday, the recent average and the projected end-of-day total, with the age-based sleep goal band"></svg>
<div class="legend">
<span class="lg trend-today"><span class="sw"></span><span id="legend-trend-today-text">Today</span></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><span id="legend-trend-yesterday-text">Yesterday</span></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>
<span class="lg trend-goal" id="legend-trend-goal" hidden><span class="sw"></span><span id="legend-trend-goal-text">Goal</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. The dashed tail continues today's line the way the average day usually plays out.</p>
</section>
+2
View File
@@ -952,6 +952,8 @@ input.switch:checked::after { transform: translateX(18px); }
fill: none;
opacity: 0.55;
}
.chart-svg .trend-goal { fill: var(--sleep); fill-opacity: 0.12; }
.lg.trend-goal .sw { background: color-mix(in srgb, var(--sleep) 20%, var(--surface)); }
.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(--eat); }