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:
+49
-4
@@ -257,6 +257,19 @@
|
|||||||
return `${wk} · ${mo} old`;
|
return `${wk} · ${mo} old`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rough age-based daily sleep goal (hours), for the trend chart's goal band:
|
||||||
|
// 0–8 weeks 20–22h, 8–16 weeks 18–20h, then 16–18h to 6 months and 14–16h 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: "0–8 weeks" };
|
||||||
|
if (a.weeks < 16) return { lo: 18, hi: 20, label: "8–16 weeks" };
|
||||||
|
if (a.months < 6) return { lo: 16, hi: 18, label: "4–6 months" };
|
||||||
|
if (a.months < 12) return { lo: 14, hi: 16, label: "6–12 months" };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function addEvent(type, note, at, { photoId, weight, grams, exerciseId } = {}) {
|
function addEvent(type, note, at, { photoId, weight, grams, exerciseId } = {}) {
|
||||||
const events = loadAll();
|
const events = loadAll();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1345,7 +1358,7 @@
|
|||||||
return { today, yesterday, avg, avgDays, projected };
|
return { today, yesterday, avg, avgDays, projected };
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSleepTrendChart(curves) {
|
function drawSleepTrendChart(curves, target) {
|
||||||
const svg = document.getElementById("chart-sleep-trend");
|
const svg = document.getElementById("chart-sleep-trend");
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
const W = 320, H = 220;
|
const W = 320, H = 220;
|
||||||
@@ -1361,7 +1374,11 @@
|
|||||||
{ pts: curves.today, cls: "trend-today", label: "Today" },
|
{ pts: curves.today, cls: "trend-today", label: "Today" },
|
||||||
].filter(s => s.pts && s.pts.length > 1);
|
].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 { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
|
||||||
|
|
||||||
const xOf = (hour) => ML + (hour / 24) * innerW;
|
const xOf = (hour) => ML + (hour / 24) * innerW;
|
||||||
@@ -1371,6 +1388,18 @@
|
|||||||
// labels stay apart.
|
// labels stay apart.
|
||||||
const yLabelCls = ySteps > 16 ? "y-dense" : "";
|
const yLabelCls = ySteps > 16 ? "y-dense" : "";
|
||||||
const parts = [];
|
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++) {
|
for (let i = 0; i <= ySteps; i++) {
|
||||||
const y = MT + innerH * (1 - i / ySteps);
|
const y = MT + innerH * (1 - i / ySteps);
|
||||||
const v = Math.round(yMax * i / ySteps * 10) / 10;
|
const v = Math.round(yMax * i / ySteps * 10) / 10;
|
||||||
@@ -1399,7 +1428,8 @@
|
|||||||
|
|
||||||
function renderSleepTrend(events) {
|
function renderSleepTrend(events) {
|
||||||
const curves = sleepTrendCurves(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),
|
// 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
|
// keep the average chip's label in step with the chart-days picker, and
|
||||||
// write each curve's slept-hours total into its chip.
|
// write each curve's slept-hours total into its chip.
|
||||||
@@ -1419,7 +1449,22 @@
|
|||||||
const pLegend = chip("legend-trend-projected");
|
const pLegend = chip("legend-trend-projected");
|
||||||
pLegend.hidden = !curves.projected;
|
pLegend.hidden = !curves.projected;
|
||||||
if (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,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": "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 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" },
|
{ "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
@@ -222,12 +222,13 @@
|
|||||||
|
|
||||||
<section class="patterns" data-panel="sleep-trend">
|
<section class="patterns" data-panel="sleep-trend">
|
||||||
<h2>Sleep trend</h2>
|
<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">
|
<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-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-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-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-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>
|
</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>
|
<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>
|
||||||
|
|||||||
@@ -952,6 +952,8 @@ input.switch:checked::after { transform: translateX(18px); }
|
|||||||
fill: none;
|
fill: none;
|
||||||
opacity: 0.55;
|
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-today .sw { background: var(--sleep); }
|
||||||
.lg.trend-projected .sw { background: color-mix(in srgb, var(--sleep) 40%, var(--surface)); }
|
.lg.trend-projected .sw { background: color-mix(in srgb, var(--sleep) 40%, var(--surface)); }
|
||||||
.lg.trend-yesterday .sw { background: var(--eat); }
|
.lg.trend-yesterday .sw { background: var(--eat); }
|
||||||
|
|||||||
Reference in New Issue
Block a user