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:
Alexander Heldt
2026-09-21 20:43:50 +00:00
parent 668f1f039e
commit 7451650b6f
3 changed files with 67 additions and 22 deletions
+34 -21
View File
@@ -1713,30 +1713,43 @@
// 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
// needs the room to be qualified.
function renderFoodTrendNote(days, trend) {
const note = document.getElementById("grams-note");
if (!note) return;
const lines = [];
// 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 window = `the last ${chartDays()} days`;
// What the line is allowed to claim, in words. Separated from the drawing
// because this is where the judgement lives: a straight line through noisy
// points always has a slope, and stating it as a fact is how a chart starts
// lying. Kept pure so the rules can be checked.
//
// Figures are rounded to 10 g. The fitted endpoints are model output, not
// measurements — quoting "287 g" would dress a guess up as a reading.
function foodTrendSentence(trend, windowDays) {
const window = `the last ${windowDays} days`;
if (!trend) {
// Says why there is no line. Without this the chart looks broken on a
// 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.`);
} 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.`);
}
return `Not enough complete days in ${window} to draw a trend — it needs four, and today doesn't count until it's over.`;
}
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);
if (missing > 0) {
@@ -1745,7 +1758,7 @@
}
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