Formatting & scale
How format, locale, and domain work — one format prop feeds a chart's labels, its accessible summary, and its interactive readout, and domain sets the value range the geometry is drawn against.
Two props decide how a chart writes its numbers: format and locale. A third, domain, decides what value range the
geometry is drawn against. All three mean the same thing on every chart, and every chart's page lists them; this page is
where their defaults are written down.
One formatter, three places
A chart writes numbers in three places: 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 one
definition, so a currency chart's spoken summary says "$1,240", not "1240".
format takes either Intl.NumberFormatOptions or a plain function (n) => string. Formatters are cached,
keyed by locale plus options, so hundreds of instances sharing one format shape build one formatter between them.
Intl.NumberFormat construction is expensive, so never build your own inside a component.
The cache holds 64 formatters and empties completely when it fills. One app uses a handful of locale × option shapes, so a bounded cache that occasionally rebuilds costs less than one that grows forever.
You can reuse the same formatting outside a chart: a matching KPI label, a table cell. Import the exported
makeFormatter helper and your prose numbers come out of the same cache as the chart's:
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 is the fallback used when format
is omitted, which is how a chart supplies its own default (see Delta below).
Percent, compact, decimals
Anything Intl.NumberFormat supports works: percent, compact notation, fixed decimals, sign display. Delta defaults
to a locale-aware percent, so a raw ratio reads as a change with no config:
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 string to render. The value is snapped to 12 significant digits first, so a number
the chart derived internally (value - target) arrives as -3.6 rather than -3.5999999999999943.
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.
The chip is terse. It names the datum and its value: a Funnel stage reads Checkout 62% (1,240), a Slope line reads
Brazil: 44 → 28. It doesn't repeat the axis names, the row label already printed beside it, or the grid position the
cell already occupies. At these sizes a chip that carried all that would be wider than the chart it annotates.
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.
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 can't paint across the
page. If you see an ellipsis, shorten the value with format (notation: "compact" usually does it) 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. Every onActive / onSelect callback receives that same string as datum.formatted,
built with the chart's own format and locale. Suppress the chip and render it in your own layout: this is the
pattern behind a KPI card whose big number tracks the 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 keep working. Leave readout at its default (true) and you get the chip and the callbacks.
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, the decimal mark, currency placement.
locale sets number and date formatting. It does not translate the summary sentence ("Trending up 200%…");
that's the strings contract on Accessibility. A fully localized
chart needs both: locale for the figures, translated strings for the prose.
Server rendering: pass a locale explicitly
Omit locale and Intl resolves the runtime's default. 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 does not patch up the
mismatched text.
This isn't specific to microcharts; it's how Intl defaults work. 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:
// 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, so a 40–60% reading looks like 40–60% of a 0–100 scale rather
than a full-height line.
Auto-fit works differently per chart. 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 misstates 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 instead of collapsing to a degenerate scale.
The choice changes what a reader can conclude: auto-fit maximizes detail for a single series, and 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 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 instead:
Active 3 of 28 days over 4 weeks. A count of days is not a number format restyles.
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), the ones that render a spoken or hovered date. 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 and 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. A (date) => string function
bypasses the default entirely and handles its own timezone.
MCP server
A local stdio MCP server that lets an assistant find a chart type, read its exact props, and render it to SVG with the generated alt text attached.
Sizing & scaling
How to size a chart — the intrinsic default box, width and height as viewBox units, filling a container with one line of CSS, and sitting inline on a line of text.