Skip to content
microcharts

Formatting & scale

Control how a chart shows numbers and dates, and what value range it's drawn against — one format prop (Intl options or a function) feeds the direct labels, the accessible summary and the interactive readout alike, locale handles separators and language, and domain fixes the scale. Formatting is cached, so it stays cheap across hundreds of charts.

Two orthogonal decisions live here: how numbers are written (format + locale) and what range they're drawn against (domain). Both are part of the one grammar — the same prop name means the same thing on every chart — and both are documented per chart, but this page is the whole story in one place, since it's easy to assume a default you didn't actually get.

One formatter, three places

A chart shows numbers in three spots: the direct labels it draws, the accessible summary a screen reader announces, and — on an interactive entry — the readout that follows the pointer. format feeds all three from a single definition — so a currency chart's spoken summary says "$1,240", not "1240". You never format the same number twice.

format takes either Intl.NumberFormatOptions or a plain function (n) => string. Formatters are cached (Intl.NumberFormat construction is expensive), keyed by locale plus options, so hundreds of instances sharing one format shape build one formatter between them — never build your own new Intl.NumberFormat in a component. The cache is deliberately small: it holds 64 formatters and empties completely when it fills, on the reasoning that one app uses a handful of locale × option shapes, and a bounded cache that occasionally rebuilds beats one that grows forever. Need the same formatting outside a chart (a matching KPI label, a table cell)? Import the exported makeFormatter helper so your prose numbers line up with the chart's exactly, from the same cache:

import { makeFormatter } from "@microcharts/react";

const fmt = makeFormatter({ style: "currency", currency: "USD" }, "en-US");
fmt(42); // "$42.00"

The arguments are positional — makeFormatter(format, locale, defaults?). The third argument is the fallback used when format is omitted; it's how a chart supplies its own default (see Delta below).

Currency label
Revenue$1,600

Percent, compact, decimals

Anything Intl.NumberFormat supports works — percent, compact notation, fixed decimals, sign display. Delta even defaults to a locale-aware percent, so a raw ratio reads as a change with no config:

Delta formats
+12.8%+4.2K+$4,200

A custom function

When Intl can't express the unit — milliseconds, "×" multipliers, a domain-specific symbol — pass a function. It receives the number and returns the exact string to render. The value is snapped to 12 significant digits before it reaches your function, so a number a chart derived internally (value - target) arrives as -3.6, not -3.5999999999999943 — you write for clean data, not for IEEE arithmetic.

Custom units
p95 latency31 ms

The interactive readout

Import a chart from its /interactive subpath and it gains a readout: a small chip that follows the pointer, or the focused datum when you rove with the keyboard. It uses the same format and locale as everything else, so a currency chart's hover value matches its label and its summary exactly.

It is deliberately terse. The chip names the datum and its value — a Funnel stage reads Checkout 62% (1,240), a Slope line reads Brazil: 44 → 28 — and nothing else. It does not repeat the axis names, the row label already printed beside it, or the grid position the cell already occupies. These are word-sized charts; a chip wider than the chart it annotates is worse than no chip.

The fuller sentence still exists. Everything the chip leaves out — the comparison, the quadrant name, the running total, the per-cycle breakdown — is announced in the chart's live region, which is what a screen reader reads. So nothing is lost by the chip being short; it is a different audience, not less information.

import { Sparkline } from "@microcharts/react/sparkline/interactive";

// the readout, the endpoint label, and the spoken summary all read "$1,600"
<Sparkline data={revenue} label="last" format={{ style: "currency", currency: "USD" }} />;

The chip is capped at max(100%, 14em) wide and ellipsizes past that, so a long formatted value cannot paint across the page. If you see an ellipsis, the value is too long for the chart — shorten it with format (notation: "compact" is usually the answer) rather than widening the chart.

Its prose comes from strings, like every other word the library renders, so a localized bundle localizes the readout too. Its colors come from --mc-surface, --mc-surface-ink and --mc-surface-edge; see Theming.

Put the value somewhere else. That same terse string is handed to you: every onActive / onSelect callback receives it as datum.formatted, formatted with the chart's own format and locale. So you can suppress the chip and render the value in your own layout — the pattern behind a KPI card whose big number tracks a sparkline you hover:

