Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f9e8783fa | ||
|
|
079eb41672 | ||
|
|
e037ab2716 | ||
|
|
7a1c0f3808 | ||
|
|
09b9b62d06 |
+221
-15
@@ -21,7 +21,7 @@
|
||||
"eat": "Ate",
|
||||
"pee": "Pee",
|
||||
"poo": "Poo",
|
||||
"weight": "Weigh-in",
|
||||
"weight": "Weight",
|
||||
"training": "Training",
|
||||
"note": "Note",
|
||||
};
|
||||
@@ -727,8 +727,10 @@
|
||||
|
||||
// Gaps (ms) between consecutive events of `type` logged within the last
|
||||
// `days` days, sorted ascending. These intervals are what tell you how
|
||||
// often the puppy needs to go out.
|
||||
function gapsBetween(events, type, days = 7) {
|
||||
// often the puppy needs to go out. Defaults to the picked chart window, so
|
||||
// the 7d/14d/30d buttons widen the timing panel along with the charts —
|
||||
// read at call time, not at definition, so a change takes on the next render.
|
||||
function gapsBetween(events, type, days = chartDays()) {
|
||||
const cutoff = startOfDay(new Date());
|
||||
cutoff.setDate(cutoff.getDate() - (days - 1));
|
||||
const from = cutoff.getTime();
|
||||
@@ -795,7 +797,7 @@
|
||||
svg.style.display = "none";
|
||||
note.hidden = false;
|
||||
note.textContent = since == null
|
||||
? `No ${row.noun}s logged in the last 7 days.`
|
||||
? `No ${row.noun}s logged in the last ${chartDays()} days.`
|
||||
: `One ${row.noun} logged, ${formatDuration(since)} ago — log another to see the typical gap.`;
|
||||
return;
|
||||
}
|
||||
@@ -840,7 +842,7 @@
|
||||
const parts = [
|
||||
`<rect class="tm-track" x="${ML}" y="${trackY}" width="${innerW}" height="${trackH}" rx="${trackH / 2}"/>`,
|
||||
bandRect(`tm-range ${row.cls}`, xShort, xLong,
|
||||
`<title>${escapeText(`${formatDuration(shortest)}–${formatDuration(longest)} between ${row.noun}s over the last 7 days`)}</title>`),
|
||||
`<title>${escapeText(`${formatDuration(shortest)}–${formatDuration(longest)} between ${row.noun}s over the last ${chartDays()} days`)}</title>`),
|
||||
bandRect(`tm-band ${row.cls}`, xShort, xTypical,
|
||||
`<title>${escapeText(`Typically ${formatDuration(typical)} between ${row.noun}s`)}</title>`),
|
||||
];
|
||||
@@ -1258,8 +1260,6 @@
|
||||
// Sync every "(last N days)" header and the picker's active button.
|
||||
function renderChartWindow() {
|
||||
const n = chartDays();
|
||||
const title = document.getElementById("daily-charts-title");
|
||||
if (title) title.textContent = `Last ${n} days`;
|
||||
document.querySelectorAll("[data-chart-days-label]").forEach(el => {
|
||||
el.textContent = `(last ${n} days)`;
|
||||
});
|
||||
@@ -1601,12 +1601,14 @@
|
||||
|
||||
// ---------- pattern charts ----------
|
||||
|
||||
// Actogram: one row per day (oldest at top), a midnight-to-midnight track with
|
||||
// the puppy's sleep shaded. Sleep windows are clipped to each day, so a night
|
||||
// that crosses midnight shows correctly split across two rows. Today's open
|
||||
// sleep runs to now (sleepWindows already clips ongoing sleep to Date.now()).
|
||||
function renderSleepTimeline(events) {
|
||||
const svg = document.getElementById("chart-sleep-timeline");
|
||||
// Actogram: one row per day (oldest at top), a midnight-to-midnight track
|
||||
// with the given windows shaded. Windows are clipped to each day, so one that
|
||||
// crosses midnight shows correctly split across two rows, and an open one
|
||||
// runs to now (both window builders already clip ongoing to Date.now()).
|
||||
// Sleep and walks are the same picture drawn from different windows, so the
|
||||
// drawing lives here once and each caller brings its windows and their fill.
|
||||
function drawActogram(svgId, windows, barCls) {
|
||||
const svg = document.getElementById(svgId);
|
||||
if (!svg) return;
|
||||
const N = chartDays();
|
||||
const W = 320;
|
||||
@@ -1621,7 +1623,6 @@
|
||||
const dayMs = 86_400_000;
|
||||
|
||||
const today = startOfDay(new Date());
|
||||
const windows = sleepWindows(events);
|
||||
const xOf = (frac) => ML + frac * innerW;
|
||||
|
||||
const parts = [];
|
||||
@@ -1650,7 +1651,7 @@
|
||||
if (e <= s) continue;
|
||||
const x = xOf((s - dayStart) / dayMs);
|
||||
const wpx = ((e - s) / dayMs) * innerW;
|
||||
parts.push(`<rect class="stl-sleep" x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${Math.max(0.6, wpx).toFixed(1)}" height="${rowH.toFixed(1)}" rx="1.5"/>`);
|
||||
parts.push(`<rect class="${barCls}" x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${Math.max(0.6, wpx).toFixed(1)}" height="${rowH.toFixed(1)}" rx="1.5"/>`);
|
||||
}
|
||||
|
||||
const label = isToday ? "Today" : `${day.toLocaleDateString(undefined, { weekday: "short" })} ${day.getDate()}`;
|
||||
@@ -1663,6 +1664,207 @@
|
||||
setChartSVG(svg, parts); // wires the .bar[data-day] click → select that day
|
||||
}
|
||||
|
||||
function renderSleepTimeline(events) {
|
||||
drawActogram("chart-sleep-timeline", sleepWindows(events), "stl-sleep");
|
||||
}
|
||||
|
||||
// The walk patterns are only worth a panel once there is a walk to draw, so
|
||||
// both hide themselves rather than showing an empty day grid to someone who
|
||||
// doesn't track walks.
|
||||
function renderWalkPatterns(events) {
|
||||
const windows = walkWindows(events);
|
||||
const any = windows.length > 0;
|
||||
for (const key of ["walk-timeline", "walk-trend"]) {
|
||||
const section = document.querySelector(`section[data-panel="${key}"]`);
|
||||
if (section) section.hidden = !any;
|
||||
}
|
||||
if (!any) return;
|
||||
drawActogram("chart-walk-timeline", windows, "wtl-walk");
|
||||
renderWalkTrend(events);
|
||||
}
|
||||
|
||||
// Cumulative minutes walked through the day, as a step per walk: flat while
|
||||
// the puppy is in, climbing only while a walk is on. Today and yesterday are
|
||||
// built from their own walk boundaries so the steps stay square; the average
|
||||
// is sampled hourly instead, since a mean over many days is a smooth
|
||||
// reference with no steps of its own to preserve.
|
||||
//
|
||||
// Unlike the sleep trend there is no projected tail. Sleep is a state the
|
||||
// puppy drifts back into, so "what the average day adds from here" is a fair
|
||||
// guess; walks are decisions, and projecting them would be predicting the
|
||||
// handler, not the dog.
|
||||
function walkTrendCurves(events) {
|
||||
const HOUR = 3_600_000, MIN = 60_000, DAY = 86_400_000;
|
||||
const windows = walkWindows(events); // an ongoing walk is already clipped to now
|
||||
const walkedMs = (from, to) => {
|
||||
let total = 0;
|
||||
for (const w of windows) {
|
||||
const s = Math.max(w.start, from);
|
||||
const e = Math.min(w.end, to);
|
||||
if (e > s) total += e - s;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
const day = selectedDay();
|
||||
const isToday = ymd(day) === ymd(new Date());
|
||||
const dayStartTs = (daysAgo) => {
|
||||
const d = startOfDay(day);
|
||||
d.setDate(d.getDate() - daysAgo);
|
||||
return d.getTime();
|
||||
};
|
||||
const pointsAt = (start, stops) =>
|
||||
stops.map(t => ({ x: (t - start) / HOUR, y: walkedMs(start, t) / MIN }));
|
||||
|
||||
// Every boundary falling inside the day, plus the day's two ends, is a
|
||||
// corner of the step line. capTs (today only) ends it at "now".
|
||||
const curveFor = (start, capTs) => {
|
||||
const end = capTs != null ? capTs : start + DAY;
|
||||
const stops = new Set([start, end]);
|
||||
for (const w of windows) {
|
||||
if (w.end <= start || w.start >= end) continue;
|
||||
stops.add(Math.max(w.start, start));
|
||||
stops.add(Math.min(w.end, end));
|
||||
}
|
||||
return pointsAt(start, [...stops].sort((a, b) => a - b));
|
||||
};
|
||||
const hourlyFor = (start) => {
|
||||
const stops = [];
|
||||
for (let h = 0; h <= 24; h++) stops.push(start + h * HOUR);
|
||||
return pointsAt(start, stops);
|
||||
};
|
||||
const totalOf = (pts) => pts[pts.length - 1].y;
|
||||
|
||||
const today = curveFor(dayStartTs(0), isToday ? Date.now() : null);
|
||||
const prev = curveFor(dayStartTs(1));
|
||||
const yesterday = totalOf(prev) > 0 ? prev : null;
|
||||
|
||||
// Mean of the last N days, skipping days with no walk at all so a gap in
|
||||
// logging doesn't drag the average toward zero.
|
||||
const avgDays = chartDays();
|
||||
const dayCurves = [];
|
||||
for (let i = 1; i <= avgDays; i++) {
|
||||
const c = hourlyFor(dayStartTs(i));
|
||||
if (c[24].y > 0) dayCurves.push(c);
|
||||
}
|
||||
let avg = null;
|
||||
if (dayCurves.length > 0) {
|
||||
avg = [];
|
||||
for (let h = 0; h <= 24; h++) {
|
||||
avg.push({ x: h, y: dayCurves.reduce((s, c) => s + c[h].y, 0) / dayCurves.length });
|
||||
}
|
||||
}
|
||||
|
||||
const fmtDay = (daysAgo) =>
|
||||
new Date(dayStartTs(daysAgo)).toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
return {
|
||||
today, yesterday, avg, avgDays,
|
||||
dayLabel: isToday ? "Today" : fmtDay(0),
|
||||
prevDayLabel: isToday ? "Yesterday" : fmtDay(1),
|
||||
};
|
||||
}
|
||||
|
||||
function drawWalkTrendChart(curves, target) {
|
||||
const svg = document.getElementById("chart-walk-trend");
|
||||
if (!svg) return;
|
||||
const W = 320, H = 180;
|
||||
const ML = 30, MR = 8, MT = 10, MB = 22;
|
||||
const innerW = W - ML - MR;
|
||||
const innerH = H - MT - MB;
|
||||
|
||||
const series = [
|
||||
// Reference lines first so today draws on top of them.
|
||||
{ pts: curves.avg, cls: "wtrend-avg", label: `${curves.avgDays}-day average` },
|
||||
{ pts: curves.yesterday, cls: "wtrend-yesterday", label: curves.prevDayLabel },
|
||||
{ pts: curves.today, cls: "wtrend-today", label: curves.dayLabel },
|
||||
].filter(s => s.pts && s.pts.length > 1);
|
||||
|
||||
// The axis has to reach the goal even on a day that fell well short of it,
|
||||
// or the line it is measured against would sit off the top of the chart.
|
||||
const rawMax = Math.max(
|
||||
target ? target.total : 0,
|
||||
...series.flatMap(s => s.pts.map(p => p.y)),
|
||||
);
|
||||
const { yMax, steps } = niceAxisLinear(rawMax);
|
||||
// Minutes are linear all the way down, so no split axis: the sleep trend
|
||||
// stretches its top because the interesting hours cluster near a 16h goal,
|
||||
// while a walk total is as readable at 10 minutes as at 60.
|
||||
const xOf = (hour) => ML + (hour / 24) * innerW;
|
||||
const yOf = (v) => MT + innerH * (1 - v / yMax);
|
||||
|
||||
const parts = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const v = yMax * i / steps;
|
||||
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 x="${ML - 4}" y="${(y + 3).toFixed(1)}" text-anchor="end">${Math.round(v)}m</text>`);
|
||||
}
|
||||
for (const hr of [0, 6, 12, 18, 24]) {
|
||||
const x = xOf(hr);
|
||||
parts.push(`<line class="grid" x1="${x.toFixed(1)}" y1="${MT}" x2="${x.toFixed(1)}" y2="${MT + innerH}"/>`);
|
||||
const anchor = hr === 0 ? "start" : hr === 24 ? "end" : "middle";
|
||||
parts.push(`<text x="${x.toFixed(1)}" y="${H - MB + 14}" text-anchor="${anchor}">${pad2(hr)}</text>`);
|
||||
}
|
||||
|
||||
// The five-minute rule as one line rather than a band: it is a target to
|
||||
// reach, with no upper bound a band would imply.
|
||||
if (target) {
|
||||
const y = yOf(Math.min(target.total, yMax));
|
||||
parts.push(
|
||||
`<line class="wtrend-goal" x1="${ML}" y1="${y.toFixed(1)}" x2="${W - MR}" y2="${y.toFixed(1)}">` +
|
||||
`<title>${escapeText(`Goal ~${target.total} min (${target.perWalk} min × ${target.walks})`)}</title></line>`
|
||||
);
|
||||
}
|
||||
|
||||
for (const s of series) {
|
||||
const d = s.pts
|
||||
.map((p, i) => `${i === 0 ? "M" : "L"}${xOf(p.x).toFixed(1)} ${yOf(p.y).toFixed(1)}`)
|
||||
.join(" ");
|
||||
const last = s.pts[s.pts.length - 1];
|
||||
parts.push(
|
||||
`<path class="${s.cls}" d="${d}">` +
|
||||
`<title>${escapeText(`${s.label} — ${Math.round(last.y)} min walked`)}</title></path>`
|
||||
);
|
||||
}
|
||||
|
||||
svg.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
function renderWalkTrend(events) {
|
||||
const curves = walkTrendCurves(events);
|
||||
const target = walkTargetFor(loadConfig().birthday);
|
||||
drawWalkTrendChart(curves, target);
|
||||
|
||||
const chip = (id) => document.getElementById(id);
|
||||
const mins = (pts) => `${Math.round(pts[pts.length - 1].y)} min`;
|
||||
// With no projection to carry it, the ✓ goes on today's own chip: the goal
|
||||
// is a total for the day, so today's line is what meets it or doesn't.
|
||||
let mark = "";
|
||||
if (target) {
|
||||
const done = curves.today[curves.today.length - 1].y >= target.total;
|
||||
mark = done ? " ✓" : "";
|
||||
chip("legend-wtrend-today-text").parentElement.title = done
|
||||
? "Past the walking goal for this age"
|
||||
: `Goal is about ${target.total} min a day at this age`;
|
||||
}
|
||||
chip("legend-wtrend-today-text").textContent = `${curves.dayLabel} ${mins(curves.today)}${mark}`;
|
||||
|
||||
const yLegend = chip("legend-wtrend-yesterday");
|
||||
yLegend.hidden = !curves.yesterday;
|
||||
if (curves.yesterday) {
|
||||
chip("legend-wtrend-yesterday-text").textContent = `${curves.prevDayLabel} ${mins(curves.yesterday)}`;
|
||||
}
|
||||
const aLegend = chip("legend-wtrend-avg");
|
||||
aLegend.hidden = !curves.avg;
|
||||
if (curves.avg) {
|
||||
chip("legend-wtrend-avg-text").textContent = `${curves.avgDays}-day avg ${mins(curves.avg)}`;
|
||||
}
|
||||
const gLegend = chip("legend-wtrend-goal");
|
||||
gLegend.hidden = !target;
|
||||
if (target) {
|
||||
chip("legend-wtrend-goal-text").textContent = `Goal ~${target.total} min`;
|
||||
}
|
||||
}
|
||||
|
||||
// Which heatmap block is focused, remembered across re-renders so a background
|
||||
// sync doesn't wipe the block the user just tapped. The counts behind it are
|
||||
// recomputed every render, so the readout stays current.
|
||||
@@ -2344,6 +2546,8 @@
|
||||
// gender; fall back to a neutral phrase when no name is configured.
|
||||
const sleepTitle = document.getElementById("sleep-timeline-title");
|
||||
if (sleepTitle) sleepTitle.textContent = cfg.name ? `When ${cfg.name} sleeps` : "When sleeping";
|
||||
const walkTitle = document.getElementById("walk-timeline-title");
|
||||
if (walkTitle) walkTitle.textContent = cfg.name ? `When ${cfg.name} walks` : "When walking";
|
||||
}
|
||||
|
||||
// Dim the quick actions that don't fit the current state — a nudge
|
||||
@@ -2396,6 +2600,7 @@
|
||||
renderWalks(events);
|
||||
renderWeekly(events);
|
||||
renderSleepTimeline(events);
|
||||
renderWalkPatterns(events);
|
||||
renderSleepTrend(events);
|
||||
renderHourHeatmap(events);
|
||||
renderTraining(events);
|
||||
@@ -4269,6 +4474,7 @@
|
||||
renderWalks(evs);
|
||||
renderWeekly(evs);
|
||||
renderSleepTimeline(evs);
|
||||
renderWalkPatterns(evs);
|
||||
renderSleepTrend(evs);
|
||||
renderHourHeatmap(evs);
|
||||
renderTraining(evs);
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
[
|
||||
{ "date": "2026-08-31", "text": "Walks now get the same two pattern views sleep has. “When … walks” is a day-per-row grid shaded where a walk was on, so you can see at a glance whether the routine is actually regular or drifts around. “Walk trend” draws the minutes walked so far at each point of the day against yesterday and the average over the picked window, with the age-based goal as a line and a ✓ once the day clears it — the line climbs only while a walk is on, so every step is one walk. Both appear under the Walks panel as soon as you have logged a walk, and stay out of the way until then" },
|
||||
{ "date": "2026-08-31", "text": "The 7d / 14d / 30d buttons now set the window for the Timing panel too. Until now the typical, shortest and longest gaps between pees, poos and meals were always measured over the last 7 days whatever you picked; switch to 30d and they are measured over 30, which settles down the typical gap once there is a month of history to draw on" },
|
||||
{ "date": "2026-08-31", "text": "The charts are grouped by subject instead of all sharing one “Last 7 days” panel. Sleep hours per day is now its own Sleep panel, sitting just above the “When … sleeps” timeline with the rest of the sleep views. Daily counts and Food have joined the by-hour chart in one “Pees, poos & meals” panel, so how many a day, how much food went with them and what hours they fall in read together. Minutes walked per day has moved into the Walks panel. The 7d / 14d / 30d picker now sits in the Sleep panel and still sets the window for every one of these charts" },
|
||||
{ "date": "2026-08-31", "text": "A sleep or walk pair is now tied together in the History log by a dotted rail down the side, in that pair's colour, running from the start row to the end row. Anything logged in between sits inside the bracket, so a pee taken on a walk reads as having happened during it. A pair still in progress, or one that ran over from yesterday, leaves its end of the rail open" },
|
||||
{ "date": "2026-08-31", "text": "Added walks: tap 🦮 Walk start when you head out and 🏁 Walk end when you get home, and the time in between is counted as exercise. The pair gets its own row under the sleep buttons. Today's total and the number of walks show in the overview, every walk of the day is listed in a new Walks section (with the age-based rule of thumb of about five minutes per month of age, twice a day), and the daily charts gain a “Walks (minutes)” bar chart once you have logged one. Like sleep, only the boundary that makes sense is tappable — no walk running means Walk end is greyed out" },
|
||||
{ "date": "2026-08-31", "text": "The Log event buttons are grouped into rows now instead of flowing into one grid: 😴 Sleep start and ⏰ Sleep end side by side on their own row, then 🍽️ Ate / 💧 Pee / 💩 Poo three across, then ⚖️ Weigh-in and 📝 Note. The two halves of a sleep pair can no longer end up split across a wrap, and no button is left stranded alone on the last row" },
|
||||
{ "date": "2026-08-31", "text": "The Log event buttons are grouped into rows now instead of flowing into one grid: 😴 Sleep start and ⏰ Sleep end side by side on their own row, then 💧 Pee and 💩 Poo, and last the three that ask for a value — 🍽️ Ate / ⚖️ Weight / 📝 Note — three across. The two halves of a sleep pair can no longer end up split across a wrap, and no button is left stranded alone on the last row. The weigh-in button is now just ⚖️ Weight, which fits the narrower slot; weigh-ins read as “Weight” in the history log to match" },
|
||||
{ "date": "2026-08-24", "text": "The timing charts now show the longest gap as well: each bar keeps its solid stretch from the shortest to the typical gap and fades on out to the longest one of the week. To stop the nightly long gap from squashing the daytime range into a sliver, the bars are stretched so the typical gap always sits dead centre — left of the middle is sooner than usual, right of it is longer, in every row" },
|
||||
{ "date": "2026-08-24", "text": "Each row of the timing panel is now a small chart instead of a number: the band spans the shortest to the typical gap over the last 7 days, and the marker is how long it has been since the last one. Inside the band means there is time yet, off the right-hand end means the puppy is due — and a row with only one event so far says so instead of drawing an empty axis" },
|
||||
{ "date": "2026-08-24", "text": "The timing panel now covers meals too — typical and shortest time between them, alongside the pee and poo gaps — so you can see the feeding rhythm the same way. It is titled just “Timing” now that it is no longer only about bathroom breaks" },
|
||||
|
||||
+63
-34
@@ -108,12 +108,11 @@
|
||||
|
||||
<section class="quick-actions">
|
||||
<h2>Log event</h2>
|
||||
<!-- Explicit rows, grouped by what the buttons mean, rather than one
|
||||
auto-fit grid: a start/end pair has to stay side by side at every
|
||||
width (a flat grid split them apart at some column counts, and
|
||||
orphaned the last button on a row of its own). A row per timed
|
||||
pair, then the one-tap moments, then the two that open a dialog
|
||||
to type a value. -->
|
||||
<!-- Explicit rows rather than one auto-fit grid: a start/end pair has
|
||||
to stay side by side at every width (a flat grid split them apart
|
||||
at some column counts, and orphaned the last button on a row of
|
||||
its own). A row per timed pair, then the two one-tap moments, and
|
||||
last the three that open a dialog to type a value. -->
|
||||
<div class="actions">
|
||||
<div class="action-row">
|
||||
<button class="action sleep" data-type="sleep-start">😴 Sleep start</button>
|
||||
@@ -123,13 +122,13 @@
|
||||
<button class="action walk" data-type="walk-start">🦮 Walk start</button>
|
||||
<button class="action walk" data-type="walk-end">🏁 Walk end</button>
|
||||
</div>
|
||||
<div class="action-row three-up">
|
||||
<button class="action eat" data-type="eat">🍽️ Ate</button>
|
||||
<div class="action-row">
|
||||
<button class="action pee" data-type="pee">💧 Pee</button>
|
||||
<button class="action poo" data-type="poo">💩 Poo</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
|
||||
<div class="action-row three-up">
|
||||
<button class="action eat" data-type="eat">🍽️ Ate</button>
|
||||
<button class="action weight" data-type="weight">⚖️ Weight</button>
|
||||
<button class="action note" data-type="note">📝 Note</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +191,7 @@
|
||||
</section>
|
||||
|
||||
<section class="timing" data-panel="timing">
|
||||
<h2>Timing <span class="muted-note">(last 7 days)</span></h2>
|
||||
<h2>Timing <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<!-- One range chart per type, drawn by drawTimingChart: the band spans
|
||||
the shortest to the typical gap and the marker is how long it has
|
||||
been since the last one, so a marker past the band reads as due. -->
|
||||
@@ -215,7 +214,7 @@
|
||||
</div>
|
||||
<!-- The axis is stretched (see drawTimingChart), so say what the middle
|
||||
of a bar means rather than leave it to be inferred. -->
|
||||
<p class="muted-note">The middle of every bar is that type's typical gap: left of it is sooner than usual, right of it is longer, and the faded stretch runs out to the longest gap of the week.</p>
|
||||
<p class="muted-note">The middle of every bar is that type's typical gap: left of it is sooner than usual, right of it is longer, and the faded stretch runs out to the longest gap in the window.</p>
|
||||
<p class="muted-note timing-hint" id="timing-hint"></p>
|
||||
</section>
|
||||
|
||||
@@ -238,36 +237,48 @@
|
||||
<!-- Age-based guidance ("the five-minute rule"), only when a birthday
|
||||
is set and the puppy is still growing. -->
|
||||
<p id="walk-goal" class="muted-note" hidden></p>
|
||||
<div class="chart walk-chart" id="walk-chart-wrap" hidden>
|
||||
<div class="chart-title">Minutes per day <span data-chart-days-label>(last 7 days)</span></div>
|
||||
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="weekly" data-panel="weekly">
|
||||
<h2 id="daily-charts-title">Last 7 days</h2>
|
||||
<!-- The two walk patterns mirror the sleep ones below, drawn from walk
|
||||
windows instead of sleep windows. Both stay hidden until there is a
|
||||
walk to draw, so they cost nothing to anyone not tracking walks. -->
|
||||
<section class="patterns" data-panel="walk-timeline" hidden>
|
||||
<h2><span id="walk-timeline-title">When walking</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<svg id="chart-walk-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Walks per day"></svg>
|
||||
<p class="muted-note">Each row is a day, midnight to midnight; shaded = out on a walk. Tap a row to open that day.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="walk-trend" hidden>
|
||||
<h2>Walk trend</h2>
|
||||
<svg id="chart-walk-trend" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Cumulative minutes walked through the selected day, the day before it, the recent average, and the age-based daily goal"></svg>
|
||||
<div class="legend">
|
||||
<span class="lg wtrend-today"><span class="sw"></span><span id="legend-wtrend-today-text">Today</span></span>
|
||||
<span class="lg wtrend-yesterday" id="legend-wtrend-yesterday"><span class="sw"></span><span id="legend-wtrend-yesterday-text">Yesterday</span></span>
|
||||
<span class="lg wtrend-avg" id="legend-wtrend-avg"><span class="sw"></span><span id="legend-wtrend-avg-text">7-day avg</span></span>
|
||||
<span class="lg wtrend-goal" id="legend-wtrend-goal" hidden><span class="sw"></span><span id="legend-wtrend-goal-text">Goal</span></span>
|
||||
</div>
|
||||
<p class="muted-note">Minutes walked so far at each point of the day, against yesterday and the average over the picked window. The line climbs only while a walk is on, so every step is one walk.</p>
|
||||
</section>
|
||||
|
||||
<!-- The day-window picker lives in this panel but governs every
|
||||
day-window chart on the page — the training grid above, both sleep
|
||||
patterns below, the counts panel and the walk chart — so changing it
|
||||
here changes all of them (see renderChartWindow). -->
|
||||
<section class="patterns" data-panel="sleep-daily">
|
||||
<h2>Sleep <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<div class="chart-days-picker" role="group" aria-label="How many days the charts cover">
|
||||
<button type="button" class="ghost" data-days="7">7d</button>
|
||||
<button type="button" class="ghost" data-days="14">14d</button>
|
||||
<button type="button" class="ghost" data-days="30">30d</button>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="chart-title">Sleep (hours)</div>
|
||||
<div class="chart-title">Hours per day</div>
|
||||
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day"></svg>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="chart-title">Daily counts</div>
|
||||
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
|
||||
<div class="legend legend-toggle" id="counts-metrics" role="group" aria-label="Which counts to show">
|
||||
<label class="lg pee"><input type="checkbox" data-metric="pees" checked /><span class="sw"></span>Pees</label>
|
||||
<label class="lg poo"><input type="checkbox" data-metric="poos" checked /><span class="sw"></span>Poos</label>
|
||||
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" id="walk-chart-wrap" hidden>
|
||||
<div class="chart-title">Walks (minutes)</div>
|
||||
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
|
||||
</div>
|
||||
<div class="chart" id="grams-chart-wrap" hidden>
|
||||
<div class="chart-title">Food (grams)</div>
|
||||
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="sleep-timeline">
|
||||
@@ -289,11 +300,29 @@
|
||||
<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. The axis is stretched above 10h to give the hours around the goal more room.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="hour-heatmap">
|
||||
<h2>By hour of day <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<!-- One panel for the three views of the same events: how many a day,
|
||||
how much food went with them, and what hours they fall in. -->
|
||||
<section class="patterns" data-panel="counts">
|
||||
<h2>Pees, poos & meals <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<div class="chart">
|
||||
<div class="chart-title">Daily counts</div>
|
||||
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
|
||||
<div class="legend legend-toggle" id="counts-metrics" role="group" aria-label="Which counts to show">
|
||||
<label class="lg pee"><input type="checkbox" data-metric="pees" checked /><span class="sw"></span>Pees</label>
|
||||
<label class="lg poo"><input type="checkbox" data-metric="poos" checked /><span class="sw"></span>Poos</label>
|
||||
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" id="grams-chart-wrap" hidden>
|
||||
<div class="chart-title">Food (grams)</div>
|
||||
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="chart-title">By hour of day</div>
|
||||
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
|
||||
<p id="hour-heatmap-info" class="muted-note hour-point-info" aria-live="polite"></p>
|
||||
<p class="muted-note">Darker = happens more often at that hour.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="weight" data-panel="weight">
|
||||
|
||||
+44
-3
@@ -184,8 +184,9 @@ body::before {
|
||||
}
|
||||
/* Keep the single row intact on narrow phones. */
|
||||
@media (max-width: 370px) {
|
||||
/* Three columns leave ~55px of text room on the narrowest phones; trimming
|
||||
the side padding keeps "🍽️ Ate" on one line instead of wrapping. */
|
||||
/* Three columns leave ~55px of text room on the narrowest phones, and
|
||||
"⚖️ Weight" is the longest of the three; trimming the side padding keeps
|
||||
it on one line down to about 360px. */
|
||||
.action-row.three-up button { padding-left: 6px; padding-right: 6px; }
|
||||
.day-bar { gap: 4px; }
|
||||
.day-bar button:not(.bar-clock) { padding: 8px 7px; }
|
||||
@@ -248,7 +249,7 @@ body::before {
|
||||
|
||||
/* Quick actions: a stack of explicit rows (see index.html) instead of one
|
||||
auto-fit grid, so the grouping is the same at every width. Equal columns
|
||||
within a row, two by default and three for the short-labelled moments. */
|
||||
within a row, two by default and three for the dialog row at the bottom. */
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1034,6 +1035,8 @@ input.switch:checked::after { transform: translateX(18px); }
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
.chart-svg .stl-sleep { fill: var(--sleep); }
|
||||
/* Same actogram, walk windows instead of sleep ones (see drawActogram). */
|
||||
.chart-svg .wtl-walk { fill: var(--walk); }
|
||||
.chart-svg .stl-today { fill: var(--accent); font-weight: 600; }
|
||||
/* Selected day: accent ring on the row's track + accent label. */
|
||||
.chart-svg .stl-track.stl-selected {
|
||||
@@ -1084,6 +1087,41 @@ input.switch:checked::after { transform: translateX(18px); }
|
||||
.lg.trend-projected .sw { background: color-mix(in srgb, var(--sleep) 40%, var(--surface)); }
|
||||
.lg.trend-yesterday .sw { background: var(--eat); }
|
||||
.lg.trend-avg .sw { background: var(--weight); }
|
||||
|
||||
/* Walk trend: the sleep trend's line weights, in the walk palette. The goal is
|
||||
a single line rather than a band — a walk total is a target to reach, with no
|
||||
upper bound to overshoot. */
|
||||
.chart-svg .wtrend-today {
|
||||
stroke: var(--walk);
|
||||
stroke-width: 2.5;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
.chart-svg .wtrend-yesterday {
|
||||
stroke: var(--eat);
|
||||
stroke-width: 1.5;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.chart-svg .wtrend-avg {
|
||||
stroke: var(--weight);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.chart-svg .wtrend-goal {
|
||||
stroke: var(--walk);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 2 3;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.lg.wtrend-today .sw { background: var(--walk); }
|
||||
.lg.wtrend-yesterday .sw { background: var(--eat); }
|
||||
.lg.wtrend-avg .sw { background: var(--weight); }
|
||||
.lg.wtrend-goal .sw { background: color-mix(in srgb, var(--walk) 45%, var(--surface)); }
|
||||
.lg[hidden] { display: none; }
|
||||
|
||||
.chart-svg .hm-cell { stroke: none; }
|
||||
@@ -1163,7 +1201,10 @@ button.ex-log {
|
||||
button.ex-edit { padding: 6px 12px; flex-shrink: 0; }
|
||||
|
||||
.training-add { width: 100%; }
|
||||
/* Charts that sit under a panel's list rather than being the whole panel, so
|
||||
they need the separation a list-then-chart stack doesn't get for free. */
|
||||
.training-chart { margin-top: 16px; }
|
||||
.walk-chart { margin-top: 16px; }
|
||||
|
||||
/* ---------- collapsible panels ---------- */
|
||||
section.collapsible > h2 {
|
||||
|
||||
Reference in New Issue
Block a user