Compare commits
1
Commits
556e4d75a8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e47aa53ff |
+27
-8
@@ -47,7 +47,7 @@ suite("today is left out, being half-eaten");
|
|||||||
// down; the fit should not see it at all.
|
// down; the fit should not see it at all.
|
||||||
const t = app.foodTrend(days([400, 400, 400, 400, 50]));
|
const t = app.foodTrend(days([400, 400, 400, 400, 50]));
|
||||||
ok(t, "four complete days are enough");
|
ok(t, "four complete days are enough");
|
||||||
eq(Math.round(t.perWeek), 0, "a flat run stays flat despite today being low");
|
eq(Math.round(t.change), 0, "a flat run stays flat despite today being low");
|
||||||
eq(t.last, 3, "the line stops at the last complete day, not at today");
|
eq(t.last, 3, "the line stops at the last complete day, not at today");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ suite("it reports a direction only when the climb beats the scatter");
|
|||||||
{
|
{
|
||||||
const rising = app.foodTrend(days([200, 250, 300, 350, 400, 450, 0]));
|
const rising = app.foodTrend(days([200, 250, 300, 350, 400, 450, 0]));
|
||||||
ok(rising.clear, "a clean climb is reported");
|
ok(rising.clear, "a clean climb is reported");
|
||||||
eq(Math.round(rising.perWeek), 350, "…at 50 g a day, which is 350 g a week");
|
eq(Math.round(rising.change), 250, "…as the move across the five days it fitted, 200 g to 450 g");
|
||||||
|
|
||||||
const falling = app.foodTrend(days([450, 400, 350, 300, 250, 200, 0]));
|
const falling = app.foodTrend(days([450, 400, 350, 300, 250, 200, 0]));
|
||||||
ok(falling.clear, "a clean fall is reported");
|
ok(falling.clear, "a clean fall is reported");
|
||||||
ok(falling.perWeek < 0, "…with a negative weekly change");
|
ok(falling.change < 0, "…with a negative change");
|
||||||
|
|
||||||
// Same mean, no direction, plenty of noise: the honest answer is "steady".
|
// Same mean, no direction, plenty of noise: the honest answer is "steady".
|
||||||
const noisy = app.foodTrend(days([200, 500, 210, 480, 190, 520, 0]));
|
const noisy = app.foodTrend(days([200, 500, 210, 480, 190, 520, 0]));
|
||||||
@@ -83,20 +83,39 @@ suite("marked days are skipped without shifting the line");
|
|||||||
// The middle day is marked; the rest describe a clean 50 g/day climb. The fit
|
// The middle day is marked; the rest describe a clean 50 g/day climb. The fit
|
||||||
// must ignore the hatch rather than reading it as a day of zero grams.
|
// must ignore the hatch rather than reading it as a day of zero grams.
|
||||||
const t = app.foodTrend(days([200, 250, 0, 350, 400, 450, 0], { excluded: [2] }));
|
const t = app.foodTrend(days([200, 250, 0, 350, 400, 450, 0], { excluded: [2] }));
|
||||||
eq(Math.round(t.perWeek), 350, "the climb is unchanged by the marked day");
|
eq(Math.round(t.change), 250, "the climb is unchanged by the marked day");
|
||||||
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");
|
suite("what the sentence is allowed to say");
|
||||||
{
|
{
|
||||||
const say = (grams, opts) => words.foodTrendSentence(app.foodTrend(days(grams, opts)), 14);
|
const say = (grams, opts, win = 14) => words.foodTrendSentence(app.foodTrend(days(grams, opts)), win);
|
||||||
|
|
||||||
const rising = say([200, 250, 300, 350, 400, 450, 0]);
|
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(/up about 250 g/.test(rising), "a clear climb gives the size of the move");
|
||||||
ok(/200 g a day then/.test(rising) && /450 g a day now/.test(rising),
|
ok(/from roughly 200 g a day to 450 g/.test(rising), "…and the figures at each end");
|
||||||
"…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");
|
ok(/the last 14 days/.test(rising), "…named against the window it was fitted over");
|
||||||
|
|
||||||
|
// The defect this replaced: the move was quoted per week while the fit spans
|
||||||
|
// at most five days on a 7-day window, so the figure and the two endpoints
|
||||||
|
// disagreed and a reader who subtracted them found the sentence wrong.
|
||||||
|
// Whatever the window, the three numbers in the sentence must reconcile.
|
||||||
|
for (const [label, grams, win] of [
|
||||||
|
["a steep 7-day fall", [460, 425, 390, 355, 320, 285, 0], 7],
|
||||||
|
["a long 14-day climb", [200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 0], 14],
|
||||||
|
["a gentle 30-day climb", [...Array(29).fill(0).map((_, i) => 300 + i * 12), 0], 30],
|
||||||
|
]) {
|
||||||
|
const s = say(grams, undefined, win);
|
||||||
|
const m = s.match(/about (\d+) g — from roughly (\d+) g a day to (\d+) g/);
|
||||||
|
ok(m, `${label}: the sentence has all three figures`);
|
||||||
|
if (m) {
|
||||||
|
const [, moved, from, to] = m.map(Number);
|
||||||
|
eq(moved, Math.abs(to - from), `${label}: the move is exactly the difference of the two ends`);
|
||||||
|
ok(new RegExp(`is ${to > from ? "up" : "down"} about`).test(s),
|
||||||
|
`${label}: and the direction matches which end is larger`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const steady = say([300, 302, 298, 301, 299, 300, 0]);
|
const steady = say([300, 302, 298, 301, 299, 300, 0]);
|
||||||
ok(/roughly steady/.test(steady), "a flat run is called steady");
|
ok(/roughly steady/.test(steady), "a flat run is called steady");
|
||||||
ok(/averaging about 300 g a day/.test(steady),
|
ok(/averaging about 300 g a day/.test(steady),
|
||||||
|
|||||||
+15
-8
@@ -1765,7 +1765,11 @@
|
|||||||
return {
|
return {
|
||||||
at: (i) => slope * i + intercept,
|
at: (i) => slope * i + intercept,
|
||||||
first, last,
|
first, last,
|
||||||
perWeek: slope * 7, // change in a day's intake from one week to the next
|
// How much the daily figure moved across the days actually fitted. Not a
|
||||||
|
// per-week rate: on a 7-day window today is never fitted, so the span is
|
||||||
|
// at most five days and a weekly figure would be extrapolated past the
|
||||||
|
// data — leaving a sentence whose own endpoints contradicted it.
|
||||||
|
change: slope * (last - first),
|
||||||
mean: my,
|
mean: my,
|
||||||
clear: rise > residualSD, // the climb outruns the scatter
|
clear: rise > residualSD, // the climb outruns the scatter
|
||||||
};
|
};
|
||||||
@@ -1858,19 +1862,22 @@
|
|||||||
return `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.`;
|
||||||
}
|
}
|
||||||
const round10 = (v) => Math.round(Math.max(0, v) / 10) * 10;
|
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 is not a move anyone could act on,
|
||||||
// Under a twentieth of a typical day, a week apart, is not a trend anyone
|
// whatever the arithmetic says.
|
||||||
// could act on, whatever the arithmetic says.
|
const slight = Math.abs(trend.change) < 5 || Math.abs(trend.change) < trend.mean * 0.05;
|
||||||
const slight = perWeek < 5 || perWeek < trend.mean * 0.05;
|
|
||||||
if (!trend.clear || slight) {
|
if (!trend.clear || slight) {
|
||||||
// The average is a real measurement and survives the noise; the fitted
|
// The average is a real measurement and survives the noise; the fitted
|
||||||
// endpoints would not, so they are not quoted here.
|
// endpoints would not, so they are not quoted here.
|
||||||
return `Over ${window}, daily intake is roughly steady, averaging about ` +
|
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.`;
|
`${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 ` +
|
// The change is derived from the *rounded* ends rather than from the slope,
|
||||||
`${perWeek} g a week — roughly ${round10(trend.at(trend.first))} g a day then, ` +
|
// so that subtracting the two figures on screen gives exactly the figure
|
||||||
`${round10(trend.at(trend.last))} g a day now.`;
|
// quoted. A reader who checks the arithmetic has to find it correct.
|
||||||
|
const from = round10(trend.at(trend.first));
|
||||||
|
const to = round10(trend.at(trend.last));
|
||||||
|
return `Over ${window}, daily intake is ${to > from ? "up" : "down"} about ` +
|
||||||
|
`${Math.abs(to - from)} g — from roughly ${from} g a day to ${to} g.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFoodTrendNote(days, trend) {
|
function renderFoodTrendNote(days, trend) {
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{ "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" },
|
||||||
{ "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-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 120 g — from roughly 280 g a day to 400 g”. 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