From 7b5ea8b3536fc9ade3f52853d40dbefc3b205d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 27 Sep 2026 14:08:50 +0200 Subject: [PATCH] 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. --- messages/de.json | 2 ++ messages/en.json | 2 ++ messages/es.json | 2 ++ messages/fr.json | 2 ++ messages/it.json | 2 ++ messages/ko.json | 2 ++ messages/pl.json | 2 ++ src/components/charts/health-chart.tsx | 29 ++++++++++++------- .../charts/__tests__/bucket-caption.test.ts | 25 ++++++++++++++++ src/lib/charts/bucket-caption.ts | 29 +++++++++++++++++++ 10 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 src/lib/charts/__tests__/bucket-caption.test.ts create mode 100644 src/lib/charts/bucket-caption.ts diff --git a/messages/de.json b/messages/de.json index 90964abea..8a29626cb 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/en.json b/messages/en.json index b9eae3f92..aca1a4098 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/es.json b/messages/es.json index 3dca0af66..948951a84 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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", diff --git a/messages/fr.json b/messages/fr.json index d7e6af27c..eb4ac6541 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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", diff --git a/messages/it.json b/messages/it.json index aeb29b538..6345f3a6a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -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", diff --git a/messages/ko.json b/messages/ko.json index 08a012016..189d2a903 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -2261,6 +2261,8 @@ "noData": "선택한 기간에 데이터가 없어요", "bucketWeekly": "주 평균", "bucketMonthly": "월 평균", + "bucketWeeklyDaily": "주별 일평균", + "bucketMonthlyDaily": "월별 일평균", "systolic": "수축기", "diastolic": "이완기", "bodyWater": "체수분", diff --git a/messages/pl.json b/messages/pl.json index bcf9065b9..c20040200 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -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", diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx index d2d766ce8..229f38a21 100644 --- a/src/components/charts/health-chart.tsx +++ b/src/components/charts/health-chart.tsx @@ -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({ ) : (

{title}

)} - {activeBucket !== "day" && ( - - {t( - activeBucket === "week" - ? "charts.bucketWeekly" - : "charts.bucketMonthly", - )} + {bucketCaption && ( + + {t(bucketCaption)} )} {/* v1.4.16 phase B8 — comparison caption. Inline with the @@ -2260,7 +2265,11 @@ export function HealthChart({ return ( ); diff --git a/src/lib/charts/__tests__/bucket-caption.test.ts b/src/lib/charts/__tests__/bucket-caption.test.ts new file mode 100644 index 000000000..70e7fd8ae --- /dev/null +++ b/src/lib/charts/__tests__/bucket-caption.test.ts @@ -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(); + }); +}); diff --git a/src/lib/charts/bucket-caption.ts b/src/lib/charts/bucket-caption.ts new file mode 100644 index 000000000..2af076a8a --- /dev/null +++ b/src/lib/charts/bucket-caption.ts @@ -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"; +}