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:
Marc-André Bombeck committed 2026-10-01 09:08:11 +02:00
1 parent d6f79c8bef
commit bc2d60cce8
16 files changed
+229 -38

No files matched your search

@@ -106,13 +106,31 @@ describe("computeTrendDescriptor", () => {
});
describe("numericDescriptorCopy", () => {
it("states a preference-bound slot in the unit the caller resolves", () => {
const d = computeTrendDescriptor(series([181.6, 178.5]), {
absoluteFloor: 0.66,
relativeFloor: 0.01,
decimals: 1,
})!;
const copy = numericDescriptorCopy("weight", d, (type) =>
type === "WEIGHT" ? "lb" : "?",
);
expect(copy?.params.unit).toBe(" lb");
expect(TREND_SLOT_DESCRIPTOR_META.distance.unitType).toBe(
"WALKING_RUNNING_DISTANCE",
);
expect(TREND_SLOT_DESCRIPTOR_META.body_temp.unitType).toBe(
"BODY_TEMPERATURE",
);
});
it("resolves the rising template with a signed delta and spaced unit", () => {
const d = computeTrendDescriptor(series([120, 128]), {
absoluteFloor: 2,
relativeFloor: 0.02,
decimals: 0,
})!;
const copy = numericDescriptorCopy("bp", d);
const copy = numericDescriptorCopy("bp", d, () => "unused");
expect(copy?.key).toBe("insights.trendDescriptor.rising");
expect(copy?.params.delta).toBe("+8");
expect(copy?.params.unit).toBe(" mmHg");
@@ -124,7 +142,7 @@ describe("numericDescriptorCopy", () => {
relativeFloor: 0.05,
decimals: 0,
})!;
const copy = numericDescriptorCopy("steps", d);
const copy = numericDescriptorCopy("steps", d, () => "unused");
expect(copy?.key).toBe("insights.trendDescriptor.rising");
expect(copy?.params.unit).toBe("");
});
@@ -135,14 +153,14 @@ describe("numericDescriptorCopy", () => {
relativeFloor: 0.01,
decimals: 1,
})!;
const copy = numericDescriptorCopy("weight", d);
const copy = numericDescriptorCopy("weight", d, () => "kg");
expect(copy?.key).toBe("insights.trendDescriptor.falling");
expect(copy?.params.delta).toBe("−1.4");
});
it("returns null for a slot with no numeric meta (mood)", () => {
const d = computeTrendDescriptor(series([3, 4]), MOOD_DESCRIPTOR_CONFIG)!;
expect(numericDescriptorCopy("mood", d)).toBeNull();
expect(numericDescriptorCopy("mood", d, () => "")).toBeNull();
});
it("covers every numeric slot in the descriptor meta", () => {
@@ -154,7 +172,7 @@ describe("numericDescriptorCopy", () => {
relativeFloor: 0,
decimals: 1,
})!;
expect(numericDescriptorCopy(metric, d)).not.toBeNull();
expect(numericDescriptorCopy(metric, d, () => "u")).not.toBeNull();
}
});
});
+6 -6
View File
@@ -51,7 +51,12 @@ export interface TrendChartConfig {
types: string[];
/** Recharts stroke colours, positionally matched to `types`. */
colors: string[];
/** Display unit (chart legend / tooltip). */
/**
* Display unit (chart legend / tooltip) for a metric no preference
* changes. A single-series slot whose type has a display transform
* (weight, distance, body temperature) carries none: the renderer takes
* the unit, scale and offset from the reader's preference instead.
*/
unit?: string;
/** Y-axis unit label when it differs from the value unit. */
yAxisUnit?: string;
@@ -114,7 +119,6 @@ export const TREND_CHART_CONFIG: Record<
kind: "health-chart",
types: ["WEIGHT"],
colors: ["var(--chart-1)"],
unit: "kg",
titleKey: "charts.weight",
captionKey: "insights.trendsRow.caption.weight",
annotationKey: "weight",
@@ -212,8 +216,6 @@ export const TREND_CHART_CONFIG: Record<
kind: "health-chart",
types: ["WALKING_RUNNING_DISTANCE"],
colors: ["var(--chart-4)"],
unit: "km",
yAxisUnit: "km",
titleKey: "charts.distance",
captionKey: "insights.trendsRow.caption.distance",
detailHref: "/insights/walking-distance",
@@ -234,8 +236,6 @@ export const TREND_CHART_CONFIG: Record<
kind: "health-chart",
types: ["BODY_TEMPERATURE"],
colors: ["var(--destructive)"],
unit: "°C",
yAxisUnit: "°C",
titleKey: "charts.bodyTemperature",
captionKey: "insights.trendsRow.caption.body_temp",
detailHref: "/insights/body-temperature",
+18 -6
View File
@@ -140,8 +140,17 @@ export function computeTrendDescriptor(
* dedicated copy rather than the numeric `{delta} {unit}` template.
*/
export interface TrendSlotDescriptorMeta {
/** Display unit interpolated into the numeric descriptor template. */
unit: string;
/**
* Display unit interpolated into the numeric descriptor template, for a
* metric no preference changes.
*/
unit?: string;
/**
* For a metric whose unit follows the reader's preference: the
* measurement type the caller resolves the unit of (and converts the
* series and the noise floor through) before describing it.
*/
unitType?: string;
config: TrendDescriptorConfig;
}
@@ -154,7 +163,7 @@ export const TREND_SLOT_DESCRIPTOR_META: Record<
config: { absoluteFloor: 2, relativeFloor: 0.02, decimals: 0 },
},
weight: {
unit: "kg",
unitType: "WEIGHT",
config: { absoluteFloor: 0.3, relativeFloor: 0.01, decimals: 1 },
},
pulse: {
@@ -186,7 +195,7 @@ export const TREND_SLOT_DESCRIPTOR_META: Record<
config: { absoluteFloor: 1, relativeFloor: 0.1, decimals: 0 },
},
distance: {
unit: "km",
unitType: "WALKING_RUNNING_DISTANCE",
config: { absoluteFloor: 0.3, relativeFloor: 0.05, decimals: 1 },
},
vo2_max: {
@@ -194,7 +203,7 @@ export const TREND_SLOT_DESCRIPTOR_META: Record<
config: { absoluteFloor: 0.5, relativeFloor: 0.02, decimals: 1 },
},
body_temp: {
unit: "°C",
unitType: "BODY_TEMPERATURE",
config: { absoluteFloor: 0.2, relativeFloor: 0.005, decimals: 1 },
},
};
@@ -231,9 +240,12 @@ export interface TrendDescriptorCopy {
export function numericDescriptorCopy(
metric: string,
descriptor: TrendDescriptor,
/** Resolves a `unitType` slot's unit under the reader's preference. */
unitFor: (type: string) => string,
): TrendDescriptorCopy | null {
const meta = TREND_SLOT_DESCRIPTOR_META[metric];
if (!meta) return null;
const unit = meta.unitType ? unitFor(meta.unitType) : (meta.unit ?? "");
// Signed, formatted magnitude (e.g. "+8", "-1.4"). `stable` still
// carries its small delta so the caption can read "stable (±0 …)" if a
@@ -255,7 +267,7 @@ export function numericDescriptorCopy(
// reads "({delta}{unit})" → "(+8 mmHg)" with a unit, "(+1200)"
// without one — no dangling space inside the parens for unit-less
// metrics (steps, flights).
unit: meta.unit ? ` ${meta.unit}` : "",
unit: unit ? ` ${unit}` : "",
},
};
}