fix(charts): name a folded point of steps as a daily average

A week or month point of a cumulative metric is the average daily total,
which keeps the axis comparable with the 7, 30 and 90 day tabs. The
caption said "Weekly avg", which reads as an average week's total. Steps,
energy, distance, flights and daylight now say "Daily avg per week" or
"per month" on the caption and in the tooltip, in all seven languages;
level metrics keep the plain average.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 14:08:50 +02:00
1 parent b45a668cf4
commit 7b5ea8b353
10 files changed
+87 -10

No files matched your search

+2
View File
@@ -2261,6 +2261,8 @@
"noData": "Keine Daten im gewählten Zeitraum",
"bucketWeekly": "Wochendurchschnitt",
"bucketMonthly": "Monatsdurchschnitt",
"bucketWeeklyDaily": "Tagesschnitt je Woche",
"bucketMonthlyDaily": "Tagesschnitt je Monat",
"days7Label": "7T",
"days7Title": "Letzte 7 Tage",
"days30Label": "30T",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "No data in selected time range",
"bucketWeekly": "Weekly avg",
"bucketMonthly": "Monthly avg",
"bucketWeeklyDaily": "Daily avg per week",
"bucketMonthlyDaily": "Daily avg per month",
"days7Label": "7d",
"days7Title": "Last 7 days",
"days30Label": "30d",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "Sin datos en el periodo seleccionado",
"bucketWeekly": "Media semanal",
"bucketMonthly": "Media mensual",
"bucketWeeklyDaily": "Media diaria por semana",
"bucketMonthlyDaily": "Media diaria por mes",
"days7Label": "7 d",
"days7Title": "Últimos 7 días",
"days30Label": "30 d",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "Aucune donnée sur la période sélectionnée",
"bucketWeekly": "Moyenne hebdomadaire",
"bucketMonthly": "Moyenne mensuelle",
"bucketWeeklyDaily": "Moyenne par jour, par semaine",
"bucketMonthlyDaily": "Moyenne par jour, par mois",
"days7Label": "7 j",
"days7Title": "7 derniers jours",
"days30Label": "30 j",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "Nessun dato nel periodo selezionato",
"bucketWeekly": "Media settimanale",
"bucketMonthly": "Media mensile",
"bucketWeeklyDaily": "Media giornaliera per settimana",
"bucketMonthlyDaily": "Media giornaliera per mese",
"days7Label": "7 g",
"days7Title": "Ultimi 7 giorni",
"days30Label": "30 g",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "선택한 기간에 데이터가 없어요",
"bucketWeekly": "주 평균",
"bucketMonthly": "월 평균",
"bucketWeeklyDaily": "주별 일평균",
"bucketMonthlyDaily": "월별 일평균",
"systolic": "수축기",
"diastolic": "이완기",
"bodyWater": "체수분",
+2
View File
@@ -2261,6 +2261,8 @@
"noData": "Brak danych w wybranym okresie",
"bucketWeekly": "Średnia tygodniowa",
"bucketMonthly": "Średnia miesięczna",
"bucketWeeklyDaily": "Średnia dzienna w tygodniu",
"bucketMonthlyDaily": "Średnia dzienna w miesiącu",
"days7Label": "7 dni",
"days7Title": "Ostatnie 7 dni",
"days30Label": "30 dni",
+19 -10
View File
@@ -59,6 +59,7 @@ import type {
ComparisonBaseline,
} from "@/lib/dashboard-layout";
import { CUMULATIVE_HK_TYPES } from "@/lib/measurements/apple-health-mapping";
import { bucketCaptionKey } from "@/lib/charts/bucket-caption";
import type { MeasurementType } from "@/generated/prisma/client";
import { ChartOverlayControls } from "./chart-overlay-controls";
import { ChartDataTable, type ChartDataTableColumn } from "./chart-data-table";
@@ -1018,8 +1019,9 @@ export function HealthChart({
// v1.4.29.1 — cumulative HealthKit types (steps, active energy,
// distance, flights, daylight) must reduce with sum, not the
// per-sample average. The server already returns one row per
// day; a day that arrives as several rows (the coarser tiers
// of the "All" range) must still add up rather than average.
// day (or, for the coarser tiers of the "All" range, one row per
// week or month carrying its average day); a day that arrives
// as several rows must still add up rather than average.
const isCumulative = CUMULATIVE_HK_TYPES.has(
type as MeasurementType,
);
@@ -1339,6 +1341,10 @@ export function HealthChart({
// the decision separately" drift the visibleSlice doc-comment warns
// about, and the two had already diverged on the windowing semantics.
const activeBucket: ChartBucketType = visibleSlice?.bucketType ?? "day";
// What a folded point is: the average day of its week or month (a daily
// total's average for steps and the like). Named on the caption chip and
// in the tooltip.
const bucketCaption = bucketCaptionKey(activeBucket, types);
const yDomain = useMemo<[number, number] | undefined>(() => {
if (!chartDataWithCompare?.length) return undefined;
@@ -1730,13 +1736,12 @@ export function HealthChart({
) : (
<h2 className="text-sm font-semibold">{title}</h2>
)}
{activeBucket !== "day" && (
<span className="bg-muted/40 text-muted-foreground text-2xs hidden rounded-md px-1.5 py-0.5 font-medium tracking-wide uppercase sm:inline-flex">
{t(
activeBucket === "week"
? "charts.bucketWeekly"
: "charts.bucketMonthly",
)}
{bucketCaption && (
<span
data-slot="chart-bucket-caption"
className="bg-muted/40 text-muted-foreground text-2xs hidden rounded-md px-1.5 py-0.5 font-medium tracking-wide uppercase sm:inline-flex"
>
{t(bucketCaption)}
</span>
)}
{/* v1.4.16 phase B8 — comparison caption. Inline with the
@@ -2260,7 +2265,11 @@ export function HealthChart({
return (
<RichChartTooltip
active
label={dateLabel}
label={
bucketCaption
? `${dateLabel} · ${t(bucketCaption)}`
: dateLabel
}
rows={rows}
/>
);
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { bucketCaptionKey } from "../bucket-caption";
describe("bucketCaptionKey", () => {
it("names a cumulative metric's folded point as a daily average", () => {
expect(bucketCaptionKey("week", ["ACTIVITY_STEPS"])).toBe(
"charts.bucketWeeklyDaily",
);
expect(bucketCaptionKey("month", ["ACTIVE_ENERGY_BURNED"])).toBe(
"charts.bucketMonthlyDaily",
);
});
it("keeps the plain average for a level", () => {
expect(bucketCaptionKey("week", ["WEIGHT"])).toBe("charts.bucketWeekly");
expect(
bucketCaptionKey("month", ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]),
).toBe("charts.bucketMonthly");
});
it("says nothing for days", () => {
expect(bucketCaptionKey("day", ["ACTIVITY_STEPS"])).toBeNull();
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* What a chart point means once the visible range folds days into weeks or
* months, as the caption and the tooltip name it.
*
* Every point of a week or month is the average day of that bucket, on every
* read path (the day points the chart folds itself, and the rollup tier rows
* the server serves for the "All" range). For a level (weight, pulse) that is
* simply the week's or month's average. For a cumulative metric (steps,
* energy, distance) it is the average DAILY total, which keeps the axis
* comparable with the 7, 30 and 90 day tabs; calling it a "weekly average"
* read as an average week's total, a figure seven times larger.
*/
import type { MeasurementType } from "@/generated/prisma/client";
import { CUMULATIVE_HK_TYPES } from "@/lib/measurements/apple-health-mapping";
import type { ChartBucketType } from "@/lib/charts/bucket-time-series";
export function bucketCaptionKey(
bucket: ChartBucketType,
types: readonly string[],
): string | null {
if (bucket === "day") return null;
const daily =
types.length > 0 &&
types.every((type) => CUMULATIVE_HK_TYPES.has(type as MeasurementType));
if (bucket === "week") {
return daily ? "charts.bucketWeeklyDaily" : "charts.bucketWeekly";
}
return daily ? "charts.bucketMonthlyDaily" : "charts.bucketMonthly";
}