From bc2d60cce8286d4e7e593949abc723d372c9987a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 1 Oct 2026 09:08:11 +0200 Subject: [PATCH] fix(insights): plot the trends row and log fevers in the reader's units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 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 +- .../illness-day-timeline-units.test.tsx | 70 +++++++++++++++++++ .../illness/illness-day-timeline.tsx | 7 +- src/components/illness/log-day-sheet.tsx | 20 ++++-- .../insights/__tests__/trends-row.test.tsx | 53 +++++++++++++- src/components/insights/trend-annotation.tsx | 22 ++++-- src/components/insights/trends-row.tsx | 17 ++++- .../__tests__/trend-descriptor.test.ts | 28 ++++++-- src/lib/insights/trend-chart-select.ts | 12 ++-- src/lib/insights/trend-descriptor.ts | 24 +++++-- 16 files changed, 229 insertions(+), 38 deletions(-) create mode 100644 src/components/illness/__tests__/illness-day-timeline-units.test.tsx diff --git a/messages/de.json b/messages/de.json index fc3b54979..606ecda35 100644 --- a/messages/de.json +++ b/messages/de.json @@ -9405,7 +9405,7 @@ "severity": "Stärke {severity}", "noSymptoms": "Keine Symptome erfasst.", "impact": "Befinden: {impact}", - "fever": "Temperatur: {value} °C", + "fever": "Temperatur: {value} {unit}", "empty": "Noch keine Tage erfasst." }, "loadError": "Diese Episode konnte nicht geladen werden.", diff --git a/messages/en.json b/messages/en.json index 5fcf26297..72344cb64 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9405,7 +9405,7 @@ "severity": "Severity {severity}", "noSymptoms": "No symptoms logged.", "impact": "How you functioned: {impact}", - "fever": "Temperature: {value} °C", + "fever": "Temperature: {value} {unit}", "empty": "No days logged yet." }, "loadError": "Could not load this episode.", diff --git a/messages/es.json b/messages/es.json index 4bdfb5534..d2e383e31 100644 --- a/messages/es.json +++ b/messages/es.json @@ -9405,7 +9405,7 @@ "severity": "Intensidad {severity}", "noSymptoms": "Sin síntomas registrados.", "impact": "Cómo funcionaste: {impact}", - "fever": "Temperatura: {value} °C", + "fever": "Temperatura: {value} {unit}", "empty": "Todavía no hay días registrados." }, "loadError": "No se pudo cargar este episodio.", diff --git a/messages/fr.json b/messages/fr.json index f1c57d412..bada54749 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -9405,7 +9405,7 @@ "severity": "Intensité {severity}", "noSymptoms": "Aucun symptôme enregistré.", "impact": "Votre forme : {impact}", - "fever": "Température : {value} °C", + "fever": "Température : {value} {unit}", "empty": "Aucun jour enregistré pour l’instant." }, "loadError": "Impossible de charger cet épisode.", diff --git a/messages/it.json b/messages/it.json index 79b439e50..2ba28ca6d 100644 --- a/messages/it.json +++ b/messages/it.json @@ -9405,7 +9405,7 @@ "severity": "Intensità {severity}", "noSymptoms": "Nessun sintomo registrato.", "impact": "Come ti sei sentito: {impact}", - "fever": "Temperatura: {value} °C", + "fever": "Temperatura: {value} {unit}", "empty": "Nessun giorno registrato finora." }, "loadError": "Impossibile caricare questo episodio.", diff --git a/messages/ko.json b/messages/ko.json index 26052e28f..b1fb0504d 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -9405,7 +9405,7 @@ "severity": "심각도 {severity}", "noSymptoms": "기록한 증상이 없어요.", "impact": "활동 정도: {impact}", - "fever": "체온: {value}°C", + "fever": "체온: {value}{unit}", "empty": "아직 기록한 날이 없어요." }, "loadError": "이 에피소드를 불러오지 못했어요.", diff --git a/messages/pl.json b/messages/pl.json index 396f758be..f3c2c7b3b 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -9405,7 +9405,7 @@ "severity": "Nasilenie {severity}", "noSymptoms": "Nie zapisano objawów.", "impact": "Jak funkcjonowałeś: {impact}", - "fever": "Temperatura: {value} °C", + "fever": "Temperatura: {value} {unit}", "empty": "Nie zapisano jeszcze żadnych dni." }, "loadError": "Nie udało się wczytać tego epizodu.", diff --git a/src/components/illness/__tests__/illness-day-timeline-units.test.tsx b/src/components/illness/__tests__/illness-day-timeline-units.test.tsx new file mode 100644 index 000000000..a01dda594 --- /dev/null +++ b/src/components/illness/__tests__/illness-day-timeline-units.test.tsx @@ -0,0 +1,70 @@ +/** + * A logged fever reads in the reader's temperature unit. The timeline and the + * day sheet printed a hardcoded °C, so an imperial account read its own + * fever in Celsius. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +const account = vi.hoisted(() => ({ + user: { unitPreference: "imperial", glucoseUnit: null } as Record< + string, + unknown + >, +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: account.user, isAuthenticated: true }), + useAccountOnceMounted: () => account.user, +})); + +vi.mock("@/hooks/use-record-capabilities", () => ({ + useRecordCapabilities: () => ({ canManageDomain: () => true }), +})); + +vi.mock("../use-illness", () => ({ + useIllnessDayLogList: () => ({ + isLoading: false, + isError: false, + data: { + dayLogs: [ + { + id: "d1", + episodeId: "e1", + date: "2026-09-30", + functionalImpact: null, + feverC: 38.5, + symptoms: [], + note: null, + updatedAt: "2026-09-30T08:00:00.000Z", + }, + ], + }, + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { IllnessDayTimeline } from "../illness-day-timeline"; + +function render(): string { + return renderToStaticMarkup( + + {}} /> + , + ); +} + +describe(" fever unit", () => { + it("shows the fever in °F for an imperial reader", () => { + account.user = { unitPreference: "imperial", glucoseUnit: null }; + const html = render(); + // 38.5 °C = 101.3 °F. + expect(html).toContain("Temperature: 101.3 °F"); + expect(html).not.toContain("°C"); + }); + + it("keeps °C for a metric reader", () => { + account.user = { unitPreference: "metric", glucoseUnit: null }; + expect(render()).toContain("Temperature: 38.5 °C"); + }); +}); diff --git a/src/components/illness/illness-day-timeline.tsx b/src/components/illness/illness-day-timeline.tsx index fa99bea56..3dd029e55 100644 --- a/src/components/illness/illness-day-timeline.tsx +++ b/src/components/illness/illness-day-timeline.tsx @@ -19,6 +19,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations, useFormatters } from "@/lib/i18n/context"; +import { useUnitDisplay } from "@/hooks/use-unit-display"; import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog"; import { useIllnessDayLogList } from "./use-illness"; @@ -32,6 +33,7 @@ function symptomLabelKey(key: string): string | null { function DayLogRow({ log }: { log: IllnessDayLogDTO }) { const { t } = useTranslations(); const fmt = useFormatters(); + const { toDisplay, unitFor } = useUnitDisplay(); return (
@@ -72,7 +74,10 @@ function DayLogRow({ log }: { log: IllnessDayLogDTO }) { {log.feverC !== null ? (

- {t("illness.timeline.fever", { value: fmt.number(log.feverC, 1) })} + {t("illness.timeline.fever", { + value: fmt.number(toDisplay("BODY_TEMPERATURE", log.feverC), 1), + unit: unitFor("BODY_TEMPERATURE"), + })}

) : null} diff --git a/src/components/illness/log-day-sheet.tsx b/src/components/illness/log-day-sheet.tsx index 0afacddc7..b5d7e69da 100644 --- a/src/components/illness/log-day-sheet.tsx +++ b/src/components/illness/log-day-sheet.tsx @@ -25,6 +25,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; +import { useUnitDisplay } from "@/hooks/use-unit-display"; import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog"; import { useIllnessDayLog, useUpsertDayLog } from "./use-illness"; @@ -140,6 +141,10 @@ export function LogDaySheet({ onsetDate, }: LogDaySheetProps) { const { t } = useTranslations(); + // The fever is stored in °C and typed in the reader's temperature unit: + // seeded through the transform, inverted on save. + const { toDisplay, fromDisplay, unitFor } = useUnitDisplay(); + const feverUnit = unitFor("BODY_TEMPERATURE"); // The day being logged — seeded from `date` (today), but the picker lets the // user backdate within [onset, today] for the retrospective journal. @@ -178,7 +183,11 @@ export function LogDaySheet({ new Map((dto?.symptoms ?? []).map((s) => [s.key, s.severity ?? null])), ); setFunctionalImpact(dto?.functionalImpact ?? null); - setFever(dto?.feverC != null ? String(dto.feverC) : ""); + setFever( + dto?.feverC != null + ? String(toDisplay("BODY_TEMPERATURE", dto.feverC)) + : "", + ); setNote(dto?.note ?? ""); } else if (!open && lastFormKey !== null) { setLastFormKey(null); @@ -213,7 +222,10 @@ export function LogDaySheet({ const input: IllnessDayLogInput = { date: selectedDate, functionalImpact, - feverC: feverValue, + feverC: + feverValue == null + ? null + : Math.round(fromDisplay("BODY_TEMPERATURE", feverValue) * 100) / 100, symptoms: Array.from(symptoms.entries()).map(([key, severity]) => ({ key, severity: severity ?? undefined, @@ -333,7 +345,7 @@ export function LogDaySheet({ summary={ feverValue != null ? ( - {feverValue.toFixed(1)} °C + {feverValue.toFixed(1)} {feverUnit} ) : null } @@ -345,7 +357,7 @@ export function LogDaySheet({ setFever(e.target.value)} className="max-w-32" diff --git a/src/components/insights/__tests__/trends-row.test.tsx b/src/components/insights/__tests__/trends-row.test.tsx index c8b18b288..8b85cfd45 100644 --- a/src/components/insights/__tests__/trends-row.test.tsx +++ b/src/components/insights/__tests__/trends-row.test.tsx @@ -21,14 +21,40 @@ import { I18nProvider } from "@/lib/i18n/context"; vi.mock("next/dynamic", () => ({ default: () => { - const Stub = ({ title }: { title?: string }) => ( -
{title ?? "chart"}
+ const Stub = ({ + title, + unit, + valueScale, + valueOffset, + }: { + title?: string; + unit?: string; + valueScale?: number; + valueOffset?: number; + }) => ( +
+ {title ?? "chart"} +
); Stub.displayName = "TrendsRowChartStub"; return Stub; }, })); +const account = vi.hoisted(() => ({ + user: null as Record | null, +})); +vi.mock("@/hooks/use-auth", async (importOriginal) => ({ + ...(await importOriginal()), + 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("", () => { 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( + , + ); + 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(); + expect(html).toContain('data-unit="km"'); + expect(html).toContain('data-scale="0.001"'); + }); }); diff --git a/src/components/insights/trend-annotation.tsx b/src/components/insights/trend-annotation.tsx index 5ce48d013..5b72e9ba9 100644 --- a/src/components/insights/trend-annotation.tsx +++ b/src/components/insights/trend-annotation.tsx @@ -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 ( diff --git a/src/components/insights/trends-row.tsx b/src/components/insights/trends-row.tsx index a7056fd80..9e7b730fa 100644 --- a/src/components/insights/trends-row.tsx +++ b/src/components/insights/trends-row.tsx @@ -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 ; } + // 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 ( diff --git a/src/lib/insights/__tests__/trend-descriptor.test.ts b/src/lib/insights/__tests__/trend-descriptor.test.ts index 9b46547e2..cf7258f7b 100644 --- a/src/lib/insights/__tests__/trend-descriptor.test.ts +++ b/src/lib/insights/__tests__/trend-descriptor.test.ts @@ -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(); } }); }); diff --git a/src/lib/insights/trend-chart-select.ts b/src/lib/insights/trend-chart-select.ts index fc87a27c2..1ed5b8bc9 100644 --- a/src/lib/insights/trend-chart-select.ts +++ b/src/lib/insights/trend-chart-select.ts @@ -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", diff --git a/src/lib/insights/trend-descriptor.ts b/src/lib/insights/trend-descriptor.ts index b01ee8ffb..f42dd31aa 100644 --- a/src/lib/insights/trend-descriptor.ts +++ b/src/lib/insights/trend-descriptor.ts @@ -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}` : "", }, }; }