Put the day's food breakdown inside the Meals card

The total sat in the Meals card and the per-kind split sat in a row beneath the
whole stats grid, so the two halves of the same figure were in different
places. The split now goes under the total it adds up to.

I argued against this when the row went in, on the grounds that a 90px tile
cannot hold a breakdown. That was true of the shape I had in mind — "Dry 260 g
· Fresh 100 g" inline is about 110px — and not of the one that belongs there:
stacked, a line per kind, it is about 60px.

A single kind gets no line of its own. The total is already that kind's figure,
so the name joins it — "540 g · Dry" — rather than repeating the number
underneath. Same rule the chart's day readout uses, and for the same reason.

A day with no kinds on its meals leaves the tile exactly as it was, which is
the overview anyone not using kinds keeps.

The changelog entry for this has shipped and said the breakdown appears "under
the stat tiles". It does not any more, so it is corrected in place: leaving a
description that is now wrong would be worse than editing an entry some readers
have already seen.
This commit is contained in:
Alexander Heldt
2026-09-22 15:10:51 +00:00
parent cd13efdbed
commit 41827664de
5 changed files with 75 additions and 41 deletions
+38 -17
View File
@@ -122,44 +122,65 @@ suite("the palette wraps rather than running out");
"…and the palette wraps, so two share rather than one having none"); "…and the palette wraps, so two share rather than one having none");
} }
// ---------------------------------------------- the day's split in the overview // ---------------------------------------------- the day's split in the Meals tile
// The Meals tile keeps the day's total; this is the breakdown under it. The // The tile holds the day's total; this puts the per-kind figures under it, so
// case that matters is the one where it must not appear at all. // the parts and the sum they make are read in one place. The case that matters
// is the one where none of it should appear.
{ {
let kinds = []; let kinds = [];
let el = { hidden: false, textContent: "" }; let kindsEl, gramsEl;
const view = load({ const view = load({
names: ["NO_KIND", "renderDayFoodKinds"], names: ["NO_KIND", "renderDayFoodKinds"],
stubs: { stubs: {
document: { getElementById: () => el }, document: {
getElementById: () => kindsEl,
createElement: () => ({ textContent: "", appendChild() {} }),
},
liveFoodKinds: () => kinds, liveFoodKinds: () => kinds,
foodKindNames: () => new Map(kinds.map(k => [k.id, k.name])), foodKindNames: () => new Map(kinds.map(k => [k.id, k.name])),
}, },
}); });
const meal = (grams, foodKindId = "") => ({ type: "eat", grams, foodKindId }); const meal = (grams, foodKindId = "") => ({ type: "eat", grams, foodKindId });
const show = (evs) => { el = { hidden: false, textContent: "" }; view.renderDayFoodKinds(evs); return el; }; const show = (evs) => {
const lines = [];
kindsEl = {
hidden: false, textContent: "",
appendChild: (n) => lines.push(n.textContent),
};
gramsEl = { textContent: "" };
const total = evs.filter(e => e.type === "eat" && e.grams > 0)
.reduce((s, e) => s + e.grams, 0);
view.renderDayFoodKinds(evs, gramsEl, total);
return { lines, grams: gramsEl.textContent, hidden: kindsEl.hidden };
};
suite("the day's food split"); suite("the day's food in the Meals tile");
{ {
kinds = []; kinds = [];
eq(show([meal(180), meal(120)]).hidden, true, const plain = show([meal(180), meal(120)]);
"meals with no kind show no breakdown — the tile's total already says it"); eq(plain.hidden, true, "meals with no kind add nothing under the total");
eq(show([]).hidden, true, "a day with no meals shows nothing"); eq(plain.grams, "", "…and leave the tile's own total line alone");
eq(show([]).hidden, true, "a day with no meals adds nothing");
kinds = [{ id: "d", name: "Dry" }];
const one = show([meal(300, "d"), meal(240, "d")]);
eq(one.hidden, true, "one kind adds no lines — it would only repeat the total");
eq(one.grams, "540 g · Dry", "…the total carries its name instead");
kinds = [{ id: "d", name: "Dry" }, { id: "f", name: "Fresh" }]; kinds = [{ id: "d", name: "Dry" }, { id: "f", name: "Fresh" }];
const both = show([meal(200, "d"), meal(100, "f"), meal(60, "d")]); const two = show([meal(200, "d"), meal(100, "f"), meal(60, "d")]);
eq(both.hidden, false, "once a meal carries a kind, the breakdown appears"); eq(two.hidden, false, "two kinds do get their own lines");
eq(both.textContent, "Dry 260 g · Fresh 100 g", "…summed per kind, in the chart's order"); eq(two.lines, ["Dry 260 g", "Fresh 100 g"], "…summed per kind, in the chart's order");
eq(show([meal(200, "d"), meal(50)]).textContent, "Dry 200 g · No kind 50 g", eq(show([meal(200, "d"), meal(50)]).lines, ["Dry 200 g", "No kind 50 g"],
"unlabelled food on a day that has kinds is named, not dropped"); "unlabelled food alongside a kind is named, not dropped");
kinds = []; kinds = [];
eq(show([meal(90, "gone")]).textContent, "Deleted kind 90 g", eq(show([meal(90, "gone")]).grams, "90 g · Deleted kind",
"a kind deleted since still labels its food rather than vanishing"); "a kind deleted since still labels its food rather than vanishing");
kinds = [{ id: "d", name: "Dry" }]; kinds = [{ id: "d", name: "Dry" }];
eq(show([meal(0, "d"), meal(120, "d")]).textContent, "Dry 120 g", eq(show([meal(0, "d"), meal(120, "d")]).grams, "120 g · Dry",
"a meal logged without an amount adds nothing to the split"); "a meal logged without an amount adds nothing to the split");
} }
} }
+28 -13
View File
@@ -447,15 +447,16 @@
return new Map(loadExercises().map(x => [x.id, x.name])); return new Map(loadExercises().map(x => [x.id, x.name]));
} }
// The day's food, split the way the chart splits the window. The Meals tile // The day's food per kind, inside the Meals tile under the total it adds up
// keeps the total — that is the headline figure, and the breakdown will not // to — the food figures belong together, and having the total in the tile
// fit in a 90px tile — so this sits under the grid instead. // while the split sat in a row of its own put them in two places.
// //
// Only shown when a meal that day actually carries a kind: an account not // gramsEl is the tile's total line, which this may append a name to: with a
// using kinds should see the overview it has always seen, and a day whose // single kind the total *is* that kind's figure, so "540 g · Dry" says it
// meals all predate kinds is exactly that case. // once instead of listing it again below. Same rule as the chart's day
function renderDayFoodKinds(dayEvents) { // readout, for the same reason.
const el = document.getElementById("stat-food-kinds"); function renderDayFoodKinds(dayEvents, gramsEl, gramsTotal) {
const el = document.getElementById("stat-meals-kinds");
if (!el) return; if (!el) return;
const names = foodKindNames(); const names = foodKindNames();
const order = liveFoodKinds().map(k => k.id); const order = liveFoodKinds().map(k => k.id);
@@ -467,19 +468,33 @@
if (id !== NO_KIND) anyKind = true; if (id !== NO_KIND) anyKind = true;
totals.set(id, (totals.get(id) || 0) + e.grams); totals.set(id, (totals.get(id) || 0) + e.grams);
} }
if (!anyKind) { el.hidden = true; return; } el.hidden = true;
el.textContent = "";
// No kinds on this day's meals: the tile keeps the plain total it always
// had, which is the whole overview for anyone not using kinds.
if (!anyKind) return;
// Same order as the chart's layers, with "No kind" last, so the two read // Same order as the chart's layers, with "No kind" last, so the two read
// as the same breakdown rather than two arbitrary lists. // as the same breakdown rather than two arbitrary lists.
const nameOf = (id) => (id === NO_KIND ? "No kind" : (names.get(id) || "Deleted kind"));
const ids = [...totals.keys()].sort((a, b) => { const ids = [...totals.keys()].sort((a, b) => {
if (a === NO_KIND) return 1; if (a === NO_KIND) return 1;
if (b === NO_KIND) return -1; if (b === NO_KIND) return -1;
const ia = order.indexOf(a), ib = order.indexOf(b); const ia = order.indexOf(a), ib = order.indexOf(b);
return (ia === -1 ? Infinity : ia) - (ib === -1 ? Infinity : ib); return (ia === -1 ? Infinity : ia) - (ib === -1 ? Infinity : ib);
}); });
el.textContent = ids
.map(id => `${id === NO_KIND ? "No kind" : (names.get(id) || "Deleted kind")} ${Math.round(totals.get(id))} g`) if (ids.length === 1) {
.join(" · "); gramsEl.textContent = `${Math.round(gramsTotal)} g · ${nameOf(ids[0])}`;
return;
}
// Several: the tile's total stays as it is and each kind gets a line under
// it, so the parts and the sum they make are read together.
for (const id of ids) {
const line = document.createElement("div");
line.textContent = `${nameOf(id)} ${Math.round(totals.get(id))} g`;
el.appendChild(line);
}
el.hidden = false; el.hidden = false;
} }
@@ -934,7 +949,7 @@
const gramsEl = document.getElementById("stat-meals-grams"); const gramsEl = document.getElementById("stat-meals-grams");
gramsEl.textContent = gramsTotal > 0 ? `${Math.round(gramsTotal)} g` : ""; gramsEl.textContent = gramsTotal > 0 ? `${Math.round(gramsTotal)} g` : "";
gramsEl.hidden = !(gramsTotal > 0); gramsEl.hidden = !(gramsTotal > 0);
renderDayFoodKinds(dayEvents); renderDayFoodKinds(dayEvents, gramsEl, gramsTotal);
// Walk time is a duration, not a count, so the tile leads with it and puts // Walk time is a duration, not a count, so the tile leads with it and puts
// "N walks" underneath — the day's exercise at a glance. // "N walks" underneath — the day's exercise at a glance.
document.getElementById("stat-walk").textContent = formatDuration(walkMsInRange(events, from, to)); document.getElementById("stat-walk").textContent = formatDuration(walkMsInRange(events, from, to));
+1 -1
View File
@@ -1,6 +1,6 @@
[ [
{ "date": "2026-09-22", "text": "Tap a bar in the Food (grams) chart and a line under it spells that day out — “Sat, Sep 20 — 340 g”, or “Dry 260 g · Fresh 100 g · 360 g in total” once you are using kinds. A phone has nothing to hover over, so the amount for a given day was previously only readable by eye off the axis. It follows whichever day is highlighted, so the ← → arrows and the date picker move it too, and it says so plainly when a day has no food logged or is marked as not counted" }, { "date": "2026-09-22", "text": "Tap a bar in the Food (grams) chart and a line under it spells that day out — “Sat, Sep 20 — 340 g”, or “Dry 260 g · Fresh 100 g · 360 g in total” once you are using kinds. A phone has nothing to hover over, so the amount for a given day was previously only readable by eye off the axis. It follows whichever day is highlighted, so the ← → arrows and the date picker move it too, and it says so plainly when a day has no food logged or is marked as not counted" },
{ "date": "2026-09-22", "text": "Meals can be labelled with a kind of food. Make up your own in Settings → Food kinds — dry, fresh, raw, whatever you feed — and pick one when you log a meal; tap the ★ beside one to have it chosen for you automatically. You can also invent a kind from inside the log dialog if you realise you need it mid-meal. All of it is optional: “No kind” is always offered, every meal you have already logged keeps working untouched, and with no kinds defined the app looks and behaves exactly as it did. Once you are using them, today's overview shows the day's food broken down — “Dry 260 g · Fresh 100 g” under the stat tiles, and the Food (grams) chart splits each day's bar by kind with a legend, and draws a separate trend line for each, so you can see fresh creeping up while dry comes down. The sentence underneath names only the kinds that are actually moving and folds the rest into one clause, so it stays short however many kinds you have. Renaming a kind updates the meals logged as it; deleting one keeps them readable under the name it had. A guest can label a meal with a kind you have created but cannot add, rename or delete them" }, { "date": "2026-09-22", "text": "Meals can be labelled with a kind of food. Make up your own in Settings → Food kinds — dry, fresh, raw, whatever you feed — and pick one when you log a meal; tap the ★ beside one to have it chosen for you automatically. You can also invent a kind from inside the log dialog if you realise you need it mid-meal. All of it is optional: “No kind” is always offered, every meal you have already logged keeps working untouched, and with no kinds defined the app looks and behaves exactly as it did. Once you are using them, today's overview breaks the day's food down inside the Meals card — “Dry 260 g” and “Fresh 100 g” under the total they add up to — and the Food (grams) chart splits each day's bar by kind with a legend, and draws a separate trend line for each, so you can see fresh creeping up while dry comes down. The sentence underneath names only the kinds that are actually moving and folds the rest into one clause, so it stays short however many kinds you have. Renaming a kind updates the meals logged as it; deleting one keeps them readable under the name it had. A guest can label a meal with a kind you have created but cannot add, rename or delete them" },
{ "date": "2026-09-21", "text": "Fixed the figures under the Food (grams) chart contradicting each other. It read like “down about 329 g a week — roughly 460 g a day then, 320 g a day now”, where subtracting the two amounts gives 140 g, not 329 g. The rate was worked out per week while the line itself only covers the complete days in the window — at most five of them on a 7-day window, since today isn't finished — so it was stretched past the days it was measured from. It now gives the change between the two ends, which is a figure you can check by subtracting them: “down about 140 g — from roughly 460 g a day to 320 g”" }, { "date": "2026-09-21", "text": "Fixed the figures under the Food (grams) chart contradicting each other. It read like “down about 329 g a week — roughly 460 g a day then, 320 g a day now”, where subtracting the two amounts gives 140 g, not 329 g. The rate was worked out per week while the line itself only covers the complete days in the window — at most five of them on a 7-day window, since today isn't finished — so it was stretched past the days it was measured from. It now gives the change between the two ends, which is a figure you can check by subtracting them: “down about 140 g — from roughly 460 g a day to 320 g”" },
{ "date": "2026-09-21", "text": "You can measure the time between two events. Press and hold one row, press and hold another, and a bar along the bottom shows the gap — “3h 42m · Ate 12:10 → Poo 15:52” — which answers things like how long after a meal he needs to go out. It stays there until you clear it with the ✕, so you can change day in between and pick the second event from another day; when the pair straddles midnight the bar shows the dates too. It works on any row that is a single moment: the history log, the notes log and weigh-ins. Holding a row you already picked unpicks it, and a third pick is ignored until you clear. Tapping a row still opens it for editing as before. One cost: because holding a row now means something, you can no longer select the text of a note to copy it" }, { "date": "2026-09-21", "text": "You can measure the time between two events. Press and hold one row, press and hold another, and a bar along the bottom shows the gap — “3h 42m · Ate 12:10 → Poo 15:52” — which answers things like how long after a meal he needs to go out. It stays there until you clear it with the ✕, so you can change day in between and pick the second event from another day; when the pair straddles midnight the bar shows the dates too. It works on any row that is a single moment: the history log, the notes log and weigh-ins. Holding a row you already picked unpicks it, and a third pick is ignored until you clear. Tapping a row still opens it for editing as before. One cost: because holding a row now means something, you can no longer select the text of a note to copy it" },
{ "date": "2026-09-21", "text": "A day marked “not counted” no longer appears in the Sleep trend or the Walk trend. It was already left out of the average and out of the “yesterday” comparison, but the day you were actually looking at was still drawn as the boldest line on the chart — so the one day you had said not to trust was the one the panel led with. Now it is left off and its legend chip goes with it, leaving the average and yesterday, which is what you would want to see on a day like that" }, { "date": "2026-09-21", "text": "A day marked “not counted” no longer appears in the Sleep trend or the Walk trend. It was already left out of the average and out of the “yesterday” comparison, but the day you were actually looking at was still drawn as the boldest line on the chart — so the one day you had said not to trust was the one the panel led with. Now it is left off and its legend chip goes with it, leaving the average and yesterday, which is what you would want to see on a day like that" },
+4 -6
View File
@@ -250,6 +250,10 @@
<div class="stat-label">Meals</div> <div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div> <div class="stat-value" id="stat-meals">0</div>
<div class="stat-sub" id="stat-meals-grams" hidden></div> <div class="stat-sub" id="stat-meals-grams" hidden></div>
<!-- The day's food per kind, under the total it adds up to.
Only when there is more than one kind that day: with one,
the total line already carries its name. -->
<div class="stat-sub stat-kinds" id="stat-meals-kinds" hidden></div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-label">Pees</div> <div class="stat-label">Pees</div>
@@ -265,12 +269,6 @@
</div> </div>
</div> </div>
<!-- The day's food broken down by kind. Its own line rather than
inside the Meals tile: that tile is about 90px wide, and a
breakdown of two or three kinds will not sit in it. Hidden unless
a meal that day actually carries a kind. -->
<p id="stat-food-kinds" class="muted-note food-kind-split" hidden></p>
<div class="lasts"> <div class="lasts">
<div class="last-row"><span>Last pee</span><span id="last-pee"></span></div> <div class="last-row"><span>Last pee</span><span id="last-pee"></span></div>
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div> <div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
+4 -4
View File
@@ -1110,9 +1110,10 @@ button.linklike:hover { text-decoration: underline; filter: none; }
} }
.excluded-note { margin: 8px 0 0; } .excluded-note { margin: 8px 0 0; }
/* Sits between the stat tiles and the "last X" rows, so it reads as a /* The per-kind lines under the Meals tile's total. Kind names are free
footnote to the Meals tile above it. */ text, so a long one wraps inside the tile rather than widening it. */
.food-kind-split { margin: -8px 0 14px; } .stat-kinds { margin-top: 1px; }
.stat-kinds div { overflow-wrap: anywhere; }
/* Tied to the bar above it, so it sits closer to the chart than the trend /* Tied to the bar above it, so it sits closer to the chart than the trend
caption below and takes the accent to read as "the highlighted one". */ caption below and takes the accent to read as "the highlighted one". */
.food-day-info { margin: 6px 0 0; color: var(--accent); } .food-day-info { margin: 6px 0 0; color: var(--accent); }
@@ -1120,7 +1121,6 @@ button.linklike:hover { text-decoration: underline; filter: none; }
/* The day's own figures stay readable but visibly step back, so "this one is /* The day's own figures stay readable but visibly step back, so "this one is
not in the numbers" is legible from the day as well as from the charts. */ not in the numbers" is legible from the day as well as from the charts. */
.overview.day-excluded .stats, .overview.day-excluded .stats,
.overview.day-excluded .food-kind-split,
.overview.day-excluded .last-row { opacity: 0.55; } .overview.day-excluded .last-row { opacity: 0.55; }
/* The hatch every chart uses for a day that doesn't count. The pattern itself /* The hatch every chart uses for a day that doesn't count. The pattern itself