fix(coach): make table weeks and months mean one thing per metric

A total's weeks and months are now the sum of its days, labelled as a
total, and a level's stay a mean. All time is read as UTC calendar months
from the rollup tier's MONTH fold, or the same fold of the live table when
the tier has no rows, so a month no longer reads as a mean of daily totals
for a short history and a monthly total for a long one. Months are keyed
by their UTC start instead of being re-read in the user's zone, which
named every month one early west of UTC.

The chart's long ranges had the same shift: tier rows are now stamped at
noon UTC of the bucket's first day, which falls inside the bucket's own
week and month in every zone.

Step length and walking speed keep two decimals, loud-sound events count
as a total in whole events, and HRV counts a night measured by both
estimators once and charts the estimator the person mostly has. The model
summary names what a period's value is and gives a total's range total.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 13:03:31 +02:00
1 parent 7c32dca7ab
commit de2200bd5c
1 file changed
+20 -2
@@ -249,7 +249,7 @@ describe("deriveChartSpec: time series", () => {
it("draws at most two series, the first compatible pair", () => {
const table = timeSeries({
domain: "hrv",
domain: "pulse",
columns: [
DAY,
valueColumn("sdnn", "ms"),
@@ -269,7 +269,25 @@ describe("deriveChartSpec: time series", () => {
});
});
it("leaves the reading count off the chart", () => {
it("draws one HRV estimator, not SDNN and RMSSD as a pair", () => {
// The two columns are alternative estimators of one quantity; drawn
// together they read as a comparison they are not.
const table = timeSeries({
domain: "hrv",
columns: [DAY, valueColumn("rmssd", "ms"), valueColumn("sdnn", "ms")],
rows: [
[dayKey(0), 40, 55],
[dayKey(1), 42, 57],
],
});
expect(deriveChartSpec(table)).toEqual({
kind: "line",
x: "day",
series: ["rmssd"],
});
});
it("leaves the reading count off the chart", () => {
const spec = deriveChartSpec(daySeries("pulse", [60, 62], "bpm"));
expect(spec && "series" in spec ? spec.series : []).not.toContain(
"readings",