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"; +}