Show whole-number metrics as whole numbers

The dashboard tiles printed every value at one decimal: '121,0 mmHg',
'60,0 bpm', '9.240,0' steps. TrendCard takes the metric's precision, and
blood pressure, pulse, HRV, steps and mg/dL glucose pass zero. The
measurement list names count-shaped units the way the insights pages do
('13 Etagen', '12,5 Atemzüge/min') instead of the source's 'count'.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 16:21:12 +02:00
1 parent b80dc14142
commit 554f36aeab
6 files changed
+108 -7

No files matched your search

@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { Activity } from "lucide-react";
import { TrendCard } from "../trend-card";
import { I18nProvider } from "@/lib/i18n/context";
/**
* A whole-number metric reads as a whole number on the tile: "121 mmHg",
* not "121,0 mmHg", and "9.240" steps rather than "9.240,0".
*/
function render(node: React.ReactNode) {
return renderToStaticMarkup(
<I18nProvider initialLocale="de">{node}</I18nProvider>,
);
}
const base = {
label: "Schritte",
latest: 9240,
unit: "",
avg7: 9535.14,
avg30: 9100,
slope30: null,
icon: Activity,
};
describe("<TrendCard> fractionDigits", () => {
it("keeps one decimal by default", () => {
const html = render(<TrendCard {...base} />);
expect(html).toContain("9.240,0");
});
it("prints whole numbers, averages and deltas at 0", () => {
const html = render(
<TrendCard {...base} fractionDigits={0} trend7Delta={-1383.2} />,
);
expect(html).toContain("9.240");
expect(html).not.toContain("9.240,0");
expect(html).toContain("9.535");
expect(html).not.toContain("9.535,1");
expect(html).toContain("−1.383");
expect(html).not.toContain("−1.383,2");
});
it("reads a sub-step delta as zero at 0 decimals", () => {
const html = render(
<TrendCard {...base} fractionDigits={0} trend7Delta={0.3} />,
);
expect(html).toContain("±0");
});
});
+12 -3
View File
@@ -101,6 +101,13 @@ interface TrendCardProps {
* before (no reserved space, no layout shift).
*/
valueAdornment?: React.ReactNode;
/**
* Decimal places for the headline, the averages and the deltas. Defaults
* to 1. A whole-number metric (steps, mmHg, bpm, mg/dL) passes 0 so the
* tile reads "121 mmHg", not "121,0 mmHg" — the precision its own
* insights page uses.
*/
fractionDigits?: number;
}
export function TrendCard({
@@ -123,6 +130,7 @@ export function TrendCard({
staleDays = null,
emptyHint = null,
valueAdornment = null,
fractionDigits = 1,
}: TrendCardProps) {
const { t, tCount } = useTranslations();
const fmt = useFormatters();
@@ -152,7 +160,7 @@ export function TrendCard({
const arrowSentiment = getTrendSentiment(primarySignal, directionSentiment);
const trendColor = sentimentColorClass(arrowSentiment);
const formatValue = (value: number) => fmt.number(value, 1);
const formatValue = (value: number) => fmt.number(value, fractionDigits);
// v1.4.15 Fix 4 — the 7-day delta now shares the arrow's colour so
// both elements communicate the same sentiment for the same signal.
@@ -171,9 +179,10 @@ export function TrendCard({
const comparisonDeltaColor = sentimentColorClass(comparisonSentiment);
const formatDelta = (value: number): string => {
if (Math.abs(value) < 0.05) return `±0`;
// Below half a step of the shown precision the delta prints as zero.
if (Math.abs(value) < 0.5 * 10 ** -fractionDigits) return `±0`;
const sign = value > 0 ? "+" : "−";
return `${sign}${fmt.number(Math.abs(value), 1)}`;
return `${sign}${fmt.number(Math.abs(value), fractionDigits)}`;
};
// The label flips from "7d" / "7T" (mean) to "7d trend" / "7T-Trend"
@@ -4,6 +4,7 @@ import {
MEASUREMENT_TYPE_LABEL_KEYS,
MEASUREMENT_TYPE_ICONS,
MEASUREMENT_TYPE_COLORS,
measurementUnitLabel,
} from "../measurement-list-meta";
import enMessages from "../../../../messages/en.json";
import deMessages from "../../../../messages/de.json";
@@ -41,3 +42,18 @@ describe("measurement-list-meta", () => {
}
});
});
describe("measurementUnitLabel", () => {
const t = (key: string) => `t:${key}`;
it("shows the localised word for count-shaped source units", () => {
expect(measurementUnitLabel("FLIGHTS_CLIMBED", "count", t)).toBe(
"t:insights.units.flights",
);
expect(measurementUnitLabel("RESPIRATORY_RATE", "count/min", t)).toBe(
"t:insights.units.respiratoryRate",
);
});
it("passes every other unit through", () => {
expect(measurementUnitLabel("WEIGHT", "kg", t)).toBe("kg");
});
});
@@ -104,6 +104,7 @@ import {
MEASUREMENT_TYPE_LABEL_KEYS as TYPE_LABEL_KEYS,
MEASUREMENT_TYPE_ICONS as TYPE_ICONS,
MEASUREMENT_TYPE_COLORS as TYPE_COLORS,
measurementUnitLabel,
} from "./measurement-list-meta";
import {
measurementListFiltersToSearch,
@@ -1262,7 +1263,11 @@ export function MeasurementList({
rowDisplay(m).value,
rawDisplayFractionDigits(m.type),
)}{" "}
{isGrouped ? m.unit : rowDisplay(m).unit}
{measurementUnitLabel(
m.type,
isGrouped ? m.unit : rowDisplay(m).unit,
t,
)}
{isGrouped && (
<span className="text-muted-foreground ml-2 text-xs font-normal">
{t("measurements.dailyTotalCaption", {
@@ -1486,7 +1491,11 @@ export function MeasurementList({
rowDisplay(m).value,
rawDisplayFractionDigits(m.type),
)}{" "}
{isGrouped ? m.unit : rowDisplay(m).unit}
{measurementUnitLabel(
m.type,
isGrouped ? m.unit : rowDisplay(m).unit,
t,
)}
</>
)}
</span>