Spell out the highlighted day under the food chart
A stacked bar cannot be read on a phone. There is nothing to hover, so the bar's title is unreachable, and the only way to get a day's figure was to estimate it against the axis — which a split bar makes harder, not easier. Tapping a bar now writes that day out beneath the chart: "Sun, Sep 20 — Dry 260 g · Fresh 100 g · 360 g in total", or just the total where no kinds are in play. The same problem exists without kinds, so it is not gated on them; the unsplit form says the number once rather than "No kind 340 g · 340 g in total". It reads off the day already selected rather than keeping a selection of its own. Tapping a bar selects that day on every chart in the app and this one already highlights it, so a second piece of "which day" state would only be something to keep in step and eventually fail to. It falls out of that choice that the arrows and the date picker move the readout too, which is the behaviour you would want anyway. Three cases say something rather than reading as blank: a day with no food, a day marked not counted, and a day outside the window — which has no bar, so no readout. The changelog entry for this sits on its own rather than inside the food-kinds one. It started life gated on kinds and is not any more, and "once you are using kinds" would have been the wrong condition to file it under.
This commit is contained in:
@@ -251,4 +251,85 @@ suite("what the sentence is allowed to say");
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- the highlighted day's readout
|
||||||
|
// Tapping a bar selects that day; this is what the selection says. It reads off
|
||||||
|
// the existing selection rather than keeping its own, so the two cannot drift.
|
||||||
|
{
|
||||||
|
let kinds = [];
|
||||||
|
let selected = "2026-09-20";
|
||||||
|
let el = { hidden: false, textContent: "" };
|
||||||
|
const info = load({
|
||||||
|
names: ["NO_KIND", "FOOD_COLORS", "foodTrend", "foodSeriesFor", "renderFoodDayInfo"],
|
||||||
|
stubs: {
|
||||||
|
document: { getElementById: () => el },
|
||||||
|
selectedDay: () => new Date(selected + "T12:00:00"),
|
||||||
|
ymd: (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`,
|
||||||
|
liveFoodKinds: () => kinds,
|
||||||
|
loadFoodKinds: () => kinds,
|
||||||
|
foodKindNames: () => new Map(kinds.map(k => [k.id, k.name])),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const day = (n, byKind, excluded = false) => ({
|
||||||
|
ymd: `2026-09-${String(n).padStart(2, "0")}`,
|
||||||
|
date: new Date(2026, 8, n),
|
||||||
|
grams: Object.values(byKind).reduce((s, v) => s + v, 0),
|
||||||
|
gramsByKind: byKind, excluded, meals: 0, mealsMissingGrams: 0,
|
||||||
|
});
|
||||||
|
const read = (days) => {
|
||||||
|
el = { hidden: false, textContent: "" };
|
||||||
|
info.renderFoodDayInfo(days, info.foodSeriesFor(days));
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
suite("the highlighted day's breakdown");
|
||||||
|
{
|
||||||
|
kinds = [{ id: "d", name: "Dry", colorIndex: 0 }, { id: "f", name: "Fresh", colorIndex: 1 }];
|
||||||
|
const days = [
|
||||||
|
day(18, { d: 200, f: 100 }), day(19, { d: 210, f: 90 }), day(20, { d: 260, f: 100 }),
|
||||||
|
];
|
||||||
|
selected = "2026-09-20";
|
||||||
|
const r = read(days);
|
||||||
|
eq(r.hidden, false, "the selected day gets a readout");
|
||||||
|
ok(/Dry 260 g · Fresh 100 g/.test(r.textContent), "each kind's amount, in the stack's order");
|
||||||
|
ok(/360 g in total/.test(r.textContent), "…and the total, so you needn't add them up");
|
||||||
|
ok(/Sep 20/.test(r.textContent), "…named, so it is clear which bar it belongs to");
|
||||||
|
|
||||||
|
selected = "2026-09-18";
|
||||||
|
ok(/Dry 200 g/.test(read(days).textContent), "selecting another bar moves the readout");
|
||||||
|
|
||||||
|
// Out of the window entirely: the chart is not showing that day at all.
|
||||||
|
selected = "2026-08-01";
|
||||||
|
eq(read(days).hidden, true, "a day outside the window has no bar and so no readout");
|
||||||
|
}
|
||||||
|
|
||||||
|
suite("the days that say something else");
|
||||||
|
{
|
||||||
|
kinds = [{ id: "d", name: "Dry", colorIndex: 0 }, { id: "f", name: "Fresh", colorIndex: 1 }];
|
||||||
|
selected = "2026-09-20";
|
||||||
|
const withEmpty = [day(18, { d: 200, f: 100 }), day(19, { d: 210 }), day(20, {})];
|
||||||
|
ok(/no food logged/.test(read(withEmpty).textContent), "a day with no food says so");
|
||||||
|
|
||||||
|
const withExcluded = [day(18, { d: 200, f: 100 }), day(19, { d: 210 }), day(20, {}, true)];
|
||||||
|
ok(/not counted/.test(read(withExcluded).textContent),
|
||||||
|
"a day marked not counted says that instead of reading as empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
suite("an unsplit chart gets the day total too");
|
||||||
|
{
|
||||||
|
// No kinds defined: there is still no hover on a phone, so the figure was
|
||||||
|
// only readable by eye off the axis.
|
||||||
|
kinds = [];
|
||||||
|
selected = "2026-09-20";
|
||||||
|
const days = [day(18, { "": 300 }), day(19, { "": 320 }), day(20, { "": 340 })];
|
||||||
|
const r = read(days);
|
||||||
|
eq(r.hidden, false, "the readout appears without any kinds defined");
|
||||||
|
eq(r.textContent, "Sun, Sep 20 — 340 g.", "…as the plain total, named by day");
|
||||||
|
ok(!/No kind/.test(r.textContent),
|
||||||
|
"…without inventing a kind name for food that has none");
|
||||||
|
ok(!/in total/.test(r.textContent),
|
||||||
|
"…and without saying the same number twice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default report("food-trend");
|
export default report("food-trend");
|
||||||
|
|||||||
+41
@@ -2056,6 +2056,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFoodLegend(series);
|
renderFoodLegend(series);
|
||||||
|
renderFoodDayInfo(days, series);
|
||||||
renderFoodTrendNote(days, series);
|
renderFoodTrendNote(days, series);
|
||||||
|
|
||||||
setChartSVG(svg, parts);
|
setChartSVG(svg, parts);
|
||||||
@@ -2108,6 +2109,46 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the highlighted bar holds, in words. Tapping a bar selects that day
|
||||||
|
// — the behaviour every chart already has — so this reads off the selection
|
||||||
|
// rather than keeping a second one of its own, which would then have to be
|
||||||
|
// kept in step with it.
|
||||||
|
//
|
||||||
|
// Shown whether or not the bars are split. A phone has nothing to hover, so
|
||||||
|
// the bar's tooltip is unreachable and the day's figure was otherwise only
|
||||||
|
// readable by eye off the axis — that is as true of one bar as of a stack.
|
||||||
|
function renderFoodDayInfo(days, series) {
|
||||||
|
const el = document.getElementById("grams-day-info");
|
||||||
|
if (!el) return;
|
||||||
|
const day = days.find(d => d.ymd === ymd(selectedDay()));
|
||||||
|
if (!day) { el.hidden = true; return; } // not a day this window draws
|
||||||
|
|
||||||
|
const date = day.date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
|
||||||
|
el.hidden = false;
|
||||||
|
if (day.excluded) {
|
||||||
|
el.textContent = `${date} — not counted.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Series order, so this reads as the stack above it read bottom to top.
|
||||||
|
const parts = series
|
||||||
|
.map(s => ({ name: s.name, g: s.of(day) }))
|
||||||
|
.filter(p => p.g > 0)
|
||||||
|
.map(p => `${p.name} ${Math.round(p.g)} g`);
|
||||||
|
const total = `${Math.round(day.grams)} g`;
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
el.textContent = `${date} — no food logged.`;
|
||||||
|
} else if (series.length <= 1) {
|
||||||
|
// Unsplit: the one part *is* the total, and naming it twice — "No kind
|
||||||
|
// 340 g · 340 g in total" — would be daft.
|
||||||
|
el.textContent = `${date} — ${total}.`;
|
||||||
|
} else {
|
||||||
|
// The total is worth repeating beside the parts: it is what the bar's
|
||||||
|
// height shows, and it saves adding them up.
|
||||||
|
el.textContent = `${date} — ${parts.join(" · ")} · ${total} in total.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderFoodLegend(series) {
|
function renderFoodLegend(series) {
|
||||||
const el = document.getElementById("grams-legend");
|
const el = document.getElementById("grams-legend");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
[
|
[
|
||||||
|
{ "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 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-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" },
|
||||||
|
|||||||
@@ -405,6 +405,11 @@
|
|||||||
<div class="chart-title">Food (grams)</div>
|
<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, split by kind, with a trend line through each"></svg>
|
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day, split by kind, with a trend line through each"></svg>
|
||||||
<div id="grams-legend" class="legend" hidden></div>
|
<div id="grams-legend" class="legend" hidden></div>
|
||||||
|
<!-- The highlighted day, broken down. Tapping a bar selects that
|
||||||
|
day (as it does on every chart), so this is what the selection
|
||||||
|
amounts to here — there is no hover on a phone, and the bar's
|
||||||
|
tooltip is unreachable. -->
|
||||||
|
<p id="grams-day-info" class="muted-note food-day-info" aria-live="polite" hidden></p>
|
||||||
<!-- What the trend line says, and when it is not saying anything —
|
<!-- What the trend line says, and when it is not saying anything —
|
||||||
see renderFoodTrendNote. -->
|
see renderFoodTrendNote. -->
|
||||||
<p id="grams-note" class="muted-note" hidden></p>
|
<p id="grams-note" class="muted-note" hidden></p>
|
||||||
|
|||||||
@@ -1113,6 +1113,9 @@ button.linklike:hover { text-decoration: underline; filter: none; }
|
|||||||
/* Sits between the stat tiles and the "last X" rows, so it reads as a
|
/* Sits between the stat tiles and the "last X" rows, so it reads as a
|
||||||
footnote to the Meals tile above it. */
|
footnote to the Meals tile above it. */
|
||||||
.food-kind-split { margin: -8px 0 14px; }
|
.food-kind-split { margin: -8px 0 14px; }
|
||||||
|
/* 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". */
|
||||||
|
.food-day-info { margin: 6px 0 0; color: var(--accent); }
|
||||||
|
|
||||||
/* 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. */
|
||||||
|
|||||||
Reference in New Issue
Block a user