Give the food trend's figures, not only its rate
"Up about 40 g a week" is a rate with nothing to anchor it: it says the line slopes without saying where it sits. The sentence now names both ends of the fit — "roughly 280 g a day then, 400 g a day now" — which is the reading anyone actually wants from a growth chart. When the fit is not trustworthy it quotes the average instead, and says the day-to-day variation is larger than any trend. That difference is the point. The average is a measurement and survives the noise; the ends of the line are the line's own output, and quoting them on a fit nobody should read would dress a guess up as a reading. Everything rounds to 10 g for the same reason — "287 g a day" would be false precision from four noisy points. The wording moves into a pure foodTrendSentence() so those rules can be checked, which is worth doing precisely because they are judgement rather than arithmetic: the checks now pin that a clear climb gives both figures, a flat run gives the average and no endpoints, a see-saw gives neither, and nothing is ever quoted to the gram.
This commit is contained in:
@@ -5,6 +5,7 @@ import { load } from "./extract.mjs";
|
|||||||
import { suite, eq, ok, report } from "./assert.mjs";
|
import { suite, eq, ok, report } from "./assert.mjs";
|
||||||
|
|
||||||
const app = load({ names: ["foodTrend"] });
|
const app = load({ names: ["foodTrend"] });
|
||||||
|
const words = load({ names: ["foodTrendSentence"] });
|
||||||
|
|
||||||
// The 7/14/30 picker reaches the trend by deciding how many days weeklyData
|
// The 7/14/30 picker reaches the trend by deciding how many days weeklyData
|
||||||
// builds — there is no second mechanism, so this is the thing to hold still.
|
// builds — there is no second mechanism, so this is the thing to hold still.
|
||||||
@@ -86,4 +87,35 @@ suite("marked days are skipped without shifting the line");
|
|||||||
ok(t.clear, "…and it is still clear, not drowned by a false zero");
|
ok(t.clear, "…and it is still clear, not drowned by a false zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suite("what the sentence is allowed to say");
|
||||||
|
{
|
||||||
|
const say = (grams, opts) => words.foodTrendSentence(app.foodTrend(days(grams, opts)), 14);
|
||||||
|
|
||||||
|
const rising = say([200, 250, 300, 350, 400, 450, 0]);
|
||||||
|
ok(/up about 350 g a week/.test(rising), "a clear climb gives the rate");
|
||||||
|
ok(/200 g a day then/.test(rising) && /450 g a day now/.test(rising),
|
||||||
|
"…and the figures at each end of the line, so it is not only a rate");
|
||||||
|
ok(/the last 14 days/.test(rising), "…named against the window it was fitted over");
|
||||||
|
|
||||||
|
const steady = say([300, 302, 298, 301, 299, 300, 0]);
|
||||||
|
ok(/roughly steady/.test(steady), "a flat run is called steady");
|
||||||
|
ok(/averaging about 300 g a day/.test(steady),
|
||||||
|
"…and quotes the average, which is a measurement rather than model output");
|
||||||
|
ok(!/then/.test(steady) && !/ now\b/.test(steady),
|
||||||
|
"…but not fitted endpoints, which would dress up a line nobody should read");
|
||||||
|
|
||||||
|
const noisy = say([200, 500, 210, 480, 190, 520, 0]);
|
||||||
|
ok(/roughly steady/.test(noisy) && /variation is larger/.test(noisy),
|
||||||
|
"a see-saw says the variation beat the trend, rather than quoting a slope");
|
||||||
|
|
||||||
|
const none = say([300, 320, 310]);
|
||||||
|
ok(/Not enough complete days/.test(none) && /needs four/.test(none),
|
||||||
|
"too few days explains itself instead of leaving the chart bare");
|
||||||
|
|
||||||
|
// False precision would make a fit look like a reading.
|
||||||
|
ok(/\b\d*[05] g a day/.test(rising), "figures are rounded to 10 g, not quoted to the gram");
|
||||||
|
const falling = say([450, 400, 350, 300, 250, 200, 0]);
|
||||||
|
ok(/down about/.test(falling), "a clear fall says down");
|
||||||
|
}
|
||||||
|
|
||||||
export default report("food-trend");
|
export default report("food-trend");
|
||||||
|
|||||||
+34
-21
@@ -1713,30 +1713,43 @@
|
|||||||
// Says what the line means, and what it cannot mean. Kept in words under the
|
// Says what the line means, and what it cannot mean. Kept in words under the
|
||||||
// chart rather than as a figure on it: "up 40 g a week" is a claim, and it
|
// chart rather than as a figure on it: "up 40 g a week" is a claim, and it
|
||||||
// needs the room to be qualified.
|
// needs the room to be qualified.
|
||||||
function renderFoodTrendNote(days, trend) {
|
// What the line is allowed to claim, in words. Separated from the drawing
|
||||||
const note = document.getElementById("grams-note");
|
// because this is where the judgement lives: a straight line through noisy
|
||||||
if (!note) return;
|
// points always has a slope, and stating it as a fact is how a chart starts
|
||||||
const lines = [];
|
// lying. Kept pure so the rules can be checked.
|
||||||
// The window comes from the 7/14/30 picker, and naming it is the only way
|
//
|
||||||
// the reader can tell that switching it changed the answer — the line
|
// Figures are rounded to 10 g. The fitted endpoints are model output, not
|
||||||
// itself often moves too little to notice.
|
// measurements — quoting "287 g" would dress a guess up as a reading.
|
||||||
const window = `the last ${chartDays()} days`;
|
function foodTrendSentence(trend, windowDays) {
|
||||||
|
const window = `the last ${windowDays} days`;
|
||||||
if (!trend) {
|
if (!trend) {
|
||||||
// Says why there is no line. Without this the chart looks broken on a
|
// Says why there is no line. Without this the chart looks broken on a
|
||||||
// short window, or on one where most days are marked.
|
// short window, or on one where most days are marked.
|
||||||
lines.push(`Not enough complete days in ${window} to draw a trend — it needs four, and today doesn't count until it's over.`);
|
return `Not enough complete days in ${window} to draw a trend — it needs four, and today doesn't count until it's over.`;
|
||||||
} else {
|
|
||||||
const perWeek = Math.round(Math.abs(trend.perWeek));
|
|
||||||
// Under a twentieth of a typical day, a week apart, is not a trend
|
|
||||||
// anyone could act on, whatever the arithmetic says.
|
|
||||||
const slight = perWeek < 5 || perWeek < trend.mean * 0.05;
|
|
||||||
if (!trend.clear || slight) {
|
|
||||||
lines.push(`Over ${window}, daily intake is roughly steady — day-to-day variation is larger than any trend.`);
|
|
||||||
} else {
|
|
||||||
lines.push(`Over ${window}, daily intake is ${trend.perWeek > 0 ? "up" : "down"} about ${perWeek} g a week.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const round10 = (v) => Math.round(Math.max(0, v) / 10) * 10;
|
||||||
|
const perWeek = Math.round(Math.abs(trend.perWeek));
|
||||||
|
// Under a twentieth of a typical day, a week apart, is not a trend anyone
|
||||||
|
// could act on, whatever the arithmetic says.
|
||||||
|
const slight = perWeek < 5 || perWeek < trend.mean * 0.05;
|
||||||
|
if (!trend.clear || slight) {
|
||||||
|
// The average is a real measurement and survives the noise; the fitted
|
||||||
|
// endpoints would not, so they are not quoted here.
|
||||||
|
return `Over ${window}, daily intake is roughly steady, averaging about ` +
|
||||||
|
`${round10(trend.mean)} g a day — day-to-day variation is larger than any trend.`;
|
||||||
|
}
|
||||||
|
return `Over ${window}, daily intake is ${trend.perWeek > 0 ? "up" : "down"} about ` +
|
||||||
|
`${perWeek} g a week — roughly ${round10(trend.at(trend.first))} g a day then, ` +
|
||||||
|
`${round10(trend.at(trend.last))} g a day now.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFoodTrendNote(days, trend) {
|
||||||
|
const note = document.getElementById("grams-note");
|
||||||
|
if (!note) return;
|
||||||
|
// The window comes from the 7/14/30 picker, and naming it is the only way
|
||||||
|
// the reader can tell that switching it changed the answer — the line
|
||||||
|
// itself often moves too little to notice.
|
||||||
|
const lines = [foodTrendSentence(trend, chartDays())];
|
||||||
|
|
||||||
const missing = days.reduce((s, d) => s + (d.mealsMissingGrams || 0), 0);
|
const missing = days.reduce((s, d) => s + (d.mealsMissingGrams || 0), 0);
|
||||||
if (missing > 0) {
|
if (missing > 0) {
|
||||||
@@ -1745,7 +1758,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
note.textContent = lines.join(" ");
|
note.textContent = lines.join(" ");
|
||||||
note.hidden = lines.length === 0;
|
note.hidden = false; // there is always a sentence now, even if it is "no trend"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minutes walked per day. Hidden until there's a walk to show, like the
|
// Minutes walked per day. Hidden until there's a walk to show, like the
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[
|
[
|
||||||
{ "date": "2026-09-21", "text": "The Food (grams) chart has a trend line through it now, so you can see whether he is eating more as he grows — the daily bars bounce around enough to hide a steady climb. A line under the chart says what it amounts to: “daily intake is up about 40 g a week”, or that it is roughly steady when the day-to-day variation is bigger than any trend, which is most of the time over a short window. Today is left out of the line, since the day isn't finished and including it would drag the line down every morning; days marked “not counted” are skipped too. The line follows the 7 / 14 / 30 day picker like the rest of the charts, and the sentence names the window so you can see it change when you switch. If there aren't four complete days to fit it says so rather than leaving you with an empty chart, and if some meals have no amount recorded it says how many, because those days read lower than they really were" },
|
{ "date": "2026-09-21", "text": "The Food (grams) chart has a trend line through it now, so you can see whether he is eating more as he grows — the daily bars bounce around enough to hide a steady climb. A line under the chart says what it amounts to in figures: “daily intake is up about 40 g a week — roughly 280 g a day then, 400 g a day now”. When the day-to-day variation is bigger than any trend, which is most of the time over a short window, it says so and gives the average instead — that is a real measurement, where the ends of the line would only be the line's own guess. Today is left out of the line, since the day isn't finished and including it would drag the line down every morning; days marked “not counted” are skipped too. The line follows the 7 / 14 / 30 day picker like the rest of the charts, and the sentence names the window so you can see it change when you switch. If there aren't four complete days to fit it says so rather than leaving you with an empty chart, and if some meals have no amount recorded it says how many, because those days read lower than they really were" },
|
||||||
{ "date": "2026-09-20", "text": "Fixed the page being wider than the screen on a phone, which is why it had started letting you zoom out. The month grid behind the date was the main culprit: it was centred on the date button, which sits near the right edge, so part of the panel hung off the side of the screen. It is anchored to the edge of the bar now and stays on screen at any width. Also fixed a long unbroken word — a link, or something copied off a food bag — in a history note, an exercise name or its instructions pushing its row wider than the screen instead of wrapping" },
|
{ "date": "2026-09-20", "text": "Fixed the page being wider than the screen on a phone, which is why it had started letting you zoom out. The month grid behind the date was the main culprit: it was centred on the date button, which sits near the right edge, so part of the panel hung off the side of the screen. It is anchored to the edge of the bar now and stays on screen at any width. Also fixed a long unbroken word — a link, or something copied off a food bag — in a history note, an exercise name or its instructions pushing its row wider than the screen instead of wrapping" },
|
||||||
{ "date": "2026-09-20", "text": "Fixed three things that went wrong around a day marked “not counted”. The Timing panel measured “how long since the last pee” from before the marked day rather than from the actual last one, so the marker sat far out to the right. The Sleep and Walk trends drew the marked day's own curve as a flat zero when you were looking at that day — marking a day means don't let it drag the average, not pretend nothing happened on it. And a nap that started on a marked day and ended the next morning vanished from that next day's figures, even though the next day wasn't marked and the puppy really did sleep those hours" },
|
{ "date": "2026-09-20", "text": "Fixed three things that went wrong around a day marked “not counted”. The Timing panel measured “how long since the last pee” from before the marked day rather than from the actual last one, so the marker sat far out to the right. The Sleep and Walk trends drew the marked day's own curve as a flat zero when you were looking at that day — marking a day means don't let it drag the average, not pretend nothing happened on it. And a nap that started on a marked day and ended the next morning vanished from that next day's figures, even though the next day wasn't marked and the puppy really did sleep those hours" },
|
||||||
{ "date": "2026-09-09", "text": "The big asleep/awake card at the top of Today is gone, and both timers now live permanently in the frozen bar at the top — visible on every tab, wherever you have scrolled to. The card only existed on one tab and the timers hid themselves whenever it was on screen, which meant the thing you most often want at a glance was the thing you had to go and find. With only one place left to show them they have their seconds back too" },
|
{ "date": "2026-09-09", "text": "The big asleep/awake card at the top of Today is gone, and both timers now live permanently in the frozen bar at the top — visible on every tab, wherever you have scrolled to. The card only existed on one tab and the timers hid themselves whenever it was on screen, which meant the thing you most often want at a glance was the thing you had to go and find. With only one place left to show them they have their seconds back too" },
|
||||||
|
|||||||
Reference in New Issue
Block a user