Stretch the Sleep trend y-axis above 10h

The interesting detail sits near the goal, in the upper teens of hours,
so the scale is piecewise: 0–10h shares a compressed 26% of the height
with 2h gridlines, and everything above gets the rest with 1h
gridlines. Falls back to linear while the axis is too short to split.
The caption notes the stretched axis.
This commit is contained in:
Alexander Heldt
2026-07-15 19:06:16 +00:00
parent 897311465c
commit 0ebaa11c93
3 changed files with 30 additions and 12 deletions
+28 -11
View File
@@ -1379,14 +1379,33 @@
target ? target.hi : 0,
...series.flatMap(s => s.pts.map(p => p.y)),
);
const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
const { yMax } = niceAxisSleepHours(rawMax);
// The interesting detail sits near the goal, in the upper teens of hours,
// so the y scale is piecewise: everything up to BREAK shares a compressed
// slice of the height and the hours above it get all the rest. Linear
// again while the axis is still too short for a split to mean anything.
const BREAK = 10, LOW_FRAC = 0.26;
const split = yMax > BREAK + 2;
const frac = (v) => !split
? v / yMax
: v <= BREAK
? (v / BREAK) * LOW_FRAC
: LOW_FRAC + ((v - BREAK) / (yMax - BREAK)) * (1 - LOW_FRAC);
const xOf = (hour) => ML + (hour / 24) * innerW;
const yOf = (v) => MT + innerH * (1 - v / yMax);
const yOf = (v) => MT + innerH * (1 - frac(v));
// Label every 1h gridline; smaller text once the axis is dense so the
// labels stay apart.
const yLabelCls = ySteps > 16 ? "y-dense" : "";
// Gridlines: every 1h in the stretched region above the break, every 2h
// in the compressed region below it (1h everywhere when linear).
const yTicks = [];
if (split) {
for (let v = 0; v < BREAK; v += 2) yTicks.push(v);
for (let v = BREAK; v <= yMax; v++) yTicks.push(v);
} else {
for (let v = 0; v <= yMax; v++) yTicks.push(v);
}
// Smaller text once the axis is dense so the labels stay apart.
const yLabelCls = yTicks.length > 16 ? "y-dense" : "";
const parts = [];
// Age-based goal band, behind everything: today's projection should land
@@ -1400,12 +1419,10 @@
`<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;
const vText = v % 1 === 0 ? v : v.toFixed(1);
parts.push(`<line class="grid" x1="${ML}" y1="${y}" x2="${W - MR}" y2="${y}"/>`);
parts.push(`<text class="${yLabelCls}" x="${ML - 4}" y="${y + 3}" text-anchor="end">${vText}h</text>`);
for (const v of yTicks) {
const y = yOf(v);
parts.push(`<line class="grid" x1="${ML}" y1="${y.toFixed(1)}" x2="${W - MR}" y2="${y.toFixed(1)}"/>`);
parts.push(`<text class="${yLabelCls}" x="${ML - 4}" y="${(y + 3).toFixed(1)}" text-anchor="end">${v}h</text>`);
}
for (const hr of [0, 6, 12, 18, 24]) {
const x = xOf(hr);