const [reading, setReading] = useState("$1,600");

// readout={false} keeps the crosshair, drops the chip; the value lands in your <output>
<output>{reading}</output>;
<Sparkline
  data={revenue}
  format={{ style: "currency", currency: "USD" }}
  readout={false}
  onActive={(d) => d && setReading(d.formatted!)}
/>;

readout={false} hides only the chip — hover, keyboard roving, touch scrub, the live-region announcements, and both callbacks all keep working. Leave readout at its default (true) and you get the chip and the callback both.

Locale: separators, not language

locale is a BCP-47 tag ("de-DE", "ja-JP", or an array of fallbacks). It controls how numbers and dates are written — the thousands separator, decimal mark, currency placement — matching the reader's conventions.

Same data, two locales
US1,600DE1.600

locale sets number/date formatting. It does not translate the summary sentence ("Trending up 200%…") — that's the strings contract on Accessibility. Pick both for a fully localized chart: locale for the figures, translated strings for the prose.

Server rendering: pass a locale explicitly

Omitting locale means Intl resolves the runtime's default — and on a server-rendered page that runtime is the server. Node typically resolves to the host's locale while the visitor's browser resolves to theirs, so the same chart can render 1,600 into the HTML and 1.600 after hydration. React reports that as a hydration mismatch, and the mismatched text is not patched up.

This is not specific to microcharts — it is how Intl defaults work — but charts hit it more often than most UI, because the number reaches three places at once: the label, the accessible summary, and the readout.

The fix is to make the choice explicit rather than ambient:

// Deterministic: the server and the browser format identically.
<Sparkline data={revenue} label="last" locale="en-US" />

Pass the locale your page has already resolved (from the request, a cookie, or your i18n provider) and use the same one on both sides. If a chart is client-only, the ambient default is fine.

Scale: the domain decision

domain sets the value range the geometry is drawn against — the vertical frame. Omit it and the chart auto-fits to its own data. Pass [min, max] to pin a fixed frame instead — so a 40–60% reading looks like 40–60% of a 0–100 scale, not a full-height line.

Auto-fit vs. fixed frame
Auto-fit

Auto-fit is not the same everywhere. Charts whose marks read as magnitude from a baseline — Sparkbar, MiniBar, PairedBars, and Sparkline once you turn on fill — anchor the auto-fitted domain at zero, because a bar cut off above zero lies about its own length. Line charts without a fill auto-fit to the data alone. Either way, a completely flat series is padded so it still has a band to sit in rather than collapsing to a degenerate scale.

This is an honesty decision, not cosmetics: auto-fit maximizes detail for a single series, but a fixed domain is what makes two charts comparable. To share one scale across several charts at once, use SparkGroup — it reads every child's data, computes one union domain, and injects it into any child that didn't set its own (zero anchors that shared domain at zero). See Composition.

Dates and times

On calendar and time charts, format still means what it means everywhere else: it formats the numbers. On EventTimeline that's a share, defaulting to a locale-aware percent. On CalendarStrip it's the day's value in the hover/focus readout — so it lives on the interactive entry only. The static CalendarStrip summary counts days — Active 3 of 28 days over 4 weeks. — and a count of days is not a number format gets to restyle.

Date and time strings are a separate prop, dateFormat, taking Intl.DateTimeFormatOptions or a (date) => string function. It lives on the interactive entries (@microcharts/react/calendar-strip/interactive, @microcharts/react/event-timeline/interactive), which are the entries that render a spoken or hovered date at all — locale applies to it the same way.

import { CalendarStrip } from "@microcharts/react/calendar-strip/interactive";

<CalendarStrip data={days} dateFormat={{ month: "short", day: "numeric" }} locale="de-DE" />;

When you pass options (rather than a function), the formatter defaults to timeZone: "UTC", so the same input renders identically in any host timezone — a chart never shifts a day because the viewer is in a different region. Your options spread after that default, so { timeZone: "America/New_York" } overrides it deliberately. A (date) => string function bypasses the default entirely and owns its own timezone handling.