Numbers, units and precision
The canonical rules for rendering a health number, covering digits, units, zero and rounding. Rounding is a truth claim here, not a formatting preference.
What this means
This is the canonical page for how a number is rendered in opsinjs. Where any other page in this documentation discusses number formatting, it defers here.
Each number a health interface displays asserts an accuracy, and the asserted
accuracy is set by how many digits are printed. 71 kg asserts a weight known to
about a kilogram, 71.4 kg asserts one known to a hundred grams, and
71.4396 kg asserts one known to a tenth of a gram, which is a laboratory
measurement rather than a domestic one. At most one of the three is true of any
given scale, and which one is a property of the instrument rather than of the
rendering code. The third is what an unformatted float produces, because printing
a float without a rule emits every digit the stored value happens to carry, and
that is an accuracy claim the measurement chain behind the value does not support.
The unit carries the other half of the claim. A single measurement takes different values in different unit systems, and in health the plausible ranges of those values overlap, so a missing or wrong unit renders as a different and believable reading rather than as an error. The reader is given no signal that a substitution has happened, and the component that rendered the number had none either unless the unit travelled with the value.
The rule
Every displayed health value is formatted by an explicit rule that states its precision, its unit and its locale. There is no default float rendering anywhere.
Seventeen requirements follow, grouped below by what they govern. They are numbered continuously across the four groups because the evidence section cites them by number.
Precision
| # | Requirement | Where it applies | Failure it prevents |
|---|---|---|---|
| 1 | Never display more precision than the source carries. Precision travels with the measurement from its source and is not chosen at render time. | Every rendering of a measured value, including a column whose figures would align more neatly at a deeper precision. | An accuracy claim the instrument that produced the value cannot support. |
| 2 | Precision is a property of the metric rather than of the value. The same metric is shown to the same number of decimal places every time. | Every surface in the product on which a given metric appears. | A reader re-parsing the format before they can compare two readings of the same metric. |
| 3 | Round half away from zero, once, at the point of display. An already-rounded value is never rounded a second time, and no value is rounded before a comparison against a threshold. | The display boundary, after every calculation and every comparison the product performs. | Compounded rounding drift, and a comparison decided by the display rule rather than by the recorded value. |
| 4 | Derived and estimated values are rounded harder than measured ones, and are labelled as estimates. | Any value the product computed or inferred rather than read from an instrument. See Data provenance and device accuracy. | An estimate carrying the precision, and with it the authority, of a measurement. |
| 5 | Never pad with false zeros: a source value of 5 is shown as 5 or as the metric's fixed 5.0, and never as 5.00. | Tables and columns in which a neighbouring metric carries two decimal places. | Precision borrowed from the metric rendered next to it. |
Units
| # | Requirement | Where it applies | Failure it prevents |
|---|---|---|---|
| 6 | A value is never displayed without its unit. | A tile, a chart tooltip, a table header whose number sits in the cell underneath, and an accessible name. | A number read against whichever unit system the reader assumed, with nothing on screen to correct the assumption. |
| 7 | The unit is part of the value, visually and semantically. It does not wrap away from its number and it is not styled as a footnote. | Type treatment and line breaking wherever a value is laid out. | A unit separated from its number by a line break, or demoted until a reader scanning the screen does not register it. |
| 8 | The unit is spoken as words rather than letters. mmHg is announced as "millimetres of mercury" and mmol/L as "millimoles per litre". | The accessible name of every value a screen reader can reach. | A screen reader reading "one twenty over eighty em em aitch gee". |
| 9 | Never convert silently: a converted value states that it has been converted, and the unit system is the reader's choice. See Unit systems. | Any surface showing a value in a system other than the one it was recorded in. | A converted value compared against a recorded one without the reader knowing the two were produced differently. |
Composition and locale
| # | Requirement | Where it applies | Failure it prevents |
|---|---|---|---|
| 10 | Use the reader's locale for the decimal separator and for grouping, and never hand-roll either. | Every numeric string the product formats, at the point of formatting. | A decimal separator read as a grouping separator, which shifts the value by a factor of ten or more; the worked case is in "Why (evidence)" below. |
| 11 | Non-decimal composite values keep their conventional form. Blood pressure is 118/76 rather than two fields, and a height in feet and inches is one value. | Any measurement whose conventional written form is not a single decimal number. | A reader reassembling into one reading what the interface has split into parts. |
| 12 | A two-ended span is joined by the word "to" and carries its unit once, after the second number: 3.9 to 5.6 mmol/L. A one-sided span is written "up to 5.6 mmol/L" or "5.6 mmol/L and upwards". No dash of any kind joins two numbers. | Any two-ended span a component renders, including one the product supplies. | Two ends read as two separate values; a unit read as applying only to the number it sits beside; and a character between the two ends that speech synthesis either skips or announces as punctuation. |
| 13 | Zero, none and unknown are three different things. 0 steps is a measurement, "No readings yet" is an absence, and "Not available" is a failure. An absence renders as words and nothing else: no glyph precedes the words, no glyph stands in for them, and the slot is never left blank or filled with 0. | Every empty, missing and error state on a surface that displays a value. | An absence read as a measurement of zero, a failure read as an absence, or a slot a reader cannot tell apart from a component that failed to render. |
| 14 | Negative and out-of-scale values are shown rather than clamped. | Gauges, dials, bars and any scale with a fixed end. | A value far beyond the end of a scale rendered in the same position as one that merely reaches it. |
| 15 | Numerals are always digits rather than words. The interface writes "You have 3 readings" and not "You have three readings". | Running interface prose as well as value slots. | A count missed by a reader scanning the screen, and by a reader with low numeracy who is looking for the digit. |
Typography
| # | Requirement | Where it applies | Failure it prevents |
|---|---|---|---|
| 16 | Use tabular figures for any number that appears in a column or updates in place. | Lists, tables, and any value that re-renders while the reader is looking at it. | Digits changing width between renders, which shifts the value sideways and leaves two readings hard to align. |
| 17 | The value is the largest thing in its container. | Every tile, card and result surface that has a single subject. | A reader locating the value by searching rather than at a glance, and reading a neighbouring figure on the way. |
Why (evidence)
Applying it
Do
Render 118/76 mmHg as one composite value in its conventional form, with
the unit given once and the whole announced as "118 over 76 millimetres of
mercury".
Don’t
Split the reading into Systolic: 118.0 · Diastolic: 76.0. Two fields carry
a precision neither measurement has, and the reader has to reassemble a
reading they already know how to read as one value.
Do
Show 71.4 kg from a scale that reports to 100 g, at that precision, every
time, on every screen that displays the metric.
Don’t
Show 71.4 kg on the dashboard, 71 kg in the summary and 71.44 kg in the
export. The reader cannot tell whether they lost weight or the app changed
its mind.
Do
Render an absence as an absence and give it a next step: "No readings yet. Log your first one to start."
Don’t
Render a missing reading as 0 mmol/L. A nullish default produces a
physiologically impossible value and presents it as a measurement.
Do
Show an out-of-scale value at its real magnitude with the scale annotated as exceeded: "218 mmHg. That is above the top of this scale."
Don’t
Pin the dot to the end of the bar. The reader sees "at the top of the range" where the value is far beyond it.
Components that implement this
Value is the enforcement point: it is specified to require a unit and a
precision rule rather than accepting a pre-formatted string, so a raw
toFixed(2) cannot reach the screen through it. Every other component that
displays a number is specified to render it through Value rather than
formatting inline.
What this does not cover
- Which unit system to display in, and conversion between systems, which is Unit systems.
- Probabilities, risks and percentages, which have their own failure modes: Risk and statistics.
- Dates and relative times in prose, which are Numbers, dates and time; that page defers to this one for the numbers themselves.
- Capitalisation, punctuation and abbreviation in UI strings, which are Grammar and mechanics.
- Chart axis scaling and tick labelling, which are Chart anatomy.
- Whether the number is correct. This page governs how a value is rendered; the accuracy of the value itself belongs to the source that produced it and to Data provenance and device accuracy.
Updates to this page
Last read through against the system on 2026-09-12. Due for review every 6 months; expiry is reported by pnpm run check:freshness.
Reference ranges
How to show a value against a range without implying a diagnosis, and why the word "normal" is banned from every user-facing string about a person's own result.
Unit systems
mmol/L against mg/dL, kilograms against stones and pounds, Celsius against Fahrenheit. Conversion is where a wrong answer is a plausible number, not an error.