mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix(insights): plot the trends row and log fevers in the reader's units
Two more surfaces carried a fixed unit. The insights trends row labelled weight in kg and body temperature in °C for every reader, and plotted walking distance as raw metres under a km label; its trend caption said the same. A single transformed series there now takes unit, scale and offset from the preference, and the caption describes the converted series against a converted noise floor. The illness day sheet and timeline showed and took a fever in °C. The fever is now seeded, typed and shown in the reader's temperature unit and stored in °C as before. Refs #1067
This commit is contained in:
1 parent
d6f79c8bef
commit
bc2d60cce8
16 files changed
+229
-38
No files matched your search
@@ -21,14 +21,40 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
vi.mock("next/dynamic", () => ({
|
||||
default: () => {
|
||||
const Stub = ({ title }: { title?: string }) => (
|
||||
<div data-slot="trends-row-chart-stub">{title ?? "chart"}</div>
|
||||
const Stub = ({
|
||||
title,
|
||||
unit,
|
||||
valueScale,
|
||||
valueOffset,
|
||||
}: {
|
||||
title?: string;
|
||||
unit?: string;
|
||||
valueScale?: number;
|
||||
valueOffset?: number;
|
||||
}) => (
|
||||
<div
|
||||
data-slot="trends-row-chart-stub"
|
||||
data-unit={unit}
|
||||
data-scale={valueScale}
|
||||
data-offset={valueOffset}
|
||||
>
|
||||
{title ?? "chart"}
|
||||
</div>
|
||||
);
|
||||
Stub.displayName = "TrendsRowChartStub";
|
||||
return Stub;
|
||||
},
|
||||
}));
|
||||
|
||||
const account = vi.hoisted(() => ({
|
||||
user: null as Record<string, unknown> | null,
|
||||
}));
|
||||
vi.mock("@/hooks/use-auth", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/hooks/use-auth")>()),
|
||||
useAuth: () => ({ user: account.user, isAuthenticated: true }),
|
||||
useAccountOnceMounted: () => account.user,
|
||||
}));
|
||||
|
||||
import { TrendsRow } from "../trends-row";
|
||||
import type { DailyBriefing } from "@/lib/ai/schema";
|
||||
|
||||
@@ -306,4 +332,27 @@ describe("<TrendsRow>", () => {
|
||||
expect(html).toMatch(/data-metric="steps"/);
|
||||
expect(html).toContain('href="/insights/steps"');
|
||||
});
|
||||
|
||||
it("plots a transformed slot in the reader's unit, scale and offset included", () => {
|
||||
account.user = { unitPreference: "imperial", glucoseUnit: null };
|
||||
try {
|
||||
const html = render(
|
||||
<TrendsRow briefing={briefing(["weight", "distance", "body_temp"])} />,
|
||||
);
|
||||
expect(html).toContain('data-unit="lb"');
|
||||
expect(html).toContain('data-unit="mi"');
|
||||
expect(html).toContain('data-unit="°F"');
|
||||
expect(html).toContain('data-offset="32"');
|
||||
expect(html).not.toContain('data-unit="kg"');
|
||||
expect(html).not.toContain('data-unit="km"');
|
||||
} finally {
|
||||
account.user = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("plots distance in kilometres, scaled from metres, for a metric reader", () => {
|
||||
const html = render(<TrendsRow briefing={briefing(["distance"])} />);
|
||||
expect(html).toContain('data-unit="km"');
|
||||
expect(html).toContain('data-scale="0.001"');
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CONFIDENCE_BADGE_CLASS } from "./confidence-badge";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B3 — single-sentence AI annotation rendered directly
|
||||
@@ -264,6 +265,7 @@ export function TrendDescriptorCaption({
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { toDisplay, transformFor, unitFor } = useUnitDisplay();
|
||||
|
||||
const isMood = kind === "mood";
|
||||
const primaryType = types[0] ?? "";
|
||||
@@ -323,18 +325,28 @@ export function TrendDescriptorCaption({
|
||||
}
|
||||
// The numeric series is already server-bounded to the trailing
|
||||
// 30-day window by the fetch, so every returned row is in-window.
|
||||
// Described in the reader's unit, the same one the chart above plots.
|
||||
return (numericQuery.data ?? []).map((row) => ({
|
||||
timestamp: new Date(row.measuredAt).getTime(),
|
||||
value: row.value,
|
||||
value: toDisplay(primaryType, row.value),
|
||||
}));
|
||||
}, [isMood, moodQuery.data, numericQuery.data]);
|
||||
}, [isMood, moodQuery.data, numericQuery.data, toDisplay, primaryType]);
|
||||
|
||||
const descriptor = useMemo(() => {
|
||||
const meta = TREND_SLOT_DESCRIPTOR_META[metric];
|
||||
// A slot that converts its series converts its noise floor with it (a
|
||||
// floor is a difference: factor only).
|
||||
const config = isMood
|
||||
? MOOD_DESCRIPTOR_CONFIG
|
||||
: TREND_SLOT_DESCRIPTOR_META[metric]?.config;
|
||||
: meta?.unitType
|
||||
? {
|
||||
...meta.config,
|
||||
absoluteFloor:
|
||||
meta.config.absoluteFloor * transformFor(meta.unitType).factor,
|
||||
}
|
||||
: meta?.config;
|
||||
return computeTrendDescriptor(points, config);
|
||||
}, [points, isMood, metric]);
|
||||
}, [points, isMood, metric, transformFor]);
|
||||
|
||||
// Tier 4 — genuinely too few points (also covers the in-flight window
|
||||
// before the small series lands). Surface the real empty hint so the
|
||||
@@ -361,7 +373,7 @@ export function TrendDescriptorCaption({
|
||||
// copy defensively (should not happen for the legacy triple).
|
||||
const copy = isMood
|
||||
? moodDescriptorCopy(descriptor)
|
||||
: (numericDescriptorCopy(metric, descriptor) ??
|
||||
: (numericDescriptorCopy(metric, descriptor, unitFor) ??
|
||||
moodDescriptorCopy(descriptor));
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type TrendAnnotationConfidenceBand,
|
||||
type TrendAnnotationStatus,
|
||||
} from "./trend-annotation";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
|
||||
/**
|
||||
* Trends row — a small chart per metric above a one-sentence
|
||||
@@ -116,17 +117,29 @@ function MetricChart({
|
||||
title: string;
|
||||
}) {
|
||||
const { user } = useAuth();
|
||||
const { transformFor, isTransformed } = useUnitDisplay();
|
||||
const userTimezone = user?.timezone;
|
||||
if (config.kind === "mood") {
|
||||
return <MoodChart title={title} mini />;
|
||||
}
|
||||
// A single transformed series (weight, distance, temperature) plots in
|
||||
// the reader's unit: the chart folds scale and offset in at its read
|
||||
// boundary, exactly as the metric's own insights page does. Distance
|
||||
// used to plot raw metres under a "km" label for every reader.
|
||||
const primary = config.types[0] ?? "";
|
||||
const transform =
|
||||
config.types.length === 1 && isTransformed(primary)
|
||||
? transformFor(primary)
|
||||
: null;
|
||||
return (
|
||||
<HealthChartDynamicMini
|
||||
types={config.types}
|
||||
title={title}
|
||||
colors={config.colors}
|
||||
unit={config.unit}
|
||||
yAxisUnit={config.yAxisUnit}
|
||||
unit={transform ? transform.displayUnit : config.unit}
|
||||
yAxisUnit={transform ? undefined : config.yAxisUnit}
|
||||
valueScale={transform?.factor}
|
||||
valueOffset={transform?.offset}
|
||||
mini
|
||||
userTimezone={userTimezone}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user