s (hydration error). Expressions also survive
prettier's md/mdx prose-wrap, which re-expands a single line. */}
{[
{ name: "CPU", data: [62, 65, 61, 68, 70, 66, 72, 75], on: true },
{ name: "Memory", data: [48, 47, 49, 46, 45, 44, 43, 42], on: false },
{ name: "Network", data: [120, 118, 122, 119, 121, 120, 119, 118], on: false },
].map((m) => (
{m.name}
))}
```
## Small multiples: one shared scale [#small-multiples-one-shared-scale]
The most important composition decision, and the easiest to get silently wrong: when you place several sparklines in a
row to *compare* them, they must share one vertical scale. Auto-scaling each to its own range makes a flat line and a
spiking one look identical — a real correctness bug.
`SparkGroup` fixes it structurally. It reads every child's `data`, computes **one** domain across all of them, and
enforces **one** physical size — so a row of charts is honestly comparable. It's hook-free and RSC-safe, and any child's
own explicit `domain`/`width` still wins.
```tsx
` that sets `data-mc-theme` (the preset) plus any `tokens` as inline `--mc-*` custom
properties — identical to writing them by hand. `theme="modern"` writes no attribute, since modern *is* the base token
set. `theme` takes any preset (`modern`, `editorial`, `mono`, `vivid`, `dark`, plus the output-context `print` and
`eink`); `tokens` takes any `--mc-*` overrides. Reach for whichever reads better in your code — it's the same cascade
underneath.
## Build a theme from one colour [#build-a-theme-from-one-colour]
Setting a token or two by hand is the common case. For a whole brand theme — a matched categorical palette *and*
hand-tuned dark twins for every colour — reach for `defineTheme` from `@microcharts/react/theme`. Hand it your brand
accent and it derives the rest in OKLCH: a harmonized, colour-blind-safe categorical palette and a lifted dark-mode twin
for each token. It never nudges the positive/negative hues off their CVD-safe green/vermillion split, so a derived theme
stays as honest as the defaults.
```tsx
import { defineTheme } from "@microcharts/react/theme";
import { MicroProvider } from "@microcharts/react";
const brand = defineTheme({ accent: "#6d28d9" });
// spread the tokens onto one scope…
;
// …or emit a global stylesheet — includes a prefers-color-scheme: dark block
const css = brand.css(":root");
```
One accent, a full derived palette (these are the real values `defineTheme` returns):
```tsx
`"
>
```
The result carries everything you need for any delivery: `vars` (a `--mc-*` object), `darkVars` (the derived dark twins,
empty when `dark: false`), `style` (an alias of `vars`, ready to spread onto `MicroProvider` or any element),
`css(selector = ":root")` (a string with the `prefers-color-scheme: dark` block included), `toString()` (the same as
`css(":root")`), and `extend()` to spin a variant off an existing theme. Extend a preset, pin any token, or pass an
explicit palette when you don't want derivation:
```tsx
// extend a preset — the brand accent flows through
defineTheme({
extends: "editorial",
accent: "var(--brand-500)",
});
// bring your own categorical palette (no derivation)
defineTheme({
accent: "#6d28d9",
cat: ["#2563eb", "#db2777", "#65a30d"],
});
// derive a specific number of categorical tones instead of the default six
defineTheme({ accent: "#6d28d9", cat: 3 });
// opt out of accent-seeded derivation, or out of dark twins entirely
defineTheme({ accent: "#6d28d9", derive: false });
defineTheme({ accent: "#6d28d9", dark: false });
// override just some dark twins (the rest stay auto-derived)
defineTheme({ accent: "#6d28d9", dark: { accent: "#a78bfa" } });
// compose a compact variant off an existing theme
defineTheme({ density: 0.85 }).extend({ labelWeight: 500 });
// pin geometry directly — strokeWidth, gap, labelSize, labelWeight, density
defineTheme({ accent: "#6d28d9", strokeWidth: 2 });
```
It's pure, dependency-free, and tree-shaken out of any bundle that never imports it.
## Density [#density]
`--mc-density` is a single scalar that scales stroke weight, label size, and small-multiple gap together — one knob for
compact tables (`< 1`) or an airier standalone layout (`> 1`). It tunes the ink and type, not the box: the plot size
still comes from `width`/`height`, so nothing reflows unexpectedly.
```css
/* a denser table of sparklines */
.metrics-table {
--mc-density: 0.85;
}
```
One asymmetry worth knowing: **stroke and gap scale without limit, but the label stops scaling at 1.25.** Direct labels
sit in a gutter the chart reserves from a character-count estimate while it draws — server-side, where text cannot be
measured. That reservation is fixed at render time and cannot see a CSS variable set later, so past about 1.25 the text
outgrows the space held for it and paints outside the chart. Below `1` there is no such limit: the label shrinks inside
a gutter already reserved for something bigger, which is always safe.
So `--mc-density: 2` gives you double-weight strokes and double gaps, with labels at 1.25×. If you want genuinely larger
text, scale the chart — `width`/`height`, or `--mc-label-size` — rather than leaning on density.
## The font [#the-font]
`--mc-font` defaults to `inherit`, so charts adopt the font of whatever contains them and read as part of your
interface. That means the rule is really about your page, not the library: if the surrounding page sets a `font-family`,
charts follow it for free. If it doesn't — as in some minimal starters and code sandboxes — SVG text falls back to the
browser default (usually a serif), and labels come out looking wrong.
Two fixes, either works: give your page a font the normal way, or font the charts directly by pointing `--mc-font` at a
stack.
```css
:root {
/* charts only — the rest of the page is untouched */
--mc-font: system-ui, sans-serif;
}
```
Numbers always render with `tabular-nums` regardless, so columns of figures stay aligned in any font. Want the figures
in a dedicated face — a tabular numeric font, or a mono for a data-dense KPI — without changing the label font? Point
`--mc-font-numeric` at it; it defaults to `--mc-font`, so leaving it alone keeps everything on one typeface.
`--mc-label-weight` (default `400`) does the same for label weight.
```css
:root {
--mc-font: Inter, sans-serif; /* labels */
--mc-font-numeric: "IBM Plex Mono", monospace; /* figures */
}
```
## The readout surface [#the-readout-surface]
The only opaque plane in the whole library is the value chip an interactive chart shows for its active unit — under the
pointer, under keyboard focus, or pinned in place after a click, tap, or **Enter**. Its three `--mc-surface*` tokens
default to the system `Canvas`, so it adapts to light, dark, and forced-colors with no configuration. Point them at your
popover tokens when you want the chip to match a themed surface exactly — one set of tokens covers the hovered, focused,
and pinned states alike.
## Dark mode and forced colors [#dark-mode-and-forced-colors]
Dark values are hand-tuned per token — never auto-inverted, so overriding one light value can't quietly break the dark
surface. microcharts follows `prefers-color-scheme` out of the box, and you can force a scope with
`data-mc-theme="dark"`. Charts never paint their own background, so they always sit cleanly on your surface.
If your app toggles theme with a **class** (next-themes `.dark`, a `[data-theme]` attribute, …) rather than the OS media
query, rebind the tokens under that class the same way you bind accent — the library's media-query defaults are
`:where()` zero-specificity, so your class rules win:
```css
:root {
--mc-accent: var(--brand);
}
.dark {
--mc-stroke: #eae9e6;
--mc-positive: #45a385;
--mc-negative: #df7856;
--mc-accent: var(--brand);
/* …or data-mc-theme="dark" on a scope, if you prefer the shipped dark bundle */
}
```
### Frosted or translucent surfaces [#frosted-or-translucent-surfaces]
Default valence inks clear 4.5:1 on opaque white and near-black. On a frosted glass / tinted panel the composited
backdrop is quieter, so deepen `--mc-stroke` / `--mc-positive` / `--mc-negative` / `--mc-neutral` on that surface until
contrast holds — don't patch chart CSS. (This docs site does exactly that under `:root` / `.dark` so marks stay legible
on glass.)
Under Windows High Contrast (`forced-colors`), the semantic tokens defer to system colors — ink to `CanvasText`, accent
to `Highlight`, muted marks to `GrayText` — so a chart stays legible even when your palette is overridden entirely.
`prefers-contrast: more` is handled too: charts bump `--mc-stroke-width` to `2` and deepen `--mc-band` from an 8% to a
16% mix of the ink, so lines and normal-range shading both gain weight without you configuring anything.
# Troubleshooting (/docs/troubleshooting)
Most issues trace back to one of a handful of causes. This page is the fast path to each fix; the full explanation for
each one lives on its own guide page, linked below.
## Charts render with no color, no strokes, just plain shapes [#charts-render-with-no-color-no-strokes-just-plain-shapes]
The stylesheet isn't loaded. Every visual property — color, stroke weight, label size, the whole theme — comes from
`styles.css`, imported once:
```tsx
import "@microcharts/react/styles.css";
```
Without it, a chart is still valid, accessible SVG (the markup and the accessible name are both there), it just has no
styling applied — geometry with no ink. See [Quickstart](/docs/quickstart#compatibility) for where to put the import in
your framework.
## A chart is invisible (or nearly) on a colored or dark panel [#a-chart-is-invisible-or-nearly-on-a-colored-or-dark-panel]
Charts never paint their own background — they're transparent SVG, always. On a plain light or dark page this is
invisible because the built-in light/dark tokens track `prefers-color-scheme`. It breaks when a chart sits on a surface
whose color doesn't match the *page's* color scheme — a dark-branded card on an otherwise light page, or a colored KPI
tile — because the tokens don't know that scope looks dark; they only know what the OS/browser reports.
Fix it by rebinding the token scope to match the surface, not the page:
```css
.dark-panel {
--mc-stroke: #f5f5f5; /* text + data strokes; add --mc-neutral for muted labels */
}
```
or force the whole preset for that scope:
```tsx
```
See [Theming](/docs/theming) for the full token set and how scope, preset, and prop precedence works.
## `Error: ... 'use client' ...` when importing a chart [#error--use-client--when-importing-a-chart]
You imported a chart's `/interactive` entry into a Server Component:
```tsx
// ❌ interactive entries are client components — this throws in an RSC
import { Sparkline } from "@microcharts/react/sparkline/interactive";
```
Two ways out: render the static default entry instead if you don't actually need hover, keyboard, touch, or selection —
```tsx
import { Sparkline } from "@microcharts/react/sparkline";
```
— or, if you do need interactivity, move the import into a component marked `"use client"` and render that component
from your server tree. See [Quickstart](/docs/quickstart#add-interactivity) for the static-vs-interactive split.
## `onPointFocus` / `onRunFocus` no longer fire [#onpointfocus--onrunfocus-no-longer-fire]
They were removed when every interactive chart moved onto one shared contract. The replacement is `onActive`, which
fires for the same reason on every chart in the catalog rather than under a different name per chart:
```tsx
// ❌ removed
setHovered(i)} />
// ✅ one name, everywhere
setHovered(d?.index ?? null)} />
```
The payload is a `MicroDatum` — `{ index, value, label?, formatted? }` (`value` is the raw number, `formatted` the
chart's ready-to-display string) — or `null` when the chart clears. `onPointFocus` on Sparkline and SparkBar and
StreakSpark's `onRunFocus` map across directly. Note that `index` identifies the chart's *navigable unit*, which isn't
always a series index: on Waveform it's a bucket, on Hypnogram a run, on SegmentedBar a segment. Each chart's page names
its own unit. If you also want click-to-pin, add `onSelect` — see
[Accessibility](/docs/accessibility#one-interaction-contract).
## `animate` does nothing [#animate-does-nothing]
The entrance engine isn't loaded. `animate` is a flag; the code that runs the animation ships in a separate side-effect
import you add once, anywhere in your client bundle:
```tsx
import "@microcharts/react/motion";
```
In development the chart logs exactly this to the console when the engine is missing. Three other reasons `animate` is
correctly inert, none of them bugs: the reader has `prefers-reduced-motion: reduce`, the chart is a static entry
(`animate` is an `/interactive`-only prop), or the markup came from the server and is being hydrated rather than freshly
mounted — an entrance would mean animating content the reader can already see. The entrance also runs once per mount by
design; remount with a changed `key` to replay it.
## The chart won't stretch to fill its container [#the-chart-wont-stretch-to-fill-its-container]
`.mc-root` — the chart's own `` — is `display: inline-block` by default, so it flows next to text instead of
stretching. Give it fluid sizing explicitly:
```tsx
```
and make sure the *parent* is block-level (a flex or grid item, or a `div`) — a chart can't grow past an inline parent
no matter what you set on the chart itself. This is identical for `…/interactive` entries: `style`, `className`, and
`width`/`height` size an interactive chart exactly the way they size its static twin, and hit-testing stays exact at any
size — the crosshair tracks the cursor and a touch drag scrubs the unit under the finger. Full recipes, including the
inline-in-a-sentence case, are on [Sizing & scaling](/docs/sizing).
## A chart stacks on its own line instead of sitting inline with text [#a-chart-stacks-on-its-own-line-instead-of-sitting-inline-with-text]
This is usually a CSS reset, not a microcharts bug. `.mc-root`'s `display: inline-block` is declared inside a
zero-specificity `:where()` so your styles always win — including ones you didn't mean to apply. Tailwind's Preflight
sets a plain `svg { display: block; ... }` rule, which has *higher* specificity than any `:where()` selector and so
overrides it, dropping the chart onto its own block-level line. Re-assert the layout you want at the call site:
```tsx
```
or wrap it in `className="mc-inline"` for text-flow placement — see
[Sizing & scaling](/docs/sizing#inline-in-a-sentence).
## Chart labels look wrong — a serif fallback, or mismatched with the page [#chart-labels-look-wrong--a-serif-fallback-or-mismatched-with-the-page]
Charts inherit `font-family` from the page; they don't set one of their own. If the surrounding page never declares a
font (a bare sandbox, a minimal starter), SVG text falls back to the browser's default serif. See
[Theming → The font](/docs/theming#the-font) for why it inherits and how to set it per-scope.
# microcharts vs Chart.js for inline charts (/docs/vs-chartjs)
[Chart.js](https://www.chartjs.org) is a Canvas-first chart library with a large plugin ecosystem, usually wrapped by
[`react-chartjs-2`](https://react-chartjs-2.js.org) in React apps. microcharts is not a replacement for it — a dense
analyst dashboard or a report full of chart panels is Chart.js territory. This page compares the two for one narrow job:
a chart the size of a word, inside a sentence, a table cell, or a KPI card.
The broader positioning lives in [When to use microcharts](/docs/when-to-use) and
[Full chart libraries](/docs/full-chart-libraries).
## The numbers [#the-numbers]
microcharts numbers come from `.size-limit.json` — the CI gate, re-measured on every build. The Chart.js figures are
pinned with version and date; they are orientation, not a scoreboard.
## Where the difference actually shows [#where-the-difference-actually-shows]
**Renderer.** Chart.js draws to a ``, and a canvas only exists in the browser — there is no static-HTML output
path. The chart appears after JavaScript runs. microcharts renders SVG markup, so the mark is real DOM: it exists in the
server-rendered HTML, prints correctly, and scales crisply at any zoom.
**Server Components.** `react-chartjs-2` manages the canvas with hooks and effects, so it runs in a client boundary and
ships Chart.js to the browser. The microcharts default export is hook-free static SVG — an RSC can render it with zero
client JavaScript.
**Accessibility.** A canvas is a bitmap to assistive technology; the Chart.js docs recommend supplying your own fallback
content or ARIA labeling per chart, and plugins exist to help. That path works — it is just manual. Every microchart
defaults to `role="img"` plus a natural-language summary generated from the data.
**Text and theming.** Inline marks sit next to your text, so they must inherit its rhythm. SVG takes CSS custom
properties, `currentColor`, and container-relative sizing directly; a canvas repaints through JavaScript when the theme
flips.
## Reach for Chart.js [#reach-for-chartjs]
* Dashboard or report canvases with many series and heavy point counts
* You already run Chart.js configs or depend on its plugin ecosystem
* Canvas rendering is a requirement, not an implementation detail
## Reach for microcharts [#reach-for-microcharts]
* The mark lives inline — prose, cells, tabs, KPI cards, streamed AI replies
* You want the chart present in server-rendered HTML, with zero client JS by default
* You want an accessible name without per-chart wiring
* Per-mark budget: gzip, zero runtime dependencies
## What this page is not saying [#what-this-page-is-not-saying]
* Not "Canvas is wrong" — above a few thousand points it is usually right
* Not "Chart.js cannot be made accessible" — it can; it is manual
* The gzip figures are dated pins, not live measurements
## Next [#next]
* [Full chart libraries](/docs/full-chart-libraries) — Recharts and Chart.js in one view
* [microcharts vs Recharts](/docs/vs-recharts)
* [When to use microcharts](/docs/when-to-use)
* [Accessibility](/docs/accessibility) — what the defaults actually do
# microcharts vs MUI X Sparkline — bundle size and RSC comparison (/docs/vs-mui-x-sparkline)
[MUI X Charts](https://mui.com/x/react-charts/) ships `SparkLineChart` — an inline-sized chart from the same family as
its full Line, Bar, and Pie charts, themed by and integrated with Material UI. If your product is built on MUI and the
sparkline sits in a MUI dashboard, that integration is a real advantage and this page may talk you out of nothing.
The comparison matters when the mark must be cheap, static, or independent of MUI: a table column of trends, a KPI row
in an RSC, a chart in a streamed AI reply.
## The numbers [#the-numbers]
The two MUI scenarios are deliberate: inside a MUI app the Material core is already in your bundle, so the sparkline's
marginal cost is the smaller figure. Without MUI, `@mui/material` and `@mui/system` are required peer dependencies — you
install and ship them to render one sparkline.
## Where the difference actually shows [#where-the-difference-actually-shows]
**Bundle cost.** `SparkLineChart` shares MUI X's charting core (data providers, interaction plugins, vendored d3). That
architecture is what makes the full chart family consistent — and it is why one sparkline carries tens of kilobytes.
microcharts charges per chart subpath: for Sparkline, catalog band
gzip.
**Server Components.** `SparkLineChart` is marked `'use client'` — the server can emit initial HTML, but the component
always hydrates and ships its JavaScript. The microcharts default export is hook-free static SVG with zero client JS;
interactivity is a separate `/interactive` import.
**Accessibility defaults.** As published, the `SparkLineChart` SVG renders with `aria-hidden="true"` and no accessible
name — the chart is invisible to assistive technology unless you label it yourself. Every microchart defaults to
`role="img"` with a natural-language summary generated from the data.
**Theming.** MUI X charts theme through the MUI theme object — coherent if you live there. microcharts themes through
`--mc-*` CSS custom properties at near-zero specificity, so it follows any design system, including a MUI one.
## Reach for MUI X Sparkline [#reach-for-mui-x-sparkline]
* Your app is already on MUI and the chart sits among MUI components
* You want one vendor for the sparkline and the full charts beside it
* You use MUI X's tooltip, highlighting, and animation stack elsewhere
## Reach for microcharts [#reach-for-microcharts]
* The mark must not pull MUI (or anything) into the bundle — zero runtime dependencies
* You render from RSCs and want zero client JavaScript for static marks
* Accessibility should be the default, not a labeling task per chart
* You need chart types beyond sparklines at word size — types, one grammar
## What this page is not saying [#what-this-page-is-not-saying]
* Not "MUI X is bloated" — the shared core is the price of a consistent full-chart family
* Not "never use SparkLineChart" — inside a MUI dashboard it is the coherent choice
* MUI X Charts (community) is MIT and actively maintained; the sizes are dated pins from the published package
## Next [#next]
* [When to use microcharts](/docs/when-to-use)
* [microcharts vs Recharts](/docs/vs-recharts)
* [React sparklines](/docs/react-sparklines)
* [Theming](/docs/theming) — fitting charts into an existing design system
# microcharts vs react-sparklines — React sparkline comparison (/docs/vs-react-sparklines)
[`react-sparklines`](https://www.npmjs.com/package/react-sparklines) put sparklines into thousands of React apps and
still gets massive weekly downloads. It is the package many "react sparkline" searches land on, so if you are choosing
between it and microcharts, this page is the factual side-by-side. It is not a takedown — the package earned its
adoption.
## The numbers [#the-numbers]
## Where the difference actually shows [#where-the-difference-actually-shows]
**Maintenance.** `react-sparklines` last published in 2017 (`1.7.0`). It predates hooks and Server Components — nothing
from that era can be expected to target them. microcharts is actively maintained against React 18 and 19, with
StrictMode-safe rendering tested in CI.
**Accessibility.** `react-sparklines` renders a bare `` — no `role`, no ``, no accessible name; a screen
reader finds nothing. Every microchart is `role="img"` with a natural-language summary generated from the data
(`describeSeries`), and interactive entries add keyboard navigation with a polite live region.
**Server Components.** `react-sparklines` components are class components, which need a client boundary in an App Router
app. The microcharts default export is hook-free static SVG that renders directly in an RSC with zero client JavaScript.
**Scope.** microcharts is a word-sized catalog — chart types, one grammar: `data` alone renders
something correct, and the same prop names mean the same thing on every chart. Sparkline is one entry in it.
`react-sparklines` is one visual idea with composable overlays.
**Size.** Both are small. `react-sparklines` is a single \~7.9 kB gzip package; microcharts charges per chart subpath
( for Sparkline) with a CI gate that keeps every chart inside its budget.
## When react-sparklines is still fine [#when-react-sparklines-is-still-fine]
* A legacy codebase already uses it, the charts are decorative, and nothing is broken
* You need its exact composable-overlay API and have your own accessibility layer
No urgency to migrate in those cases. The gaps above matter when accessibility, RSC, or maintenance actually bite.
## Migrating [#migrating]
The shapes map directly:
```tsx
// react-sparklines
import { Sparklines, SparklinesLine } from "react-sparklines";
;
// microcharts
import { Sparkline } from "@microcharts/react/sparkline";
;
```
`title` gives the chart its accessible subject; the summary is generated from the data. Bars map to
[SparkBar](/docs/charts/sparkbar), reference lines to [``](/docs/annotations).
## Next [#next]
* [React sparklines](/docs/react-sparklines) — the mark itself, in depth
* [Sparkline API](/docs/charts/sparkline)
* [When to use microcharts](/docs/when-to-use)
* [Accessibility](/docs/accessibility)
# microcharts vs Recharts — inline charts comparison (/docs/vs-recharts)
[Recharts](https://recharts.org) is a full chart library and microcharts is not a replacement for it. If your page *is*
the chart — axes, legend, tooltip, brush — stop reading and use Recharts. This page compares the two for exactly one
job: a word-sized chart *inside* an interface, where the surface is mostly words and UI.
The broader positioning lives in [When to use microcharts](/docs/when-to-use) and
[Full chart libraries](/docs/full-chart-libraries). This page is the measured detail for the Recharts decision.
## The numbers [#the-numbers]
Both microcharts columns come from `.size-limit.json`, the CI gate that fails the build when a chart grows past its
budget. The Recharts numbers are pinned with version, method, and date — they are orientation, not a scoreboard, and
they will age.
## Where the difference actually shows [#where-the-difference-actually-shows]
**Bundle cost per mark.** A tree-shaken Recharts `LineChart` carries the shared Recharts core. That cost is fine
amortized over a dashboard; it is hard to justify for one sparkline in a table header. microcharts imports one chart per
subpath, so a page that uses only `Sparkline` pays only for `Sparkline`.
**Server Components.** Recharts components are hook-based, so in an App Router app they run inside a client boundary and
ship their JavaScript. The microcharts default export is hook-free static SVG — it renders in an RSC with zero client
JavaScript, and interactivity is a separate opt-in `/interactive` import.
**Accessibility defaults.** Recharts ships an accessibility layer, on by default in v3 — keyboard navigation and ARIA
roles on the chart surface. A natural-language description of the data is still yours to write. Every microchart is
`role="img"` with that description generated from the data by default (`describeSeries`); the decorative opt-out is
explicit (`summary={false}`).
**Chart chrome.** Recharts draws axes, ticks, legends, and tooltips because dashboard surfaces need them. microcharts
hard-codes them away because word-sized marks cannot afford them. Neither choice is a defect; they serve different
surfaces.
## Reach for Recharts [#reach-for-recharts]
* The page is mostly chart: analytics views, report panels, explorable dashboards
* You need ticks, legends, brush, zoom, or a tooltip that follows the pointer
* Your team already has a Recharts component tree it trusts
## Reach for microcharts [#reach-for-microcharts]
* The mark sits in a sentence, a table cell, a tab, a KPI card, or a streamed AI reply
* You want static chart SVG out of a React Server Component with nothing to hydrate
* You want an accessible name generated from the data without wiring it per chart
* Budget matters per mark: gzip, zero runtime dependencies
## Same app, both libraries [#same-app-both-libraries]
This is the common ending, not a compromise:
```tsx
// analytics page — Recharts
…
;
// the table that links to it — microcharts
import { Sparkline } from "@microcharts/react/sparkline";
;
```
## What this page is not saying [#what-this-page-is-not-saying]
* Not "Recharts is heavy" — it is correctly sized for the job it does
* Not "microcharts can replace your dashboards" — it cannot and does not try
* The gzip figures are dated pins, not live measurements
## Next [#next]
* [Full chart libraries](/docs/full-chart-libraries) — Recharts and Chart.js in one view
* [When to use microcharts](/docs/when-to-use) — the product decision
* [microcharts vs Chart.js](/docs/vs-chartjs)
* [Performance](/docs/performance) — our own CI receipts
# microcharts vs visx — tiny charts comparison (/docs/vs-visx)
[visx](https://airbnb.io/visx/) is Airbnb's collection of low-level visualization primitives: shapes, scales, axes, and
gradients you compose into your own charts. It is deliberately not a chart library — you are the chart library.
microcharts is the opposite bet: finished, opinionated word-sized marks with the design decisions already made.
That makes this less a competition than a fork in the road. Both keep bundles lean through per-package (visx) or
per-chart (microcharts) imports; the real question is who does the chart design — you or the library.
## The numbers [#the-numbers]
The visx figure is a floor, not a typical cost: `LinePath` plus one scale, before curves, tooltips, or axes. Each
`@visx/*` package you add brings its own dependencies (d3 arrives vendored through `@visx/vendor`).
## Where the difference actually shows [#where-the-difference-actually-shows]
**Who owns the design.** With visx you decide geometry, padding, label placement, empty-data behavior, negative-value
handling, and color semantics per chart — full control, and a real design task at 16 pixels tall. microcharts hard-codes
those decisions: areas anchor at zero, direction is never color-alone, labels reserve deterministic gutters, empty and
all-null data render documented placeholders.
**Who owns accessibility.** A visx `LinePath` renders a bare `` — `role`, ``, and a description of the data
are yours to author on every chart you build. Every microchart defaults to `role="img"` with a natural-language summary
generated from the data.
**Dependency posture.** visx keeps React in control of the DOM and slices d3 into per-package imports — genuinely
lighter than shipping all of d3. microcharts goes further for its narrower job: scales, paths, easing, color, and
summaries are in-house, so `dependencies: {}` is a CI-enforced invariant.
**Edge cases.** NaN, ±Infinity, single points, all-equal series — with primitives, each is your code path. Every
microchart documents and tests these in a shared fixture suite.
## Reach for visx [#reach-for-visx]
* The visualization is bespoke — no catalog anywhere has your chart
* You have design capacity and want d3's power with React's rendering
* You are building your organization's own charting layer, at any surface size
## Reach for microcharts [#reach-for-microcharts]
* The chart you need already exists in the catalog — word-sized types, one grammar
* You want accessibility, edge cases, and design handled by default
* You want static RSC output with zero client JavaScript, at gzip per chart
## What this page is not saying [#what-this-page-is-not-saying]
* Not "visx is heavy" — 16 kB for a floor is honest for hand-built charts, and visx's modularity is well executed
* Not "never build your own" — a bespoke visualization deserves primitives
* The visx figure is a dated pin for one minimal composition, not a benchmark of visx overall
## Next [#next]
* [When to use microcharts](/docs/when-to-use)
* [Design notes](/docs/design-notes) — the decisions microcharts makes for you
* [microcharts vs Recharts](/docs/vs-recharts)
* [Composition](/docs/composition) — placing marks in real surfaces
# When to use microcharts (/docs/when-to-use)
microcharts is a set of tiny, handcrafted charts for React — built to sit *inside* an interface, where a full chart
library would be too heavy and too loud. A trend after a number, a bullet in a KPI, a strip in a table cell, a mark in a
streamed reply. One quiet signal, read at a glance.
It is not a substitute for [Recharts](https://recharts.org), [Chart.js](https://www.chartjs.org), Nivo, ECharts, or
visx. Those libraries draw surfaces that are mostly chart: axes, legends, tooltips, brush, zoom. microcharts draws the
opposite: mostly words and UI, with a word-sized mark.
Use a full library when the page *is* the chart. Use microcharts when the chart sits inside something you were already
reading — or use both in the same product.
## Fit [#fit]
| Situation | Fit |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| Sparkline, bullet, or delta in prose, a cell, a tab, or a card | **microcharts** |
| Static chart from a React Server Component with **zero** client chart JS | **microcharts** (default export) |
| A model or agent emitting a chart mid-reply | **microcharts** — see [AI-native](/docs/ai) |
| Full Line / Bar / Area with ticks, legend, tooltip, brush | **Recharts, Chart.js, …** |
| Dense Canvas series, Chart.js plugins, analyst dashboards | **Chart.js** (or similar) |
| One library for every chart in the product | Usually a **full** library; add microcharts for the inline marks |
## What this library refuses [#what-this-library-refuses]
Pie, needle gauges, waffles, and similar shapes fail at word size — they are excluded on purpose. Each has a
strictly-better in-catalog replacement (for example [SegmentedBar](/docs/charts/segmented-bar),
[Bullet](/docs/charts/bullet), [IconArray](/docs/charts/icon-array)). See the [introduction](/docs).
## Size [#size]
Every chart is imported from its own subpath and budget-gated in CI: gzip, **zero runtime
dependencies**, static SVG from an RSC with nothing to hydrate. Accessible by default — `role="img"` with a summary
generated from the data.
Pinned orientation numbers for Recharts and Chart.js (version, date, method):
[Full chart libraries](/docs/full-chart-libraries). Our own receipts: [Performance](/docs/performance).
## Same app, two jobs [#same-app-two-jobs]
A product can use Recharts on an analytics page and a Sparkline in the table that links to it. Different import,
different budget.
```tsx
// dashboard panel — your full chart library
… ;
// same product, table cell — microcharts
import { Sparkline } from "@microcharts/react/sparkline";
;
```
## Go deeper [#go-deeper]
* [React sparklines](/docs/react-sparklines) — the most common word-sized mark
* [Inline charts](/docs/inline-charts) — sentences, cells, KPIs, tabs, RSC
* [Full chart libraries](/docs/full-chart-libraries) — Recharts and Chart.js, side by side
* Comparing a specific library? [vs Recharts](/docs/vs-recharts) · [vs Chart.js](/docs/vs-chartjs) ·
[vs react-sparklines](/docs/vs-react-sparklines) · [vs MUI X Sparkline](/docs/vs-mui-x-sparkline) ·
[vs visx](/docs/vs-visx)
* [Quickstart](/docs/quickstart) — install and first chart
* [Introduction](/docs) — grammar, catalog, and where they live
# ABStrips (/docs/charts/ab-strips)
ABStrips answers "did B beat A — and by more than the overlap?". It draws **two graded quantile strips on one shared
scale**: a faint p5–95 band, a stronger p25–75 middle half, and a median dot, with row A muted and row B accent. The
visible overlap of the two middle halves is the answer — and the overlap number is always in the summary, because an
average delta without its spread is how A/B results lie.
```tsx
```
## Install [#install]
```tsx
import { ABStrips } from "@microcharts/react/ab-strips";
const control = Array.from({ length: 80 }, (_, i) => 130 + ((i * 13) % 44) - 22);
const test = Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22);
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — an A/B experiment result in a KPI card, control vs test in an experiments table, any two-sample
comparison where the spread matters.
* **Avoid for** — a single distribution (BenchmarkStrip) or more than two arms (small multiples).
## Variants [#variants]
```tsx
130 + ((i * 13) % 44) - 22);
const test = Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22);
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
Either arm with zero finite values makes the whole comparison unanswerable — no strips draw, and the accessible name
reads plainly **"No data."** rather than a comparison against nothing. An arm with fewer than 8 points still draws (a
small sample is still the honest sample you have); only its outer band changes meaning, from the 5th–95th percentile to
the plain min–max, because percentile estimates need more than a handful of points to mean anything.
## Why this default [#why-this-default]
Overlap in the summary, because an average delta without spread is how A/B results lie. The strips are mandatory context
— it never degrades to a bare mean bar — and the delta label never appears without the distributions behind it. When the
middle halves fully overlap the summary says "no clear difference"; when they're disjoint it says "clearly separated";
and an arm with fewer than 8 points falls back to a min–max band.
## Accessibility [#accessibility]
The accessible name states both medians, the delta, and the overlap — **"Test median 115 ms vs Ctrl 129 ms (-11%);
middle halves overlap 35%."**. The interactive entry roves the rows (↑/↓) and the quantile edges (←/→): the median
announces the delta vs the other arm, other edges announce the percentile.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ a: number[]; b: number[] }` | The two arms — raw samples, not summaries. |
| `seriesLabels` | `[string, string]` | Row identities for the gutter tags + summary (default ['A', 'B']). |
| `positive` | `"up" \| "down"` | Which direction of the B−A delta reads as good (colors the delta). |
| `label` | `"delta" \| "none"` | Signed median delta in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ActivityGrid (/docs/charts/activity-grid)
ActivityGrid maps ordered values onto a grid of cells, each shaded by intensity. It's the contribution-graph shape:
cadence, streaks, and seasonality, readable at a glance.
```tsx
`"
>
```
## Install [#install]
```tsx
import { ActivityGrid } from "@microcharts/react/activity-grid";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — daily activity, streaks and cadence, seasonality at a glance.
* **Avoid for** — exact values, or precise comparison between two individual cells. Color intensity is deliberately
approximate; pair it with a number when precision matters.
## Sizing [#sizing]
ActivityGrid sizes from `cell` — the edge length of one square in viewBox units. Bump it to scale the whole grid, or
drive the width from CSS to fill a container; the viewBox keeps the grid's aspect ratio.
## Variants [#variants]
`layout="strip"` collapses to a single row for inline use.
```tsx
`"
>
```
`shape` swaps the cell mark — same data, same levels, different voice: `"round"` for friendlier product surfaces,
`"dot"` adds breathing room in dense strips.
```tsx
`"
>
```
`anchor` aligns the grid to the real calendar: the first column pads down to the weekday of the given day (UTC), so rows
read as weekdays — Monday first by default (`weekStart={0}` for Sunday).
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Five levels (the empty track plus four intensity steps) matches the five steps HeatCell and HeatStrip default to, and
the four intensity steps share their exact opacity ramp — one calibration means "how intense" reads the same across the
library, and a reader never has to relearn a color scale per chart. Only level 0 differs: here it is a faint empty
track, because an activity slot with no activity is genuinely empty, not a bottom-of-scale value. Levels are discrete,
never a continuous gradient: a continuous ramp implies precision a handful of pixels can't actually deliver, and
discrete bins are honest about that. Monday-first weeks (`weekStart={1}`) match the ISO calendar most teams already
track cadence against; `weekStart={0}` switches to Sunday-first without touching the data. Column-major fill (each
column is a week, top-to-bottom) is what makes the grid read as a calendar instead of an arbitrary sequence of cells.
## Accessibility [#accessibility]
Intensity is a color channel, so ActivityGrid always pairs it with a numeric summary of its total and peak — the
information survives forced-colors and color-blind viewing. The interactive entry adds 2-D arrow-key navigation,
announcing each cell's value as you move.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Ordered values, one per cell. |
| `layout` | `"grid" \| "strip"` | 7-row calendar or single strip. |
| `shape` | `"square" \| "round" \| "dot"` | Cell mark: crisp square, soft corners, or padded dot. |
| `anchor` | `string \| Date` | First slot's calendar day (UTC) — pads the first column so weekday rows align. |
| `weekStart` | `0 \| 1` | Start of week for anchor alignment (0 Sunday, 1 Monday). |
| `cell` | `number` | Cell edge length in viewBox units. |
| `domain` | `[number, number]` | Explicit range for level bucketing. |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BalanceBeam (/docs/charts/balance-beam)
BalanceBeam answers "which of two sides outweighs, and roughly by how much?" A beam tilts toward the heavier side; the
direction is instant and the angle grows with the imbalance, but it **saturates** — read it as direction plus rough
magnitude, not an exact ratio. The two weights are area-true (their area, not their width, is proportional to value).
For a precise comparison, reach for `PairedBars` or `Delta`; this is for the glance.
```tsx
```
## Install [#install]
```tsx
import { BalanceBeam } from "@microcharts/react/balance-beam";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a buy vs sell or in vs out read in a sentence, a pro vs con weight in a KPI card, or an A-vs-B pair
where direction is the story.
* **Avoid for** — exact ratios (PairedBars / Delta), more than two items (MiniBar), or trends.
## Variants [#variants]
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, both the announced values and the in-chart numerals
(`label="values"`) follow that locale's own grouping and decimal marks.
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
Equal values level the beam and report **"balanced"** rather than picking an arbitrary heavier side. Two zeros are the
same case — the beam stays level and both weights shrink to nothing, which is honest: there is nothing to weigh.
## Why this default [#why-this-default]
Ratio mode is the default because most two-sided comparisons are share-of-whole questions, and square weights are the
default because circles under-read area at this size. The tilt saturates at `maxTilt` degrees — a deliberate honesty
choice: past a point, a steeper beam would imply a precision the eye can't extract, so the angle levels off. Direction
and rough magnitude always agree, and the two channels (tilt and weight area) never disagree. For same-scale rows that
should tilt comparably, `mode="difference"` scales the imbalance by a shared `domain`.
## Accessibility [#accessibility]
The accessible name states both sides and the winner — **"Inflow 620 vs Outflow 480; Inflow heavier."** — or "…;
balanced." when they match. The interactive entry eases the beam to its new tilt, reveals a side's value on hover or
←/→, and announces when the heavier side flips.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `[{label,value},{label,value}]` | Exactly two items. |
| `maxTilt` | `number` | Degrees at full saturation (default 12). |
| `shape` | `"square" \| "round"` | Weight shape (default square). |
| `mode` | `"ratio" \| "difference"` | ratio = share-of-whole; difference = absolute, scaled by domain. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BenchmarkStrip (/docs/charts/benchmark-strip)
BenchmarkStrip answers "is this value normal for its peer group?". A focal dot sits on a common scale against the peers'
own **empirical** quantile bands — never a fitted distribution. There is no axis: the band is the reference frame. Small
samples fall back to min–max so tail quantiles are never fiction.
```tsx
`"
>
```
## Install [#install]
```tsx
import { BenchmarkStrip } from "@microcharts/react/benchmark-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a value against its cohort, per-row peer comparison in tables, SLA context.
* **Avoid for** — a single trend (Sparkline) or two groups head-to-head (ABStrips).
## Variants [#variants]
```tsx
// 42 peer latencies (ms) const peerLatencies = [ 180, 201, 237, 286, 341, 396, 443, 394, 412, 413, 398, 372, 340, 310,
205, 196, 205, 230, 271, 322, 378, 347, 391, 421, 434, 430, 412, 383, 268, 239, 221, 218, 231, 261, 306, 275, 331, 382,
422, 447, 455, 447, ];
`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, both the right-gutter label and the accessible
summary's numbers follow that locale's own grouping and decimal marks: the value above reads "1.240" in German, not
"1,240".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Bands come from the peers' own empirical quantiles, never a fitted distribution — the strip never implies a normal curve
that the data doesn't back up. There is no axis: the band itself is the reference frame, so the focal dot's position
against it is the whole read. Below 8 peers, tail quantiles (p5/p95) are statistically unreliable, so the outer band
falls back to the observed min–max automatically — a deliberate honesty trade the `range` prop can also force at any
sample size. A value that falls outside the plotted domain clamps to the edge and draws a small wedge rather than
silently disappearing.
## Accessibility [#accessibility]
The accessible name states the value, its percentile, the peer count, and the middle-half interval — **"312 — 43rd
percentile of 42 peers (middle half 244.5–408.5)."** A flat cohort states it plainly instead: "52 — all 8 peers at 50."
The interactive entry roves the five quantile edges with ←/→, announcing each edge's name and value (e.g. "p75:
408.5.").
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Peer values. |
| `value` (required) | `number` | The focal reading. |
| `range` | `"p5p95" \| "minmax"` | Outer band; minmax for small samples. |
| `label` | `"value" \| "percentile" \| "none"` | What the right gutter states (default percentile). |
| `positive` | `"up" \| "down"` | Which side of the band is good (colors the focal dot). |
| `median` | `boolean` | Center tick (default true). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BiasStrip (/docs/charts/bias-strip)
BiasStrip answers "do two ways of measuring the same thing systematically disagree?" — the agreement story a scatter
can't tell. Each dot is one pair placed at its mean and its difference; the dashed line is perfect agreement, the accent
line is the measured bias, and the faint band is the limits of agreement. Dots render at 75% opacity so overplot reads
as density, and pairs beyond the limits are enlarged and re-inked so outliers read on shape, not color alone.
```tsx
{"Device and reference "}
{" agree, +2 bias."}
```
## Install [#install]
```tsx
import { BiasStrip } from "@microcharts/react/bias-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — checking whether a new instrument, method, or model agrees with a reference; instrument drift in a KPI
card.
* **Avoid for** — unpaired samples (use MicroScatter) or a single time series (Sparkline).
A correlation plot answers "do these move together?" — but two methods can correlate perfectly and still disagree by a
constant offset. BiasStrip is built for that offset: it plots the difference, so a bias jumps off the zero line where a
scatter would hide it on the diagonal.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
Empty data draws just the frame with "No data." as the summary. Under 5 pairs the limits aren't meaningful, so the band
and bias line drop out and only the dots and the zero reference remain — the mean difference is still stated in the
summary. When the two methods agree perfectly the band collapses to a hair around the zero line rather than vanishing,
so "nothing to see" still reads as a deliberate, measured result. Pairs with a non-finite value are dropped before the
stats and the count; past 40 pairs the dots are uniformly downsampled for display while the bias and limits still use
every pair.
## Why this default [#why-this-default]
The y-axis is symmetric about zero so perfect agreement is always the visual center: a systematic offset then reads as
the whole cloud drifting up or down, which is exactly the decision the chart exists to surface. The band is the classic
±1.96σ limits of agreement, and `limits` widens or tightens it (2.58 for \~99%) without ever redefining what the marks
mean. Under five pairs the chart makes no interval claim at all — too little evidence to draw one honestly.
## Accessibility [#accessibility]
The accessible name is the bias plus the evidence-backed agreement share — **"Bias +2.21 across 20 pairs; 90% within the
limits of agreement."** — or the shorter **"Bias +2 across 4 pairs."** when there are too few pairs to draw limits.
Localizing carries the number format through: with `locale="de-DE"` the same chart reads **"Bias +2,21 across 20 pairs;
90% within the limits of agreement."** The interactive entry steps pairs ordered by mean and announces each pair's mean,
difference, and whether it clears the limits.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ a; b }[]` | Paired measurements. |
| `limits` | `number` | k in bias ± k·σ (default 1.96 ≈ 95% limits of agreement). |
| `label` | `"bias" \| "none"` | Seat-gated bias caption (default) or hidden. |
| `r` | `number` | Base dot radius, clamped [1, 3]. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BreathingDot (/docs/charts/breathing-dot)
BreathingDot answers "how loaded is the system right now?" as an ambient signal. In the interactive entry the dot
**pulses** — slow and small when calm, fast and large when strained — so you feel the state without reading a number.
The static frame is a real chart with zero JavaScript: a core dot colored by band, and a ring whose distance from the
core is the level. This is a low-precision ambient read; for an exact figure reach for `Progress` or `Sparkline`.
```tsx
```
## Install [#install]
```tsx
import { BreathingDot } from "@microcharts/react/breathing-dot";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## Motion, and reduced motion [#motion-and-reduced-motion]
The pulse **is** the encoding — its rate and amplitude are snapped to three named bands (calm, elevated, strained), so
the motion is readable, not decorative. This is the deliberate exception to the no-looping-animation rule, allowed only
because the loop parameter (the rate) is the datum. The animation is gated on two things: it pauses when the dot scrolls
off-screen (a shared observer), and it never runs at all under `prefers-reduced-motion` — reduced-motion readers get
exactly the static frame, where the ring's distance from the core already carries the level. The band is announced
through a polite live region only when it changes, never on every pulse.
## When to use it [#when-to-use-it]
* **Good for** — an ambient "how strained is it right now" read, a live status dot in a header or KPI card, or per-node
load in a dense table.
* **Avoid for** — an exact load figure (`Progress`, `Sparkline`), discrete events (`HeartbeatBlip`), or a trend over
time (`Sparkline`).
## Sizing [#sizing]
One `size` prop, in viewBox units, sets the glyph box — it defaults to `16`, about the height of a line of body text.
There is no separate `width`/`height`: the dot is square, and the rendered box widens only by the gutter a `label`
reserves. `fontSize` follows `size` unless you set it.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
`format`/`locale` reach the `label="value"` percent numeral — always a whole percent, so a locale mostly changes digit
shapes here, not grouping or decimals. The band words in the accessible summary ("calm" / "elevated" / "strained") stay
in `strings` (`BreathingDotStrings`), not the number formatter.
## Why this default [#why-this-default]
Three bands, because ambient motion supports about three discriminable states — more would be false precision. The band
color is always doubled: by the ring offset in the static frame and by the pulse rate in motion, so the state never
rests on color alone. When the value is unknown (`null` or `NaN`) the dot goes gray, drops its ring, and **never
pulses** — an unknown system must not look calm. The boundary with `HeartbeatBlip`: a continuous level is a
BreathingDot; discrete arriving events are a HeartbeatBlip.
## Accessibility [#accessibility]
The accessible name is the level and its band word — **"Load 20% — calm."** — or **"Load unknown."** when the value is
missing. The band color is never the only signal (ring offset and pulse rate double it), motion is fully gated on
`prefers-reduced-motion`, and the live region announces band changes only.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Level 0–1 (clamped). null / NaN → unknown. |
| `thresholds` | `[number, number]` | calm / elevated / strained edges (default [0.5, 0.8]). |
| `label` | `"value" \| "none"` | Percent numeral beside the dot. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BubbleRow (/docs/charts/bubble-row)
BubbleRow answers "roughly how do these few magnitudes compare?" with physical presence — a row of circles whose
**area** (not width) is proportional to value. Area is the weakest common channel, so this is the catalog's honest
low-precision exemplar: **for a precise comparison, use `MiniBar`.** The value numerals are on by default, because a
low-precision channel owes the reader the number.
```tsx
```
## Install [#install]
```tsx
import { BubbleRow } from "@microcharts/react/bubble-row";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a few magnitudes with physical presence in a sentence, a market-size or segment impression in a KPI
card, or an editorial callout where the number is printed too.
* **Avoid for** — precise comparison (that's `MiniBar`), trends (Sparkline), or more than about eight items.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
Empty data draws just the frame with "No data." as the summary. A `null` value still holds its place with a small
presence ring (never a zero-radius void, and never skipped from the row) but drops its numeral — a missing figure is
visibly present yet explicitly unlabeled, never faked as a zero.
## Why this default [#why-this-default]
Value numerals are on by default because area lies quietly — a bubble twice the diameter is four times the area, and the
eye can't reliably invert that, so the number keeps the read honest. The radius is always `r ∝ √value` with no
exceptions; a linear-radius map would be a roughly squared lie. There is no sorting prop: the order is your data's
order, because reordering is your statement, not the chart's. The LOW precision rating and the MiniBar steer are printed
in the catalog, the machine `/catalog.json`, and this page's header — BubbleRow is the catalog's worked example of an
honest low-precision admission.
## Accessibility [#accessibility]
The accessible name names the extremes — **"4 items; largest EMEA at 1,240, smallest LATAM at 210."** The interactive
entry roves the bubbles with ←/→ (or hover), announcing each one's exact value — **"EMEA: 1,240."** — the number the
area itself can't carry.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, value }[]` | A few non-negative magnitudes. |
| `align` | `"center" \| "baseline"` | center (specimen) or baseline (weights on a shelf). |
| `label` | `"value" \| "both" \| "none"` | value (default), both, or none. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Bullet (/docs/charts/bullet)
Bullet compares one measured value to a target, against a backdrop of qualitative bands (poor / okay / good). It answers
"are we there yet?" in a single row — ideal for progress, SLAs, budgets, and quotas.
```tsx
```
## Install [#install]
```tsx
import { Bullet } from "@microcharts/react/bullet";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — progress to a goal, a value against a target, KPIs with thresholds.
* **Avoid for** — trends over time (use [Sparkline](/docs/charts/sparkline)) or distributions.
## Sizing [#sizing]
`width` and `height` are viewBox units that also set the rendered pixel box — a bullet reads best wide and short. Omit
them and drive the width from CSS to fill a table cell or card column; the viewBox keeps the aspect ratio.
## Variants [#variants]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The qualitative bands sit lowest and graduate in shade by step, because they are context, not the answer — the measure
bar has to read as the loudest mark on the row. The target is a tick, not a second bar: a distinct shape at a distinct
position so "value vs target" survives grayscale print and never depends on the two colors staying apart. The measure
bar itself is a thin band centered in the track (Few's proportion, not a full-height fill) so the bands stay legible on
both sides of it.
## Accessibility [#accessibility]
Bullet announces its value against its target — **"72 of 80 target."**. No color is required to read whether the target
was met: the measure's length and the target tick encode it by position, and the qualitative bands sit behind them. The
interactive entry reads the value and target on hover and focus.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The measured value. |
| `target` | `number` | Target tick to compare against. |
| `bands` | `number[]` | Ascending qualitative thresholds. |
| `domain` | `[number, number]` | Explicit [0, max]; auto-fit otherwise. |
| `label` | `"none" \| "value" \| "target" \| "both"` | Value/target readout in a right gutter (default none). |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BumpStrip (/docs/charts/bump-strip)
BumpStrip answers "where do we rank, and which way is it moving?" — position on an inverted ordinal scale, so #1 sits on
top and climbing draws upward. Ranks are not values: Sparkline would lie about the distance between #2 and #3.
```tsx
```
## Install [#install]
```tsx
import { BumpStrip } from "@microcharts/react/bump-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — leaderboard rows, category-rank trends in KPI cards.
* **Avoid for** — continuous values (Sparkline), or more than \~15 rank levels.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`"
>
```
A single ranked period draws its two end labels with no visible line — there is no trajectory to show yet. An all-`null`
series renders the frame with neither line nor labels; the accessible name reports it directly rather than describing a
blank chart. A flat run (rank never changes) draws a level line with zero change dots, since `dots="changes"` only marks
the periods where rank actually moved.
## Why this default [#why-this-default]
Change dots mark only the periods where rank actually moved — flat runs stay quiet, so the eye lands on the transitions.
End labels ("#5" → "#1") anchor the ordinal read without an axis.
## Accessibility [#accessibility]
The accessible name is the full trajectory — **"From #8 to #1 over 12 weeks; best #1."** The interactive entry steps the
periods (**"Week 7 of 12: #2."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | 1-based integer ranks; null = unranked period (gap). |
| `maxRank` | `number` | Fix the band so small multiples share a rank scale. |
| `dots` | `"changes" \| "none"` | Mark the moments rank actually moved. |
| `label` | `"ends" \| "last" \| "none"` | "#5" → "#1" endpoint labels. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# BurnChart (/docs/charts/burn-chart)
BurnChart answers "will we finish on time?". It draws the **plan** line (dashed, full length to the deadline), the
**actual** line to today, and a **dotted projection** whose slope is a plain linear fit over the last few actual points
— provisional by construction, never a smoothed or optimistic curve. The gap label states the signed schedule landing,
because "will we finish" is a number, not a vibe.
```tsx
`"
>
```
## Install [#install]
```tsx
import { BurnChart } from "@microcharts/react/burn-chart";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a sprint burndown in a tab header, will-we-finish in a KPI card, plan vs actual with a projected
landing.
* **Avoid for** — a single progress number (Progress) or a plain series (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
With a `locale`, the accessible summary's numbers follow that locale's own grouping — "2.300" in German, not "2,300".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
With no plan (`plan: []`), the projection still runs — the fitted slope only needs `actual` — but there's nothing to
compare it to, so the summary drops the "vs planned" clause. A stalled burn (`actual` flat) never reaches zero, so
`finishes` is `false` and the summary states that plainly rather than extrapolating a fake landing. A single `actual`
point draws no projection at all (the fit needs at least two).
## Why this default [#why-this-default]
The gap label, because "will we finish" is a number, not a vibe. History is drawn solid and exact; the projection is
dotted, muted, and computed as a linear fit over the last `max(2, ⌈actual/3⌉)` actual points — a stated method, never an
optimistic hand-drawn curve. When the recent burn flattens, the projection never reaches zero and the summary says so
outright ("not finishing at the current pace"), rather than implying a landing that isn't coming.
## Accessibility [#accessibility]
The accessible name states progress against the plan and the projected landing — **"6 of 11 days in: 19 points done vs
20 planned — projected to finish 2 days late."**. The interactive entry steps the days; history announces actual vs
plan, and the dotted region announces the projection.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ plan: number[]; actual: number[] }` | Remaining work per period (mode='down') or completed (mode='up'). |
| `mode` | `"down" \| "up"` | Burn-down (remaining → 0, default) or burn-up (done → scope). |
| `projection` | `boolean` | The dotted extrapolation to the deadline (default true). |
| `work` | `string` | Work-unit noun for the summary and readout. Defaults to `strings.burnWork` ('points' in EN), so a localized bundle replaces it. |
| `unit` | `string` | Period noun for the summary and gap label (default 'day'). |
| `label` | `"gap" \| "none"` | Signed schedule landing vs the deadline in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CalendarStrip (/docs/charts/calendar-strip)
CalendarStrip answers "what did the last few weeks actually look like, day by day?" — week-aligned recent activity where
real calendar position matters. Weekday rhythm is the read; for longer ordinal histories, use ActivityGrid.
```tsx
```
## Install [#install]
```tsx
import { CalendarStrip } from "@microcharts/react/calendar-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — habit/deploy cadence in KPI cards, week-aligned recent activity.
* **Avoid for** — long ordinal histories (ActivityGrid), exact per-day values (MiniBar).
## Variants [#variants]
```tsx
`">
```
```tsx
`">
```
`cell`/`gap` bump the edge length and spacing directly — grid-sibling parity with ActivityGrid and GardenGrid.
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Monday start + 4 weeks: one glance covers a month of weekday rhythm without scrolling history. All date math is UTC —
the same input renders identically in any host timezone, so SSR output never depends on where the server runs. `end`
defaults to today but should be pinned wherever determinism matters.
Empty ≠ zero: a day with no record renders as a faint outline, a day with value 0 as a filled track cell — a feed gap
never masquerades as a quiet day. Future days are blank, never extrapolated.
## Accessibility [#accessibility]
The accessible name counts real days — **"Active 11 of 24 days over 4 weeks."** The interactive entry walks the grid in
2-D and announces real calendar days (**"Thursday, June 11: 0."**, **"Tuesday, June 23: no data."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ date; value }[]` | Date-keyed values; duplicates sum with a dev warning. |
| `weeks` | `number` | Window length in whole weeks ending at `end` (default 4). |
| `end` | `string \| Date` | Last day of the window (defaults to today UTC — pin it for SSR determinism). |
| `weekStart` | `0 \| 1` | Locale start-of-week (default Monday). |
| `steps` | `number` | Intensity steps including the zero track (default 5). |
| `shape` | `"square" \| "round" \| "dot"` | Shared cell vocabulary. |
| `cell` | `number` | Cell edge length in viewBox units (default 7). |
| `gap` | `number` | Gap between cells (default 1). |
| `dateFormat` | `Intl.DateTimeFormatOptions \| (d: Date) => string` | (interactive) Announced day label (defaults to weekday + month + day, UTC). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CalibrationStrip (/docs/charts/calibration-strip)
CalibrationStrip answers "when this model says 70%, does it happen 70% of the time — and where is there enough data to
even ask?". Each bin is a dot at its predicted probability and its observed frequency, read against the identity
diagonal, with a quiet support lane underneath. Bins with too little data render open and faded, so a confident-looking
dot is never backed by three samples.
```tsx
```
## Install [#install]
```tsx
import { CalibrationStrip } from "@microcharts/react/calibration-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — classifier reliability / trust, probability-forecast auditing.
* **Avoid for** — a single accuracy number (Delta) or where the errors go
([ConfusionGrid](/docs/charts/confusion-grid)).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
Nothing visible changes with `locale` — this chart draws no numerals. It localizes the announced numbers: with
`locale="de-DE"` the accessible name above reads "3 bins; largest gap at 0,7 predicted (observed 0,52); 0 low-support
bins." — the locale's own decimal mark, not "0.7".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Dots plus an always-on support lane because "how sure" and "based on how much" must travel together — a reliability read
without support disclosure is exactly the failure this chart exists to prevent. Low-support bins are drawn open and
faded and the count is never disableable. There is deliberately no single-number calibration score (ECE) rendered or
announced: one number would hide precisely the per-bin structure the chart exists to show.
## Accessibility [#accessibility]
The accessible name reports the worst miscalibration and the support gaps — **"5 bins; largest gap at 0.7 predicted
(observed 0.52); 1 low-support bin."** The interactive entry roves the bins with ←/→, announcing each bin's predicted
and observed probability and its sample support.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `RawPair[] \| BinnedRow[]` | Raw pairs or pre-binned reliability rows. |
| `bins` | `number` | Uniform bin count for raw input. |
| `minSupport` | `number` | Below this a bin renders low-confidence. |
| `mode` | `"dots" \| "bars"` | Bars draw signed deviation columns. |
| `color` | `string` | Accent stroke/fill override. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ChangePoint (/docs/charts/change-point)
ChangePoint answers "when did the behavior change level?". It shades alternating regimes, draws a per-regime mean, and
marks the break where one level became another — because a spike means nothing without the regime it broke. The detector
is a **documented heuristic, not statistics**; for production anomaly pipelines, pass your own break indices and the
chart becomes pure annotation.
```tsx
`"
>
```
## Install [#install]
```tsx
import { ChangePoint } from "@microcharts/react/change-point";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — context for an anomaly, an error rate / latency / cost that stepped to a new level, or annotating a
known deploy or incident (pass explicit `breaks`).
* **Avoid for** — a gradual trend (Sparkline) or a plain time series with no regime question.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
With a `locale`, the summary and delta label format their numbers in that locale's own grouping and decimal marks.
Auto-detection (`breaks="auto"`) only runs at 8 or more points — a six-point series with an obvious jump still renders
as one flat, unbroken regime unless you pass `breaks` explicitly; this is deliberate, since a two-segment fit on that
few points is mostly noise. A single point has no line to draw and no break to find; `data={[]}` renders the frame with
the "no data" summary and no marks.
## Why this default [#why-this-default]
Context for every anomaly — the shading names the regime a value belongs to, and the break marks where the level truly
changed, so a reader sees "errors stepped up here" rather than "errors are high". The detector is a **labelled
heuristic**: a two-segment mean-shift found by binary segmentation, accepted only when the split both cuts the variance
enough and clears an effect-size threshold, so a constant series shows no break. It is a mean-shift test, not a trend
test: a steady ramp is still fitted as two regimes (the ramp variant above reports a shift around point 10), so pass
`breaks` explicitly when you already know the series is trending rather than stepping. For anomaly pipelines that
already know their breakpoints, `breaks={[…]}` turns the detection off and the chart becomes a faithful annotation.
## Accessibility [#accessibility]
The accessible name states the shift, the break, the regime means, and what happened after — **"Level shifted up 60%
around point 14 (mean 30 → 48); stable since."** — or, with no detected shift, "No clear level shift across N points."
The interactive entry steps the points with ←/→ (value + regime) and cycles the breaks with Tab, each announcing its
mean shift.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | A single series. |
| `breaks` | `"auto" \| number[]` | Explicit indices override the heuristic entirely — the production path. |
| `maxItems` | `number` | Max detected breaks (1–3). More regimes stop being glanceable. |
| `means` | `boolean` | Per-regime mean hairlines (default true). |
| `label` | `"delta" \| "none"` | Signed % across the most recent break, in a gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CitySkyline (/docs/charts/city-skyline)
CitySkyline answers "how do these groups compare on size, and how activated is each?" — two variables in one row.
**Height** is the primary, precise channel (zero-anchored bars, like a MiniBar); the **lit-window fraction** is a
secondary, low-precision channel you read as "mostly lit, half lit, or dark", not a number. Omit `lit` everywhere and
it's a plain, honest bar row.
```tsx
`"
>
```
## Install [#install]
```tsx
import { CitySkyline } from "@microcharts/react/city-skyline";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — team or region size plus an activation read, an org KPI where two variables are the story, or a per-BU
comparison with utilization.
* **Avoid for** — a single variable (that's `MiniBar`), precise activation reads, or more than about eight groups.
## Variants [#variants]
```tsx
({ label, value }))} />
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Windows come on when `lit` is present, because the two-variable read is exactly why this beats a `MiniBar`. Heights are
always zero-anchored bars; the lit windows are quantized to the window count and filled bottom-up, so the activation
reads as a fill level. There is no roofline variation, no antennas, and nothing encoded in building width — width, roof,
and ground are constants, because every mark must earn its place. The secondary channel drops out before the primary: a
building too short for any window row is a solid tower, and its `lit` still shows in the per-building readout on hover
or keyboard focus.
## Accessibility [#accessibility]
The accessible name names the count and the tallest — **"3 groups; tallest A at 46."** The interactive entry roves the
buildings with ←/→ (or hover), announcing each on its own: on the team demo at the top of this page, Platform reads
"Platform: 46; 70% lit." — the size precisely, the activation as a percent.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, value, lit? }[]` | value = height; lit = 0–1 window fraction. |
| `labels` | `boolean` | Category labels under the buildings. |
| `ground` | `boolean` | The baseline hairline (default true). |
| `label` | `"none" \| "value"` | Numeral above each building. |
| `unit` | `string` | Category noun for the summary (default 'groups'). |
| `bw` | `number` | Building width in viewBox units (default 9). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CohortTriangle (/docs/charts/cohort-triangle)
CohortTriangle stacks retention cohorts as rows and ages as columns, shading each cell by a discrete retention level.
Because newer vintages have been observed for fewer ages, the block takes the classic **triangle** shape — and reading
down a column compares every cohort at the **same maturity**, which is where "who retains worst" actually lives.
```tsx
```
## Install [#install]
```tsx
import { CohortTriangle } from "@microcharts/react/cohort-triangle";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — monthly or weekly cohorts side by side, spotting which vintage decays worst, reading retention at equal
maturity in a KPI card.
* **Avoid for** — exact per-cell values (color intensity is deliberately approximate; pair it with a number when
precision matters), or a single cohort's decay, where [RetentionCurve](/docs/charts/retention-curve) draws the curve
directly.
## Sizing [#sizing]
CohortTriangle sizes from `cell` — the edge length of one square in viewBox units. Bump it to scale the whole grid; the
viewBox keeps the aspect ratio when CSS drives the width. Row labels seat automatically and drop out cleanly below
roughly an 8-unit cell.
## Variants [#variants]
Row labels are on by default. Turn them off for a dense, glanceable block, or `highlight` one vintage to anchor the
equal-maturity comparison.
```tsx
`"
>
```
The `unit` prop renames the age columns in the summary and announcements — `"month"`, `"week"`, `"day"`, whatever the
cohort period is.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
A single cohort skips the comparison and just states its first reading. A `null` (or any non-finite) value renders as an
outlined **gap** slot — a "measured nothing here" cell, never a shaded 0%. `data` accepts either a 0–1 fraction or a
0–100 percent series — whichever the max value implies — so a raw percent export renders identically to its fraction
form. With a `locale`, every announced number follows that locale's own formatting.
## Why this default [#why-this-default]
Rows are cohorts in input order and columns are age, because the decision — *which vintage retains worst* — is a
**vertical** read at a fixed age, and the ragged right edge falls out naturally from newer cohorts simply having fewer
observed ages. Intensity is quantized to five discrete levels (never a continuous ramp): a smooth gradient would imply a
precision a handful of pixels cannot deliver. A worst-vintage flag is **off** by default — the marks stay honest and the
summary names the laggard in words instead of painting a verdict onto the grid.
## Accessibility [#accessibility]
Intensity is a color channel, so CohortTriangle always pairs it with a numeric summary that compares at equal maturity —
**"5 cohorts; at month 1, Mar retains worst (47%); newest May starts at 100%."**. Direction is stated in words, never by
color alone, so it survives forced-colors and color-blind viewing. The interactive entry adds 2-D arrow-key navigation,
announcing each cell as *"Feb cohort, month 1: 58%"* as you move.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, values }[]` | One row per cohort; values[i] = retention at age i (0–1 or 0–100, ragged). |
| `labels` | `boolean` | Cohort labels in a left gutter (default true; drops at tiny cell sizes). |
| `highlight` | `string` | Ring the cohort with this label — the comparison focus. |
| `unit` | `string` | Age-column noun for the summary (default "period"). |
| `cell` | `number` | Cell edge length in viewBox units. |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CometTrail (/docs/charts/comet-trail)
CometTrail answers "where is the value now, and where has it just been?" A bright head dot marks the current value;
behind it, a trail of recent points fades out with age, like a comet. In the interactive entry the head eases to each
new value and the old head decays into the trail, so a live stream draws a comet and a stall goes still. Opacity encodes
**age only** — the y position does the value — so the trail is recency context, never data you have to decode.
```tsx
```
## Install [#install]
```tsx
import { CometTrail } from "@microcharts/react/comet-trail";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## Motion, and reduced motion [#motion-and-reduced-motion]
Motion happens **only on a data change** — there is no idle loop. When a new value arrives the head eases (\~200 ms, the
library's canonical strong ease-out) from its old position to the new one and the previous head steps down into the
trail; a continuous stream produces the comet, a stalled stream simply goes still, which is itself the signal (staleness
reads as stillness). The dot jumps to the truth, eased — it never simulates phantom positions between updates. Under
`prefers-reduced-motion` the head repositions instantly (the static encoding is already complete), and the same is true
off-screen — one shared viewport check means the ease only runs while the chart is actually in view.
## When to use it [#when-to-use-it]
* **Good for** — a live price or metric with a little recency context, a realtime KPI that should show momentum, or
per-stream "where is it now" in a table.
* **Avoid for** — the full history (`Sparkline`), an exact multi-point comparison (`Sparkline`, `DotPlot`), or discrete
events (`HeartbeatBlip`).
## Sizing [#sizing]
`width` and `height` are viewBox units that also set the rendered pixel box — they default to `60 × 16`, sized to sit in
a table cell or beside a line of text. Omit them and drive the width from CSS to fill a column; the viewBox keeps the
aspect ratio. `trail` controls how many recent points the tail carries, which is a density decision rather than a size
one.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
Empty data draws just the frame, with "No data." as the summary. A single point has no trail to fade — just the head and
"Now 52." A `trail` larger than the data never backfills or pads: the trail simply shows every point there is.
## Why this default [#why-this-default]
`label="last"` is on because a live value without its number is a mood, not a measurement — the whole point is knowing
where it is now. Opacity encodes age and nothing else, so changing `trail` gives you more or less recency context
without ever changing the head read. The trail is capped at 20 points: past that it stops being a trail and becomes a
sparkline, which is the better tool for the full history.
## Accessibility [#accessibility]
The accessible name is the now-value and the recent trend — **"Now 62, rising over the last 3 updates."** — or just the
value when there is a single point, as in the edge case above: **"Now 52."** Arrow keys step back through the trail ("3
updates ago: 78.") and return toward now; motion is gated on `prefers-reduced-motion`, and the trail opacity is age,
never a value you must read from fading.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | The rolling window, oldest → newest (last = now). |
| `trail` | `number` | Points kept visible (default 12, cap 20). |
| `label` | `"last" \| "none"` | Numeral after the head (default last). |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ConfusionGrid (/docs/charts/confusion-grid)
ConfusionGrid answers "where do the errors go?" — the one thing accuracy-as-a-number hides. Rows are the actual class,
columns the predicted class, and each cell's ink is the row-normalized share: of the actual X, where did the predictions
go? The diagonal (agreement) is accented by shape, never by colour, so good and bad are positions on the grid, not hues.
The one-line key — rows actual, columns predicted — belongs next to every example.
```tsx
`"
>
```
## Install [#install]
```tsx
import { ConfusionGrid } from "@microcharts/react/confusion-grid";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — classifier evaluation, and any paired-classification agreement (triage vs outcome, plan vs actual,
rater A vs rater B).
* **Avoid for** — a single accuracy number (Delta) or more than four classes (use a full-size heatmap).
## Variants [#variants]
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
4 clamps to the first four classes (dev-warns)"
code="` `"
>
```
A fifth class doesn't grow the grid — legibility caps at 4×4, so a `k` of 5 or more clamps to the first four labels and
rows/columns, with a dev warning steering to a full-size heatmap. The grid never silently drops classes in production
(the warning is dev-only), but the render itself does truncate; pass pre-filtered `labels`/`counts` if you need a
specific four.
```tsx
`"
>
```
## Why this default [#why-this-default]
The row view answers the question practitioners actually ask — "of the real cats, how many did we call dogs?" — so
row-normalization is stated in the summary phrasing ("% of cats") and the denominator travels with every number.
Accuracy is off by default and never rendered without the grid: the number may not leave its context. The diagonal
accent is a shape (an inset stroke), and the ink ramp is identical for agreement and error cells — good and bad are
positions, not colours, which is what keeps it readable under forced colours.
## Accessibility [#accessibility]
The accessible name states accuracy and the worst confusion, with the row-normalized denominator carried in the phrasing
— the grid at the top of this page reads **"Accuracy 73%. Most confused: B predicted as C (43% of Bs)."** A perfect
classifier reads **"Accuracy 100%. No confusion."**, and a class with no samples is named rather than hidden — the
`cat`/`dog` example above reads **"Accuracy 80%. Most confused: cat predicted as dog (20% of cats). No dog samples."**
The interactive entry roves the cells with the arrow keys (Home and End jump to the two ends of the diagonal),
announcing each cell's actual and predicted class as a share of the actual class.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ labels, counts }` | k×k matrix; rows actual, columns predicted. |
| `normalize` | `"row" \| "none"` | Row = recall view (default). |
| `accent` | `"diagonal" \| "errors"` | Agreement or the worst confusion. |
| `label` | `"accuracy" \| "none"` | Overall accuracy in the gutter (opt-in). |
| `shape` | `"square" \| "round"` | Cell shape from the shared vocabulary (default 'square'). |
| `color` | `string` | Accent fill override. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Constellation (/docs/charts/constellation)
Constellation answers "when did the rare events happen, and how big were they?" Each event is a dot placed by time (x)
and value (y), with an optional magnitude driving its area-true size. A hairline line connects the events in time order,
so the sequence of a handful of incidents reads at a glance. It is built for *rare* events — a dozen or fewer; dense
streams belong to `Seismogram` or `EventTimeline`.
```tsx
["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep"][x];
`"
>
```
## Install [#install]
```tsx
import { Constellation } from "@microcharts/react/constellation";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — sparse incidents or outages on a timeline, milestones carrying a magnitude, or any rare events where
the sequence is the story.
* **Avoid for** — dense event streams (`Seismogram`, `EventTimeline`), a continuous trend (`Sparkline`), or precise
value comparison (dot area is a low-precision channel).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
Empty data draws no marks at all — just the sized box — with "No data." as the summary. A single event has no line to
draw — just one dot, and the summary says "1 event at …" rather than forcing a range. When every point omits `y`, the
vertical spread is deterministic jitter for legibility only — the summary and the interactive readout never mention it,
and the connector's slope carries no meaning in that mode.
## Why this default [#why-this-default]
The chronology line is on by default because the sequence is usually the story of rare events — you want to see the
order and rhythm, not just a scatter. Dot size is *area-true* (`r ∝ √m`), because area is how the eye reads magnitude;
it is a deliberately low-precision channel, so the number lives in the summary and the hover readout, not in a size you
are asked to measure.
When no event carries a value, the vertical position is **deterministic jitter that encodes nothing** — it only spreads
the dots so they do not stack. In that case the connector's slope is meaningless (it still runs in time order), and the
summary never mentions vertical position. Time is sacred: the x position is never jittered, so simultaneous events sit
at the same x.
## Accessibility [#accessibility]
The accessible name states the count, the span, and the largest event — **"4 events between 0 and 8; largest at 8."**
The interactive entry lets you arrow through the events chronologically, each announced with its time, value, and
magnitude through a polite live region; a hover readout shows the same. Magnitude is redundant with the dot size, never
carried by it alone.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ x: number; y?: number; m?: number }[]` | Events: x = time, y = value, m = magnitude (area-true size). |
| `connect` | `boolean` | The faint chronology line (default true). |
| `label` | `"max" \| "none"` | Numeral at the largest event. |
| `xFormat` | `(x: number) => string` | Formats time for the summary (e.g. a month name). |
| `xDomain` | `[number, number]` | Time (x) extent (default: data extent). |
| `rBase` | `number` | Base dot radius in viewBox units (default 1.6). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ControlStrip (/docs/charts/control-strip)
ControlStrip answers "is the process in control — or did something leave the band?". It's a Shewhart individuals chart:
the control band is the process center ± 3σ̂, where **σ̂ = mean moving range ÷ 1.128** — the individuals estimator, stated
outright (sample SD is not used; it inflates the limits under drift). In-control points are bare vertices; only
out-of-control points are marked (ringed, in the negative color), because an in-control process should look boring.
```tsx
`"
>
```
## Install [#install]
```tsx
import { ControlStrip } from "@microcharts/react/control-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a production line or metric in a table cell, an SPC control chart in a KPI card, flagging
out-of-control excursions at a glance.
* **Avoid for** — a plain trend (Sparkline) or a strongly trending series (ChangePoint — control limits assume a
stationary process).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
The chart draws no in-chart numbers, so `format`/`locale` shape the accessible summary and the interactive announcements
— the strip above reads "All 12 points within control limits (center 2,5, limits 2,1–2,9)." with German decimal commas.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Flag only the out-of-band points, because an in-control process should look boring — the eye should land on the
exception, not scan every dot. The σ̂ estimator is the moving-range one (`MR̄ / 1.128`), never a vague "±3 sigma";
`limits="percentile"` states its exact quantiles for skewed processes. Western Electric rules are conventions, so the
implemented subset is enumerated (WE-1 beyond 3σ, WE-2 two-of-three beyond 2σ, WE-4 eight on one side) and none fires
silently. With fewer than 10 points the band is dashed and the summary says "limits provisional".
## Accessibility [#accessibility]
The accessible name states the excursion count, the center, and the limits — **"2 of 30 points outside control limits
(center 74.17, limits 67.84–80.49)."**; an in-control run reads "All N points within control limits …". The interactive
entry steps the points and out-of-control points announce which limit they crossed.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Sequential measurements. |
| `limits` | `"sigma" \| "percentile"` | ±3σ̂ (default) or empirical p0.135/p99.865 for skewed processes. |
| `baseline` | `number` | Known process center from a reference period (else = mean). |
| `rules` | `"none" \| "we"` | Western Electric secondary run rules (WE-1/2/4 subset). |
| `dots` | `"out" \| "all" \| "none"` | Mark only out-of-control points (default), every point, or none. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CoverageStrip (/docs/charts/coverage-strip)
CoverageStrip answers "can I trust this data — where was nothing measured?". Measured slots are filled; gaps are hollow
with a hairline outline. The distinction between `null` (no measurement) and `0` (a measured zero) is the whole chart,
and it is carried by **shape**, so it survives forced-colors and print. Nothing is ever interpolated across a gap.
```tsx
```
## Install [#install]
```tsx
import { CoverageStrip } from "@microcharts/react/coverage-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — data-quality cells beside a metric, sensor-uptime rows, trailing-gap detection.
* **Avoid for** — magnitude over time (HeatStrip) or exact values (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
`locale` formats the coverage percentage in the gutter and the accessible summary — the strip above reads "56 %" (German
percent spacing) instead of "56%". `format` shapes the measured values the interactive entry announces.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Shape carries the presence/absence distinction, not just color, so it survives forced-colors and print. A measured `0`
is a filled cell like any other value; only `null` (no record) is hollow — the two never look the same. `expected` lets
a trailing shortfall count as coverage loss: a feed that simply stops reporting is the worst gap of all, so it isn't
silently dropped from the denominator. Nothing is ever interpolated across a gap — a missing slot stays visibly empty
rather than being smoothed into its neighbors.
## Accessibility [#accessibility]
The accessible name states how many slots were measured, the coverage percentage, and the longest gap — **"10 of 18
slots measured (56 %); longest gap 4 slots."** An empty series states "No data." instead. The interactive entry roves
slots with ←/→, announcing each one as a measured value or "no measurement".
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | Time-ordered slots; null = no measurement, 0 = a measured zero. |
| `expected` | `number` | Slots the window should contain — lets trailing missingness count. |
| `mode` | `"binary" \| "intensity"` | Presence only (default), or shade measured cells by value. |
| `steps` | `number` | Intensity granularity (default 5). |
| `shape` | `"square" \| "round" \| "dot"` | Cell shape from the shared vocabulary (default 'square'). |
| `label` | `"percent" \| "none"` | State the coverage number in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# CyclePlot (/docs/charts/cycle-plot)
CyclePlot answers "what repeats beneath the trend — and is any slot itself drifting?". The series is reshaped into
`period` slots (7 for weekdays, 12 for months); each slot shows its own raw values across cycles as a muted polyline in
**time order**, plus a mean tick, and the accent spine connects the slot means. Seasonality (the spine) and drift (the
within-slot lines) are different questions of the same data — so they never share a mark.
```tsx
`"
>
```
## Install [#install]
```tsx
import { CyclePlot } from "@microcharts/react/cycle-plot";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a KPI card that says "the week has a shape", weekday traffic / hourly load / monthly sales seasonality,
or spotting one slot that is itself drifting.
* **Avoid for** — a plain time series (Sparkline) or one composition (SegmentedBar). `period` outside 4–12 warns in
development: below that there is no cycle to see, above it the slots stop being separable.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
A slot with no finite values in it draws no mean tick and no within-slot line; the spine connects the non-empty slots
only, joining an empty slot's neighbours directly rather than dipping to a made-up center. A single cycle (one value per
slot) draws the spine and dots as usual, but no within-slot polylines — a line needs at least two observations in a
slot.
## Why this default [#why-this-default]
A mean spine plus raw slot lines, because seasonality and drift are different questions asked of the same data — and
answering both on one mark would blur them. The within-slot lines are never smoothed and never joined across a slot
boundary: each slot's polyline begins and ends inside its own column, so a Monday trend can never bleed into Tuesday.
The `center` choice is explicit — mean by default, median when a slot's distribution is skewed — and the interactive
per-slot announcement names which one it used.
## Accessibility [#accessibility]
The accessible name states the peak and dip slots and any leading drift — **"Peaks slot 6 (61), dips slot 1 (38); slot 2
rising across 3 cycles."** The interactive entry steps the slots (mean, cycle count, drift) with ←/→, and steps the
individual observations within a slot with ↑/↓.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | A flat series, reshaped row-major into `period` slots. |
| `period` (required) | `number` | Slots per cycle (4–12) — e.g. 7 for weekdays. |
| `slots` | `string[]` | Slot names for summaries, e.g. weekday labels. |
| `center` | `"mean" \| "median"` | Center statistic — median for skewed slot distributions. |
| `trend` | `boolean` | Within-slot micro-trend line (default true); false = spine + ticks only. |
| `spine` | `boolean` | The slot-center spine (default true); false leaves within-slot drift only. |
| `cycleUnit` | `string` | Cycle noun for the summary, e.g. 'weeks' (default 'cycles'). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DataDiff (/docs/charts/data-diff)
DataDiff answers "what changed between these two versions?". Each key gets a diverging bar: **removed leftward, added
rightward, both always drawn** on one symmetric shared scale. Net is a mark you can turn on, never a replacement for the
two bars — a +500/−480 churn and a +20/−0 trickle must never look alike.
```tsx
```
## Install [#install]
```tsx
import { DataDiff } from "@microcharts/react/data-diff";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a table cell per dataset version, a KPI card for a sync or import job, any per-key added/removed audit
where churn matters.
* **Avoid for** — a plain ranking (MiniBar) or parts of a single whole (SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
An empty `data` array renders an empty chart and reports **"No data."** A non-empty diff where every key is 0/0 still
draws its hairline placeholder ticks and reports **"No changes across 2 keys."** — the keys are named even when nothing
about them changed.
## Why this default [#why-this-default]
Both directions are always drawn, because net-only diffs hide churn: a table that added 500 rows and dropped 480 is not
the same event as one that added 20 and dropped nothing, even though both net to roughly the same number. One symmetric
scale spans every row so a big change never shrinks to fit a small one, and a key with no change still gets a hairline
tick — the absence of a change is not the absence of the key. Cross-chart comparison goes through a shared `domain`.
## Accessibility [#accessibility]
The accessible name states the totals, the key count, and the single largest change — **"+468 added, −170 removed across
3 keys; largest change: users (+220)."** The interactive entry steps the rows and announces each key's added, removed,
and net change.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ key; added; removed }[]` | Per-key change counts — added and removed are non-negative magnitudes. |
| `labels` | `boolean` | In-chart key tags for standalone use (host tables carry keys by default). |
| `net` | `boolean` | A tick at added−removed per row — a summary mark, never the two bars. |
| `order` | `"data" \| "net" \| "magnitude"` | Default 'data' keeps input order (schema order is often meaningful). |
| `label` | `"totals" \| "none"` | 'totals' prints a +added / −removed footer. |
| `maxItems` | `number` | Row cap (default 12); rows beyond it are dropped with a dev warning. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Delta (/docs/charts/delta)
Delta is a text-first metric: a signed number with a direction glyph. It reads inline, next to a KPI, or inside a table
cell. Direction is always doubled — a triangle *and* a color — so it never relies on color alone.
```tsx
```
## Install [#install]
```tsx
import { Delta } from "@microcharts/react/delta";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a KPI's change, period-over-period percent, inline metric movement.
* **Avoid for** — showing a series or comparing magnitudes across items.
## Sizing [#sizing]
Delta is text, not a fixed box — it takes the `font-size` of whatever wraps it and its glyph scales in `em`. Size it by
the surrounding type: inline, it inherits the sentence; beside a KPI figure, lift the font-size to match.
## Variants [#variants]
`from` derives a percent change; `positive="down"` flips only the color for metrics where down is good (latency, churn,
cost).
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the magnitude follows that locale's own grouping and
decimal marks (a German reader sees a comma decimal and a space before the `%`, not a period). The leading sign is the
library's own `+` / `−`, so it reads the same in every locale.
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
A zero delta shows the flat glyph and **"No change."** — never a `+0%`/`−0%` that implies a direction with nothing
behind it. `NaN` and `±Infinity` degrade the same way: the flat glyph, an em dash instead of a number, and **"No
change."** rather than a misleading `NaN%`.
## Why this default [#why-this-default]
Direction is always double-encoded — the triangle's shape carries it as much as the color does — because a
red/green-only read fails for color-blind viewers and under `forced-colors`. `positive` flips only which color means
"good"; it never changes which way the glyph points, so a metric where down is good (latency, churn, cost) still shows
an honest down-pointing triangle, just in the "good" color. Percent is the default format because most deltas are
relative change, not raw magnitude — pass `format` for currency, counts, or any other unit. Non-finite input renders as
an em dash rather than `NaN%`, because a broken number should read as "nothing to report," not as a fabricated value.
## Accessibility [#accessibility]
Delta renders accessible inline text — the glyph is decorative and the value carries the meaning. Color is a redundant
channel on top of the direction glyph and the sign, so the change survives forced-colors and color-blind viewing. The
interactive entry re-announces the figure through a polite live region when the value changes.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The change, or current value when from is set. |
| `from` | `number` | Prior value; Delta shows the percent change. |
| `positive` | `"up" \| "down"` | Which direction is good (colors only). |
| `format` | `Intl.NumberFormatOptions \| fn` | Number formatting. |
| `locale` | `string \| string[]` | BCP 47 locale(s) for the formatted number. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DepthWedge (/docs/charts/depth-wedge)
DepthWedge answers "how much pressure is stacked on each side of the current level, and how wide is the gap between
them?". Demand accumulates leftward from the spread and supply rightward, so the two filled wedges show the book's
posture in a glyph. The y-scale is linear, full stop — log-depth reading belongs to full-size tools.
```tsx
`"
>
```
## Install [#install]
```tsx
import { DepthWedge } from "@microcharts/react/depth-wedge";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — order-book depth / liquidity, supply vs demand posture.
* **Avoid for** — a time series (Sparkline) or a single ratio (Delta).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Linear scale plus a stated `levels` span because a wedge you can't interrogate must not editorialize — a silent log axis
would exaggerate near-spread depth and flatter thin books. The visible range is part of the claim, so whenever the
summary names a leading side it scopes the ratio with "within the shown range", and the spread — the gap between the
best bid and best ask — is the headline number above the mid line.
## Accessibility [#accessibility]
The accessible name states the posture, range-scoped — **"Demand outweighs supply 1.42× within the shown range; spread
1."** (the hero demo's order book). The interactive entry walks the levels with ←/→, announcing the cumulative depth on
each side and its distance from the mid.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ demand, supply }` | Level/amount rows per side. |
| `levels` | `number` | ± level distance from mid to include. |
| `normalize` | `boolean` | Plot cumulative shares per side. |
| `label` | `"spread" \| "none"` | The gap is the headline number. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DicePips (/docs/charts/dice-pips)
DicePips answers "what is this small count or severity?" the way dice do — a face you read without counting. The brain
subitizes the canonical 1–6 patterns instantly. Zero is an empty face (zero, not missing); above six there is no
subitizable pattern, so the face shows the exact numeral rather than inventing a seven-pip layout. The face never
pretends.
```tsx
```
## Install [#install]
```tsx
import { DicePips } from "@microcharts/react/dice-pips";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a severity or rating 0–6 in a cell, an at-a-glance small count in a sentence, or an incident-severity
badge.
* **Avoid for** — counts above six (TallyMarks), magnitudes (MiniBar), or proportions (Progress).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
The face outline keeps a lone die legible on any surface, so it is on by default; `face={false}` drops it for repeated
table columns where the header already frames the column. Six is the ceiling because pip patterns above six are not
subitizable — asking the reader to count invented pips would be slower than reading a number, so the chart refuses and
shows the numeral instead. That numeral fallback is the documented honesty rule, shown live at `value={9}`.
## Accessibility [#accessibility]
The accessible name states the value against its range — **"4 out of 6."** — and drops the frame to just the exact
number once past six (**"9."**). Zero reads "0 out of 6." (a real zero), and an invalid value reads "No data." The
interactive entry pops the pips into place on change (a short scale-up with a per-pip stagger, skipped under
`prefers-reduced-motion`) and announces the new face through a polite live region; the pips are one value, so there is
no cursor to move.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Integer 0–6 (rounded); above 6 shows a numeral. |
| `face` | `boolean` | Draw the die outline (default true). |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DotPlot (/docs/charts/dot-plot)
DotPlot answers "how do a few named values compare on one scale?" with the minimum ink per comparison. Dots over bars
when the scale doesn't start at zero — position lies less than truncated bar length.
```tsx
`"
>
```
## Install [#install]
```tsx
import { DotPlot } from "@microcharts/react/dot-plot";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — KPI leaderboards, named comparisons in cards, rows where a truncated bar would lie.
* **Avoid for** — more than 7 rows or time series (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
An empty series has nothing to compare, so no dots draw and the accessible name reads **"No data."** A single category
still renders normally — one dot, one label — with the summary's "category"/"categories" wording matching the count
(**"1 category. Highest Only 42, lowest Only 42."**). A `null` value draws no dot and no label for its row, but the row
still occupies its slot in the vertical rhythm — the real values around it keep the same spacing they'd have in a fully
populated set — and it's excluded from the category count in the summary, not counted as a comparison with nothing.
## Why this default [#why-this-default]
Without `stem` the domain fits the data (a position read); with `stem` the domain is forced through zero (a magnitude
read) — the prop flips the honesty regime, and the docs say so. Coincident dots on adjacent rows de-overlap by half a
unit; labels truncate by character count and drop entirely when rows get too dense — all pure arithmetic, nothing
measured.
## Accessibility [#accessibility]
The accessible name carries count and extremes — **"3 categories. Highest Ada 96, lowest Kim 41."** The interactive
entry roves rows and announces each with its rank (**"Ada: 96 — 1st of 3."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Named values. |
| `stem` | `boolean` | Hairline from zero — flips to a magnitude read (zero-anchored domain forced). |
| `highlight` | `number \| string` | Accent one category. |
| `label` | `"value" \| "none"` | Value text beside each dot (drops out under 8-unit rows). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DualSparkline (/docs/charts/dual-sparkline)
DualSparkline answers "how is this series doing against its benchmark?" Exactly two series, ever — three overlapped
lines at 16 px are unreadable (SparkGroup for that). The benchmark whispers: dashed, thinner, neutral — never
distinguished by color alone.
```tsx
```
## Install [#install]
```tsx
import { DualSparkline } from "@microcharts/react/dual-sparkline";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — metric-vs-benchmark in table cells, actual-vs-plan in KPI cards.
* **Avoid for** — 3+ series (SparkGroup), or different units per series (never dual axes).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`"
>
```
Each series carries its own gaps independently — a `null` in `data` breaks only the primary line at that index, a `null`
in `compare` only the benchmark, and the nearest-x hover still reports both values (or "no data" for whichever side is
missing). With a `locale`, the endpoint label and both hover values follow that locale's grouping.
## Why this default [#why-this-default]
One shared domain across both series — the entire point is comparability, so there are no dual axes and no per-series
normalization. A shorter benchmark simply ends; stretching it would fake correlation. Coincident endpoints dedupe to one
dot so ink never doubles.
## Accessibility [#accessibility]
The accessible name reads both trends and both endpoints — **"Trending up 75% vs benchmark up 33%. Last 21 vs 16."**
Identical series read "Matching benchmark." The interactive entry announces pairs (**"Point 9 of 12: 17 vs 15."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | The series being judged. |
| `compare` (required) | `(number \| null)[]` | The benchmark — dashed, thinner, neutral. |
| `curve` | `"linear" \| "smooth" \| "step"` | Line shape (default 'linear'). |
| `band` | `[number, number]` | Normal-range band behind both (shared grammar). |
| `label` | `"last" \| "none"` | Endpoint value label for the primary series. |
| `dots` | `"auto" \| "none"` | Endpoint dots on both lines (default "auto"). |
| `seriesStrings` | `SeriesStrings` | i18n strings for the per-series trend clauses. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# DualWindowMeter (/docs/charts/dual-window-meter)
DualWindowMeter answers "is the level compliant against its target both right now and on average — momentary spikes vs
sustained drift?". From one raw series it computes two rolling means: a thin fast window that reacts instantly and a
thick slow window that carries the sustained read, both against a target line. It is the general form of any noisy
metric with a compliance target.
```tsx
```
## Install [#install]
```tsx
import { DualWindowMeter } from "@microcharts/react/dual-window-meter";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — loudness / LUFS metering, latency SLO or CPU-headroom compliance.
* **Avoid for** — a single series (Sparkline) or anything without a target to compare against.
## Variants [#variants]
```tsx
-22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2,
);
`"
>
```
```tsx
-22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2,
);
// both window sizes are part of the reading — state them, never hide them
`"
>
```
```tsx
-22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2 - (i > 40 ? 2 : 0),
);
`"
>
```
With a `locale`, the right-edge readings and the accessible summary follow that locale's decimal mark — the fast reading
above renders "-23,9", not "-23.9", and the accessible name reads "Slow window -25 vs target -23; fast -23,9."
## Edge cases [#edge-cases]
```tsx
`"
>
```
## Why this default [#why-this-default]
Thin-fast, thick-slow because the sustained read should carry more ink — a momentary spike on the fast trace matters
less than a slow-window drift away from target. The window sizes are part of the reading, not a hidden implementation
detail — `windows` is an explicit prop, and the examples that change it state both sizes. Both traces always share one
domain; a trace begins only where its window has filled, never faking a partial-window value.
## Accessibility [#accessibility]
The accessible name leads with the sustained read — **"Slow window -25 vs target -23; fast -23,9."** The interactive
entry roves the samples with ←/→, announcing the fast value, the slow value, and the target at each point.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw samples; two rolling means are computed. |
| `target` (required) | `number` | The compliance line — required. |
| `windows` | `[number, number]` | Fast/slow integration windows (samples). |
| `band` | `[number, number]` | A compliance corridor instead of one line. |
| `domain` | `[number, number]` | Fix the vertical scale instead of auto-fitting both traces. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Dumbbell (/docs/charts/dumbbell)
Dumbbell answers "where did each row start and end?". The hollow dot is *before*, the filled dot is *after* — direction
is shape-coded, so it never relies on color alone. With `positive` the connector takes the valence token by direction.
```tsx
```
## Install [#install]
```tsx
import { Dumbbell } from "@microcharts/react/dumbbell";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — salary bands, before/after per table row, ranges.
* **Avoid for** — many categories where crossings matter (Slope) or the path between (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
When a row's `from` equals its `to`, the connector is dropped and only one filled dot is drawn — never a
hollow-and-filled pair sitting on top of each other pretending to be two points. The summary says **"No change at 55."**
for a single row.
## Why this default [#why-this-default]
Hollow → filled reads as before → after without a legend, and renders identically whichever way the data runs —
direction comes from the values, not the array order. For *ranges* (min→max, confidence spans) the docs require dropping
`positive`: a range has no valence, and coloring it green or red would invent one.
## Accessibility [#accessibility]
Single row: **"From 40 to 60, up 50%."**; a degenerate pair says **"No change at 55."**; multi-row leads with the
largest change (**"2 rows. Largest change Paris, up 17%."**). The interactive entry roves rows with ↑/↓ (←/→ do the
same), announcing each row's own pair — **"From 52 to 61, up 17%."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label?; from; to }[]` | Start/end pairs. |
| `positive` | `"up" \| "down"` | Direction valence for CHANGES; drop it for ranges (no valence). |
| `label` | `"value" \| "none"` | From/to values outside the dots (drop when the span is tight). |
| `highlight` | `number \| string` | Accent one row. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# EnsembleGhosts (/docs/charts/ensemble-ghosts)
EnsembleGhosts answers "what could happen, across the simulated futures?". It draws a faint bundle of member paths with
one emphasized representative — because a mean line hides that futures disagree in **shape**, not just endpoint. Ghost
selection is fully deterministic (evenly spaced endpoint-rank quantiles), so the same input renders identically every
time.
```tsx
`"
>
```
## Install [#install]
```tsx
import { EnsembleGhosts } from "@microcharts/react/ensemble-ghosts";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a KPI card that shows the futures, not the average; Monte-Carlo / simulation output where paths
disagree in shape; showing that outcomes fan out.
* **Avoid for** — interval precision (ForecastCone) or a single path (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
With one member, there is no bundle to show — the chart draws that single path as the representative, and the summary
states its ending value rather than a spread. A member that contains a non-finite value (`NaN`, `Infinity`) is excluded
entirely from ghost selection and the median, not gapped mid-path — a dev-only console warning flags it, since a
partially invalid future usually means bad simulation output.
## Why this default [#why-this-default]
A few faint paths and one emphasized, because a mean line hides that futures disagree in shape, not just endpoint — two
ensembles can share an average and a final range while telling completely different stories along the way. The
emphasized path defaults to the **real member** closest to the pointwise median, so the representative is an outcome
that actually happened in the simulation, not a synthetic average of paths that never occurred (`emphasis="median"`
draws that synthetic path instead). Selection and emphasis are deterministic — nothing about this chart varies between
renders of the same data. `label="end"` (default) states the emphasized path's landing value in a right gutter; pass
`label="none"` to drop it.
The static frame is not a hypothetical-outcome plot. The **loop** — flipping through the members one at a time on hover
— is the HOP, and it lives only in the interactive entry, gated on reduced motion with arrow-key stepping as the
non-animated equivalent.
## Accessibility [#accessibility]
The accessible name states the endpoint spread and the typical path — **"5 simulated paths end between 28 and 61;
typical path ends near 50."** The interactive entry flips through the members (the HOP loop) on hover; with reduced
motion, ←/→ step them discretely, and each member is announced with its endpoint.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[][]` | Ensemble members — 2–50 simulated paths. |
| `ghosts` | `number` | Rendered member count (deterministic endpoint-rank selection). Default 8, cap 12. |
| `emphasis` | `"nearest-median" \| "median" \| number` | A real median-like member, the synthetic median, or a pinned member. |
| `endpoints` | `boolean` | Ghost endpoint dots — makes the final-value spread countable. |
| `label` | `"end" \| "none"` | Emphasised path endpoint in a right gutter (default end). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ErrorBudget (/docs/charts/error-budget)
ErrorBudget answers "are we burning the error budget too fast to survive the window?". The budget remaining rides
against the **steady-burn diagonal** — the pace that exactly spends the window — so "too fast" is legible at a glance:
below the diagonal means you're outrunning your budget. Faster burn-rate reference lines (the Google-SRE 1×/6×/14.4×
**convention**, not physics) sit below it as faint policy context.
```tsx
```
## Install [#install]
```tsx
import { ErrorBudget } from "@microcharts/react/error-budget";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — an SLO error budget in a KPI card, a service list where each row is a budget, spotting a fast-burn
before it exhausts the window.
* **Avoid for** — a plain uptime series (Sparkline) or a one-number budget (Progress).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
`format` defaults to a percent formatter; with `locale` it follows that locale's own conventions for the
remaining-budget label — "62 %" (space before the sign) under `de-DE` rather than the English "62%".
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
With no data the chart renders an empty root and the accessible name says **"No data."**; a single point still draws the
diagonal, wedges, and endpoint, though `currentRate` (which needs a prior step) reads `0`. Once the remaining line
reaches zero, an ✕ at that first zero-crossing replaces the endpoint dot, and the summary reads **"Budget exhausted at
day 7 of 20."** instead of the normal remaining/rate sentence. Values are clamped to `0–1`, so nothing is ever drawn
below the floor or above a full budget.
## Why this default [#why-this-default]
Remaining-vs-diagonal, because "too fast" is only legible against the pace that exactly spends the window. The
1×/6×/14.4× reference lines are the SRE Workbook's multiwindow burn-rate alert **convention** — a policy default, never
universal law — so `rates` is configurable and the lines render as faint background context, never data-colored.
`currentRate` is the observed burn slope over the last `max(2, ⌈n/6⌉)` steps ÷ the steady slope; when the budget hits
zero, an ✕ marks the crossing and the summary says so.
## Accessibility [#accessibility]
The accessible name states the budget, the elapsed window, and the burn rate — **"62% of error budget remains at day 12
of 30 — burning at 0.6× the steady rate."**; an exhausted window reads "Budget exhausted at day N of M." The interactive
entry steps the days and announces each step's remaining and local burn rate.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Budget remaining (0–1) per elapsed step; index 0 is 1.0. |
| `window` | `number` | Total steps in the SLO window (default = data.length). |
| `rates` | `number[]` | Burn-rate reference multiples (default the SRE 1×/6×/14.4× convention). |
| `unit` | `string` | Period noun for the summary (default "day"). |
| `label` | `"remaining" \| "none"` | Current budget % in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# EtaBar (/docs/charts/eta-bar)
EtaBar answers "how long is this actually going to take, given how it has actually been going?". Its x-axis is time, not
fraction: the solid part is the elapsed share of the predicted total, and the muted remainder is sized by the observed
rate. When the rate drops, the remainder honestly grows — the download bar, told truthfully.
```tsx
```
## Install [#install]
```tsx
import { EtaBar } from "@microcharts/react/eta-bar";
`${Math.round(t)} min`} title="Export" />
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — download / export progress, job-queue ETAs, deploy timers.
* **Avoid for** — fraction-only progress (Progress) or unbounded counters (Delta).
## Motion, and reduced motion [#motion-and-reduced-motion]
The interactive entry eases the elapsed/remainder split (SVG `x`/`width`) to its new geometry whenever `progress`,
`elapsed`, or `rate` change — the split only ever moves to a value computed from data you passed in on that render; it
never interpolates ahead of an as-yet-unseen future frame. The transition lives in shared CSS (`.mc-eta-live rect`), so
it inherits the shared `:where(.mc-root *) { transition: none }` reduced-motion block: with
`prefers-reduced-motion: reduce` the bar snaps straight to each new split instead of easing. The live region
re-announces the forecast on a throttle (`announceEvery`, default 10 s) so a fast-updating value doesn't spam a screen
reader.
## Variants [#variants]
```tsx
`">
```
```tsx
`"
>
```
Without an `etaFormat`, the remaining-time figure falls back to `format`/`locale` — under `de-DE` that's a decimal comma
("11,6") rather than the English decimal point ("11.6"). Pass `etaFormat` (as the hero example does) to add a unit; it
always receives the raw number, so it can format the unit itself in whatever locale-aware way the caller needs.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
An explicit `rate` of `0`, or an absent `rate` from which no positive average can be derived from `elapsed`, renders the
remainder as a diagonal-hatched texture instead of a solid bar — "unknown," not a hidden guess — and the summary says
**"stalled"** rather than naming a duration. When the predicted remainder is far larger than the elapsed time, the done
segment would round to an unreadable sliver, so the geometry clamps it to a visible 10% and adds a chevron marking the
clamp as approximate. At `progress >= 1` the bar fills completely regardless of `rate`.
## Why this default [#why-this-default]
The ETA label is the default because "how long?" is the question a plain progress bar pretends to answer. The remainder
is sized by the observed rate, never linear interpolation — so a stalling transfer looks stalled instead of marching to
a fake finish. When the rate is unknown or zero the remainder becomes an indeterminate texture and the summary says so,
rather than inventing a countdown.
## Accessibility [#accessibility]
The accessible name is the honest forecast — **"42% done; about 11.6 remaining at the current rate."** A stalled
transfer reads **"30% done; stalled."** and a finished one **"Done."** The interactive entry re-announces on a throttle
as the forecast changes.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `progress` (required) | `number` | Completed fraction 0–1. |
| `elapsed` (required) | `number` | Time spent, any unit. |
| `rate` | `number` | Progress per time unit — pass a recent-window rate. |
| `label` | `"eta" \| "percent" \| "none"` | The remaining-time read is the product. |
| `etaFormat` | `(t: number) => string` | Unit-bearing ETA label ("2 min") — the caller owns units. |
| `announceEvery` | `number` | (interactive) Minimum ms between live-region announcements as the ETA streams (default 10000). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# EventRaster (/docs/charts/event-raster)
EventRaster answers "when did each source fire — and do sources fire together, in sequence, or not at all?". Vertical
bands reveal synchronization, diagonals reveal propagation, and sparse rows reveal silence. One tick is always one event
— the only exception is the disclosed `overflow="bin"` mode for aliasing lanes.
```tsx
```
## Install [#install]
```tsx
import { EventRaster } from "@microcharts/react/event-raster";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — service events across sources, agent steps, cron / sensor triggers.
* **Avoid for** — a single lane (RugStrip) or continuous rates (Sparkline).
## Variants [#variants]
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
i * 0.3) }]}
overflow="bin"
/>`"
>
```
```tsx
`">
```
## Why this default [#why-this-default]
Lanes stay aligned to a shared time domain because vertical banding — several sources firing at the same instant — is
the phenomenon the chart exists to reveal. An empty lane is kept, not dropped: silence is signal. When a lane is so
dense that ticks would alias into a solid smear, it switches to per-bucket counts shown as opacity, and the summary says
which lanes were binned — the encoding change never hides. That is `overflow="bin"`, the default; `overflow="clip"` opts
out and keeps one tick per event whatever the density.
## Accessibility [#accessibility]
The accessible name summarises the field — **"3 lanes, 22 events; busiest api (11)."** The interactive entry roves lanes
with ↑/↓ and events with ←/→, announcing each event's lane, time, and position within the lane.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, events }[]` | One lane per source. |
| `emphasis` | `string` | Accents one lane — the sync read. |
| `labels` | `boolean` | Left-gutter lane names (on ≤ 8 lanes). |
| `overflow` | `"bin" \| "clip"` | Aliasing lanes bin to counts (disclosed). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# EventTimeline (/docs/charts/event-timeline)
EventTimeline answers "what happened when, and for how long?" — uptime windows, on-call shifts, release spans and
incident points on one row. Diamonds mark instants, rects mark durations: the type distinction is a shape, so it
survives 12 px where color coding wouldn't.
```tsx
`"
>
```
## Install [#install]
```tsx
import { EventTimeline } from "@microcharts/react/event-timeline";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — per-service uptime rows, on-call shifts and release windows in cards.
* **Avoid for** — more than \~12 items, or aggregated durations (MiniBar of totals).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Duration is length on a linear time axis — never log or compressed time. Items that overlap the window edge are clipped
flat (honest partial visibility); fully-outside items are excluded with a dev warning. Overlapping spans render
translucent in data order — a legibility device, not an encoding; the exact intervals live in the announcements.
## Accessibility [#accessibility]
The accessible name is the coverage read — **"2 spans covering 46% of the window; 0 events."** — where coverage merges
intervals first, so overlaps never double-count. The interactive entry cycles items chronologically: spans announce
**"Freeze: Jun 3, 01:00 to Jun 3, 05:00 — 4h."**, instants **"Incident: Jun 3, 11:00."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ start; end?; label?; kind? }[]` | Spans (with end) and point events (without), ms epoch or Date. |
| `domain` | `[start, end]` | The window — fix it across rows for small multiples. |
| `now` | `number \| Date` | Current-moment tick; authored, never implicit. |
| `label` | `"none" \| "spans"` | Centered in-span labels with deterministic drop-out. |
| `dateFormat` | `Intl.DateTimeFormatOptions \| (d: Date) => string` | (interactive) Announced timestamp format for focused events (defaults to a locale date-time). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# FatDigits (/docs/charts/fat-digits)
FatDigits answers "which numbers in this dense column are big, before I read them?" The numeral is always the exact
value; font *weight* is a second, redundant channel — five (or three) ordinal tiers mapped from your `domain` — so the
large numbers pop preattentively as you scan a column. Weight is never the primary read: it is ordinal and coarse, and
the number is right there.
```tsx
```
## Install [#install]
```tsx
import { FatDigits } from "@microcharts/react/fat-digits";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a dense numeric table column you scan for the big ones, a KPI number that should carry its own
magnitude, or an amount in a sentence.
* **Avoid for** — trends (Sparkline), proportions (Progress), or comparisons (MiniBar).
## Variants [#variants]
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — the numeral itself follows the locale's own grouping and decimal marks.
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
A non-finite value renders nothing (no numeral, no weight) and reports **"No data."** rather than guessing a tier.
## Why this default [#why-this-default]
Value mode with five tiers is the default because scanning a column is the use case, and five is the most weight steps
that stay discriminable at text size. Always pass a `domain` — without one a lone number has no tier to sit in, so it
renders at the middle weight and means nothing. `encode="digit"` instead weights each digit by its own magnitude, a
redundancy that helps scan long ids and amounts. This is adapted from the FatFonts idea: the research encodes magnitude
as glyph ink area with a custom font; shipping a font would add a dependency, so weight tiers on the inherited font
carry the ordinal instead — the numeral is always the exact value. On a font without many weights the browser snaps to
the nearest available face, so fewer of the tiers stay visually distinct.
## Accessibility [#accessibility]
The accessible name is the exact value and its tier — **"1,204 — tier 3 of 5."** — and in digit mode just the number,
since each digit carries its own weight. The interactive entry eases the weight to its new tier on variable fonts (it
snaps otherwise) with no layout shift, and announces the value and tier through a polite live region.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The number (always the exact value). |
| `domain` | `readonly [number, number]` | Maps value to a weight tier — always pass one. |
| `encode` | `"value" \| "digit"` | value weights the whole numeral; digit weights each digit by its own magnitude. |
| `tiers` | `3 \| 5` | Weight steps (default 5). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# FillWord (/docs/charts/fill-word)
FillWord answers "how far along is this named task?" by making the label *be* the bar. A muted word is overlaid with an
accent copy of itself, clipped to the value fraction of the word's own inked extent — so 50% visually bisects the word.
The fill is a percentage of the glyphs you see, never of a hidden wider track. It trades some precision for a read that
needs no separate label.
```tsx
```
## Install [#install]
```tsx
import { FillWord } from "@microcharts/react/fill-word";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a labelled progress read in a sentence or cell, a sync/upload status where the label names the task, or
a quota/TTL where the word is the metric.
* **Avoid for** — precise percentages (Progress), trends (Sparkline), or many parallel bars (MiniBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
At 0% the word is entirely the muted track; at 100% it is entirely accent ink — both ends are honest limits of the same
clip, not a special case.
```tsx
`">
```
An empty `word` renders nothing and reports **"No data."** — there is no label to be the bar.
## Why this default [#why-this-default]
Fill (not drain) is the default because progress is the ninety-percent case — a word filling as a step completes.
`mode="drain"` empties the ink from the left as the value rises, the remaining-time story that fill cannot tell (TTLs,
expiring sessions). Because glyph ink is uneven, fine reads are ±5% — the fill edge lands between letters, not on an
exact tick. When you need the number, add `label="value"` for the percent alongside, or reach for `Progress` when
precision is the point.
## Accessibility [#accessibility]
The accessible name names the task and its state — **"storage: 40% complete."** — or, in drain mode, where `value` is
the fraction already drained, **"expiring: 30% remaining."** for `value={0.7}`. An empty word reads "No data." The
interactive entry glides the ink edge with a reduced-motion-gated clip-path transition and announces changes through a
polite live region, throttled to at most once a second so a streaming value never spams.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `word` (required) | `string` | The text that is the chart. |
| `value` (required) | `number` | Fraction 0–1 (clamped). |
| `mode` | `"fill" \| "drain"` | fill grows the ink (complete); drain empties it (remaining / TTL). |
| `label` | `"none" \| "value"` | Append the percent numeral after the word. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# FoldedDayBand (/docs/charts/folded-day-band)
FoldedDayBand answers "what does a typical period look like — and is the current one typical?". It folds many days (or
weeks, or any cycle) onto a single period axis and shows the median with 25–75 and 5–95 percentile envelopes. Drop in a
`today` overlay and the chart answers the second question: is right now inside the usual band, or an outlier?
```tsx
40 + 42 _ Math.max(0, 1 - Math.abs(h - 14) / 10); const observations = Array.from({ length: 14 },
(\_d, d) => Array.from({ length: 24 }, (\_h, h) => ({ t: d _ 24 + h, value: Math.round(curve(h) + Math.sin(d + h) \* 8),
})), ).flat(); const today = Array.from({ length: 24 }, (\_h, h) => ({ t: h, value: Math.round(curve(h) + 14), }));
`"
>
```
## Install [#install]
```tsx
import { FoldedDayBand } from "@microcharts/react/folded-day-band";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — typical-day traffic or load profiles, on-call or energy capacity planning.
* **Avoid for** — a raw time series (Sparkline) or a single period with nothing to fold.
## Variants [#variants]
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the accessible summary's fold position and peak value
follow that locale's own decimal mark ("14,0" in German, not "14.0"). The band and median line themselves don't change
shape; only the announced numbers are localized.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
({ t: i, value: 7 }))}
/>`"
>
```
A single observation reports a real accessible name (median and peak both equal that one value) but has no width to fold
across, so nothing visibly paints — the chart is accessible-first even when there's too little data to draw. Identical
values across every bin still render as a flat line, distinct from an empty `data` array, which renders nothing and
announces "No data."
## Why this default [#why-this-default]
Two envelopes plus a median is the typical-day grammar that clinical glucose profiles standardized on. The envelopes
come from real per-bin quantiles — never smoothed across bins into a shape the data doesn't support — and the outer
boundary fades so 5–95 doesn't read as a hard limit. When a bin has too few observations the band collapses to the
median there rather than inventing a width.
## Accessibility [#accessibility]
The accessible name reports the peak — **"Median peaks at 14 (82.5)."** The interactive entry roves the folded axis with
←/→, announcing the median and middle-half at each position.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ t, value }[]` | Raw observations across many periods. |
| `period` | `number` | Fold length (168 folds a week). |
| `today` | `{ t, value }[]` | The current period overlaid. |
| `percentiles` | `[number, number][]` | Percentile pairs, outermost last. |
| `bins` | `number` | Fold-axis resolution (default 24). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ForecastCone (/docs/charts/forecast-cone)
ForecastCone answers "will we land where we need to?". History is a solid line; the forecast is a fan of prediction
bands that **widen over the horizon**, with a **dashed** median — because an estimate should never render as fact. The
whole point of a fan chart is visible confidence decay, so three rules are not style options: at most two bands (50/80 —
a 95% band reads as false tail confidence at micro scale), the median is always dashed, and a cone that fails to widen
is flagged, never quietly inflated.
```tsx
```
## Install [#install]
```tsx
import { ForecastCone } from "@microcharts/react/forecast-cone";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a "will we hit Q4?" forecast in a KPI card, a projection with honest uncertainty in a sentence,
band-vs-target landing reads.
* **Avoid for** — a forecast with no uncertainty (Sparkline) or one estimate's spread (GradedBand).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the in-chart landing label and the accessible
summary's median, interval, and target numbers all follow that locale's own grouping and currency placement (the label
above reads "4.200 €", not "€4,200").
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
With no history the cone still draws from the first forecast point, and the accessible summary quietly drops its "from N
today" clause instead of naming a value that doesn't exist. A cone whose bands narrow instead of widen is an
input-honesty failure — it renders exactly as supplied (never auto-inflated to look right) and logs a one-time dev
warning so the mistake surfaces in development rather than silently misleading a reader.
## Why this default [#why-this-default]
50/80 bands, not 95, because micro-scale tails invite overreading — a 95% band paints false confidence about the
extremes. The median is dashed so it never reads as a committed number, and the cone must visibly widen: if the supplied
bands don't (they narrow, or stay flat), that is an input-honesty failure the component surfaces rather than papering
over — a forecast whose uncertainty doesn't grow with the horizon is misrepresenting confidence decay.
## Accessibility [#accessibility]
The accessible name states the median, the horizon interval, today's actual, and — with a target — whether the band
clears it: **"Median forecast 42 by week 11 (80% between 33 and 55), from 38 today. The 80% band straddles the 50
target."**. The interactive entry is region-aware: history points announce a value, forecast points announce the median
and 80% interval.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Historical actuals. |
| `forecast` (required) | `{ mid: number[]; p80: [lo,hi][]; p50?: [lo,hi][] }` | Median + prediction bands (at most 2: 50/80). |
| `target` | `number` | The landing reference the cone must clear (adds a clearance clause). |
| `unit` | `string` | Period noun for the summary (default "week"). |
| `label` | `"landing" \| "none"` | Median endpoint value in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Funnel (/docs/charts/funnel)
Funnel answers "where does the pipeline leak?". Stepped rectangles, zero-anchored — never the classic tapered
silhouette, which interpolates data that doesn't exist. The drop *between* neighbors is the read.
```tsx
```
## Install [#install]
```tsx
import { Funnel } from "@microcharts/react/funnel";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — per-campaign funnels in table cells, conversion bridges in cards.
* **Avoid for** — unordered categories (MiniBar) or more than 6 stages.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
`rate` always normalizes to stage 1, never to the previous stage — per-stage rates hide compounding loss. A stage larger
than its predecessor (re-engagement funnels) renders truthfully and the summary appends the inversion. The connectors
and zero-anchored columns keep the read a measurement, not a shape.
## Accessibility [#accessibility]
The accessible name is the conversion story — **"4 stages, 12,400 to 1,116 — overall 9%."**, plus **"Stage 3 exceeds
stage 2."** when a pipeline inverts. The interactive entry roves stages (**"Activated: 2,730 — 22% of Visitors."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Ordered stages. |
| `mode` | `"absolute" \| "rate"` | Rate = % of the FIRST stage (never the previous). |
| `connectors` | `boolean` | Retained-share slats between stages. |
| `label` | `"none" \| "percent" \| "value"` | Above each column (deterministic drop-out; default percent). |
| `highlight` | `number \| string` | Accent the leak stage. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# GardenGrid (/docs/charts/garden-grid)
GardenGrid answers "what's the rhythm of activity over time?" the way ActivityGrid does — but with a single ink. Instead
of color, dot **area** carries a five-step ordinal, so the rhythm reads in grayscale and print where a color heatmap
would collapse. The radius is √-quantized so perceived area steps evenly. A zero cell is a hairline ring (present, but
quiet); a `null` cell is nothing at all, because "missing" is not "zero".
```tsx
```
## Install [#install]
```tsx
import { GardenGrid } from "@microcharts/react/garden-grid";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a contribution or activity rhythm you print or read in grayscale, a per-repo or per-team activity
strip, or any calendar-shaped intensity where color isn't available.
* **Avoid for** — exact per-cell values (ActivityGrid with hover, or HeatStrip) or trends (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Rings for zero are the default because print and grayscale lose the zero-versus-missing distinction that color grids get
for free — this chart exists for exactly that medium, so it makes "present but quiet" visible. The radius is √-quantized
so **area** carries the ordinal; a linear radius map would exaggerate the highs quadratically. The steps are ordinal,
not values — the interactive entry announces a cell's step rather than implying a precise read, and `ActivityGrid` is
the color twin when exact values matter.
## Accessibility [#accessibility]
The accessible name summarizes the rhythm — **"12 periods; peak 34, 10 active."** The interactive entry walks the grid
in 2-D with the arrow keys (or hover), announcing each cell as its ordinal step — **"3 of 21: 8, step 2 of 5."** — never
a false-precise value, since dot area reads to a step, not a number.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | Binned values; null = missing. |
| `rows` | `number` | Grid rows (default 7); 1 = strip. |
| `steps` | `3 \| 5` | Radius quantization steps (default 5). |
| `empty` | `"outline" \| "blank"` | How zero cells render (default outline). |
| `unit` | `string` | Noun for the summary count (default "periods"). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# GradeProfile (/docs/charts/grade-profile)
GradeProfile answers "how hard is this route, and where does it bite?" Each segment between two points is filled from
the baseline and coloured by its grade — gentle to brutal — with the elevation ridge riding on top. The steepest pitch
is called out, so the wall in the route is the first thing you see.
**How to read it** — colour is a *quantized* grade bin, never a smooth ramp: flats and every descent take the faint
band, then a mid tone, the negative colour, and the darkest fill for the brutal pitches. Height is the elevation ridge
for shape; the encoded channel is the colour, and the interactive readout gives the exact grade.
```tsx
```
`d` and `elev` must share a unit (both metres, say) so that grade — rise ÷ run — is a true percent. Distance need only
be monotonic; the values themselves can be any scale.
## Install [#install]
```tsx
import { GradeProfile } from "@microcharts/react/grade-profile";
`${n} m`} title="Queen stage" />
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — route and climb profiles (cycling, running, hiking); showing *where* the hard pitches fall.
* **Avoid for** — a single elevation series (Sparkline), or an unordered track (distance must be monotonic).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
`format` accepts `Intl.NumberFormatOptions` (with a `locale`) or a formatting function, and applies to **distance and
elevation** numbers in the summary and readout — not grade. Grades are always percent-encoded; the locale still decides
the decimal mark: the alpine profile above reads **"12.000, 1.250 gain; steepest 12,3% at 3.500."** in German.
## Edge cases [#edge-cases]
```tsx
\`\${n} m\`} title="Descent" />`"
>
```
```tsx
`"
>
```
A descent-only route has no climb to grade, so no pitch is emphasised and the summary reports **"600 m, no real
climb."** — climbing difficulty is the whole story, so descents always take the gentlest colour rather than borrowing a
climb's. A non-finite elevation breaks the profile into a gap: the segments touching it drop out and the ridge splits,
so a missing reading never invents a grade. A flat route reports the "no real climb" sentence too, and a single point
has no segment to grade at all, so its accessible name falls back to **"No data."**
## Why this default [#why-this-default]
Two decisions carry the chart. Grade is **quantized** into bins, not drawn as a continuous colour ramp — a route reads
as "gentle / rolling / steep / wall", categories a rider can act on, instead of a gradient nobody can decode at a
glance. And the default `bins` of `[3, 6, 10]` percent match how climbs are actually described, with the steepest pitch
labelled so the hardest moment is never buried. Below 72 pixels wide the four bins collapse to a simple
flat-versus-climb read, because the finer classes stop being separable there.
## Accessibility [#accessibility]
The accessible name states the route's shape in words — **"900, 67 gain; steepest 16% at 800."** — distance, total
climb, and where the hardest pitch falls, none of it dependent on colour. The interactive entry announces each pitch as
you rove it (`←`/`→`), giving the true grade and the cumulative climb, so the encoding never has to be learned to be
used.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ d: number; elev: number }[]` | Distance + elevation, monotonic in d, same unit so grade is a true percent. |
| `bins` | `[number, number, number]` | Ascending grade-% thresholds (always percent) that quantize the four difficulty bins. |
| `format` | `Intl.NumberFormatOptions \| (n) => string` | Formats distance and elevation in the summary and readout; grades always render as percent. |
| `label` | `"max" \| "none"` | Mark the steepest pitch, or render the profile alone. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# GradedBand (/docs/charts/graded-band)
GradedBand answers "how sure are we about this one number?". Nested central intervals are graded by opacity, with a
median tick. It is **never** a bar from zero — a bar with an error bar induces edge-literalism bias, so this form exists
precisely to avoid it. Opacity maps to probability level and nothing else.
```tsx
```
## Install [#install]
```tsx
import { GradedBand } from "@microcharts/react/graded-band";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a forecast with its uncertainty, estimate-vs-actual in a KPI card, posterior summaries.
* **Avoid for** — countable odds (QuantileDots) or a forecast over time (ForecastCone).
## Variants [#variants]
```tsx
21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3)),
);
`"
>
```
```tsx
(21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3))) / 10,
);
`"
>
```
With a `locale`, the median label and the accessible summary follow that locale's decimal mark — the label above renders
"2,2", not "2.2".
## Edge cases [#edge-cases]
```tsx
`"
>
```
The accessible name for that strip is **"Point value 42, no interval."** — a degenerate sample is announced as a point
estimate, never dressed up as certainty about a range.
```tsx
`"
>
```
## Why this default [#why-this-default]
Nested central intervals graded by opacity — never a bar from zero. A bar with an error whisker invites reading the
bar's end as "the answer" and the whisker as noise; the graded band has no endpoint to over-read, so opacity maps to
probability and nothing else. Inner intervals are clipped inside their outer, so quantile rounding can never invert the
nesting.
## Accessibility [#accessibility]
The accessible name states the median and the innermost + outermost intervals — for the forecast above that's **"Median
22; 50% within 15–27, 95% within 8–36."** The interactive entry roves the band edges and announces each interval's
coverage and bounds.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Sample / posterior draws; the component derives the intervals. |
| `levels` | `number[]` | 1–3 nested central intervals (default [50, 80, 95]). |
| `value` | `number` | Observed value overlaid as a dot. |
| `softEdge` | `boolean` | Fade past the outer band — 'this is approximate'. |
| `label` | `"median" \| "none"` | States the median in a right gutter (default none). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# HeartbeatBlip (/docs/charts/heartbeat-blip)
HeartbeatBlip answers "is it alive, and how busy?" at a glance. Each event in the recent window is an ECG-style spike;
in the interactive entry the trace sweeps left in real time and a new event blips in at the right edge, so the rate you
see **is** the event rate. Every spike is one real event — never a synthesized pulse on a timer — and an empty, flat
baseline is the down signal, carried by shape, not color.
```tsx
```
## Install [#install]
```tsx
import { HeartbeatBlip } from "@microcharts/react/heartbeat-blip";
// pass 'now' from your data layer — never Date.now() in a server render
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## Motion, and reduced motion [#motion-and-reduced-motion]
The trace advances in real time so old spikes drift left and new events enter at the right — the liveness comes from
watching the blips arrive. This is the deliberate idle-loop exception, allowed because the loop parameter (elapsed time)
is the datum. It pauses off-screen (a shared observer) and, under `prefers-reduced-motion`, does not sweep at all: the
static strip simply re-renders on each data change — the same information, discretely updated. The one rule this chart
must never break: **a flat service never gets a fake pulse.** Every spike is a real event; when the window empties, the
baseline goes flat and stays flat.
## When to use it [#when-to-use-it]
* **Good for** — at-a-glance liveness of a service or stream, request rate in a header, or per-service liveness in a
status table.
* **Avoid for** — exact event counts (`Seismogram`, `EventTimeline`), a continuous level (`BreathingDot`), or long-term
trends (`Sparkline`).
## Sizing [#sizing]
`width` and `height` are viewBox units that also set the rendered pixel box — they default to `60 × 16`, sized to sit in
a status table row or a header. Omit them and drive the width from CSS to fill a column; the viewBox keeps the aspect
ratio. `window` sets how much time the strip covers, which changes the density of the trace rather than its box.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
`format`/`locale` only reach the `label="count"` numeral — the accessible summary's count is a plain integer (never run
through the formatter), and the in-chart spikes never carry text, so there's nothing else to localize.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The 60-second window is the default because liveness questions are about the last minute. The `now` clock is a
required-in-spirit prop: the static entry must never call `Date.now()` (a server render and the client hydrate would
disagree and mismatch), so you pass the clock from your data layer. Flatline is never dressed up — down and no-data are
different states, and the summary distinguishes them. The boundary with `BreathingDot`: discrete arriving events are a
HeartbeatBlip; a continuous level is a BreathingDot.
## Accessibility [#accessibility]
The accessible name is the count, the window, and the time since the last event — **"3 events in the last minute; last
3s ago."** — or **"No events in the last minute."** when the trace is flat. The trace sweeps only when motion is allowed
and on-screen; the live region announces on data change, and there is no per-spike navigation because the spikes are
transient — the summary is the record.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `events` (required) | `number[]` | Event timestamps (ms). |
| `window` | `number` | The visible recent window in ms (default 60000). |
| `now` | `number` | Explicit clock — defaults to the latest event (SSR-safe). |
| `label` | `"count" \| "none"` | Event-count numeral at the right. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# HeatCell (/docs/charts/heat-cell)
HeatCell answers "how intense is this one value against a known scale?". It is the single-cell building block for grids
you lay out yourself — a matrix of tenants × hours, a custom calendar, a density chip in a sentence. Color is quantized
into discrete steps: continuous opacity would fake precision a 12-px cell can't deliver.
```tsx
{[12, 35, 58, 79, 96].map((v) => (
))}
```
## Install [#install]
```tsx
import { HeatCell } from "@microcharts/react/heat-cell";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — table-cell matrices, custom grids, intensity chips beside labels.
* **Avoid for** — precise comparison (MiniBar, DotPlot) or time series (HeatStrip, ActivityGrid).
## The shared-domain rule [#the-shared-domain-rule]
A lone cell has no data to auto-scale from, so `domain` defaults to `[0, 1]` — pass your grid's real scale. **Every cell
in one grid must share one domain**: per-cell auto-scaling would make the brightest hour of a quiet tenant look like the
brightest hour of a loud one.
```tsx
// one domain, computed once, shared by every cell
const domain: [number, number] = [0, Math.max(...allValues)];
rows.map((r) => r.hours.map((v) => ));
```
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Five steps is the most a reader reliably distinguishes at cell size — the same ramp ActivityGrid uses, so intensity
means one thing across the library. Values outside the domain clamp to the end steps (documented, never silently
rescaled), and a zero-width domain renders the single mid step with a dev warning.
## Accessibility [#accessibility]
The accessible name carries the value *and* its calibration — the shape cells above read **"70 — level 4 of 5."** — so
the color scale is never the only channel. Non-finite input renders a designed empty track and says **"No data."** The
interactive entry reveals the same reading on hover/focus, with ActivityGrid announcement parity.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The value to calibrate. |
| `domain` | `[number, number]` | Calibration scale — defaults to [0, 1]; every cell in a grid must share one. |
| `steps` | `number` | Discrete perceptual steps (default 5, shared with ActivityGrid). |
| `shape` | `"square" \| "round" \| "dot"` | Shared cell vocabulary. |
| `label` | `"value" \| "none"` | Centered number when the cell doubles as a chip. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# HeatStrip (/docs/charts/heat-strip)
HeatStrip answers "how did intensity evolve, glanceably?". It is the 1×N sibling of ActivityGrid: the same discrete step
scale, the same cell vocabulary, laid on a single timeline. Empty is not zero — a slot with no record renders a hairline
outline, visibly different from a low value.
```tsx
```
## Install [#install]
```tsx
import { HeatStrip } from "@microcharts/react/heat-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — per-tenant load rows, intensity ribbons in dense tables.
* **Avoid for** — exact shape (Sparkline) or weekday rhythm (ActivityGrid).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
## Why this default [#why-this-default]
Square cells with a density-adaptive gap keep boundaries legible where round shapes blur at 10 px. Every real value
renders at a visible opacity (0.25–1); the faint track look is reserved for truly-empty slots. Three ways a heat strip
lies — continuous ramps, per-row autoscale, mean-based downsampling — are all closed: discrete steps, shared `domain`,
max-per-bucket only.
## Accessibility [#accessibility]
The summary reuses `describeSeries` verbatim — for the load strip above that's **"Trending up 383%. Range 12 to 88. Last
value 58."** — so a color ramp is never the only channel. The interactive entry roves cells with ActivityGrid-parity
announcements (**"Point 8 of 20: 90."**, empty slots as "no data").
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | Time-ordered values; null = no record (≠ zero). |
| `steps` | `number` | Shared step-scale granularity (default 5). |
| `shape` | `"square" \| "round" \| "dot"` | Shared cell vocabulary. |
| `domain` | `[number, number]` | Cross-row calibration — share one domain per table. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# HistogramStrip (/docs/charts/histogram-strip)
HistogramStrip answers "what does the distribution look like?". Raw observations in, ≤ 12 uniform bins out — counts
zero-anchored, never density-smoothed. Pre-aggregated counts are not supported; that's SparkBar's contract.
```tsx
```
## Install [#install]
```tsx
import { HistogramStrip } from "@microcharts/react/histogram-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — latency clusters in a sentence, distributions per table row.
* **Avoid for** — raw marks (RugStrip) or series over time (Sparkline).
## Variants [#variants]
```tsx
i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60),
);
`"
>
```
```tsx
`"
>
```
The accessible summary states the modal bin's edges through `format`/`locale` — under `de-DE` a value like 24000 reads
"24.000" (period as the thousands separator) instead of the English "24,000".
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
An empty series renders zero bars and an accessible name of "No data." rather than a misleading flat line. A constant
series collapses to ONE full-height bin instead of the usual up-to-12 slivers a naive equal-width binner would draw for
a zero-span domain.
## Why this default [#why-this-default]
Auto bin count is √n capped at 12 — enough shape to see skew, few enough bars to survive 60 px. All values identical
collapse to ONE bin (never twelve slivers); explicit bin counts collapse to the observation count. `markValue` marks the
bin of a value and never re-bins around it.
## Accessibility [#accessibility]
The accessible name names the modal bin — **"120 values, most between 42.09 and 47.36."** The interactive entry roves
bins with range announcements (**"42.09 to 47.36: 26 values."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw observations. |
| `bins` | `number` | Bin count; auto = min(12, √n). |
| `markValue` | `number` | A VALUE whose bin gets accent. |
| `domain` | `[number, number]` | Fixed bin edges across multiples. |
| `format` | `Intl.NumberFormatOptions \| fn` | Formats the bin edges named in the summary. |
| `locale` | `string \| string[]` | BCP 47 locale(s) for the formatted bin edges. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Honeycomb (/docs/charts/honeycomb)
Honeycomb answers "how many of the available slots are taken?" as filled cells in an area-filling hex grid. The unit is
the cell, so the count is genuinely countable — this is occupancy of a capacity, not a magnitude. It fills row-major
from the top-left, so occupancy reads as a sweep, and the whole grid is exactly two SVG paths (filled and empty) no
matter how large the total.
```tsx
```
## Install [#install]
```tsx
import { Honeycomb } from "@microcharts/react/honeycomb";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — seats or licenses taken of a capacity, an occupancy read in a KPI card, or a countable of-total in a
cell.
* **Avoid for** — a capacity over about sixty (that's `Progress`), a magnitude with no total (MiniBar), or trends.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Outline empties are the default because takenness must survive grayscale — a filled cell versus an outlined cell reads
without color. `empty="blank"` drops the empty cells entirely instead of dimming them, for surfaces where the outline
reads as noise. Auto packing is the default because a near-square honeycomb is the recognizable form. The unit is the
cell, and its size never changes with value — the count varies, the geometry doesn't — which is what keeps this honest
area-filling *occupancy*, distinct from a `PictogramRow` counting unlike things. Above sixty cells, unit counting stops
being countable, so the chart emits a dev warning and the docs steer to `Progress`. A value past the total fills every
cell, but the accessible name still states the true number — occupancy is never silently clipped.
## Accessibility [#accessibility]
The accessible name is the exact occupancy — **"45 of 40 seats filled."** — always the true value, even when it exceeds
the total. The interactive entry announces the count on change through a polite live region and reveals the value /
total on hover. Focus it and the arrow keys rove cell by cell (←/→ within a row, ↑/↓ holding the column), each cell
announced as **"Cell 7 of 40 — filled."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Filled count (fractional rounds). |
| `total` | `number` | Capacity = cell count (default 10). |
| `rows` | `number \| "auto"` | auto (near-square) or a number; 1 = strip. |
| `empty` | `"outline" \| "blank"` | How empty cells render (default outline). |
| `unit` | `string` | Noun for the summary (e.g. "seats"). |
| `label` | `"none" \| "count" \| "percent"` | Centered readout when the comb has room (default "none"). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Horizon (/docs/charts/horizon)
Horizon answers "what happened across this wide-range series — in a 14-pixel row?" The series is cut into bands and
folded: layer opacity carries magnitude, so extremes stay visible at heights where a sparkline would flatten into noise.
**How to read it** — every band rises from the row's bottom edge, positive or negative; darker means farther from the
baseline, not which direction. Above-baseline values shade in the accent color, below-baseline in the negative color, so
direction is never color-alone even though both fold upward the same way.
```tsx
```
## Install [#install]
```tsx
import { Horizon } from "@microcharts/react/horizon";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — dense monitoring rows (dozens stacked), wide-range series in tight cells.
* **Avoid for** — first-glance audiences (folding needs a key), or a few rows with room (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the accessible summary's range and last-value numbers
follow that locale's own grouping ("4.500" in German, not "4,500"). The interactive readout's per-point values localize
the same way; the folded geometry never changes.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
Negative values take the negative token and fold from the same bottom edge as positive values (the default
`mode="mirror"`) — both directions get darker as they move away from the baseline, so "denser" always means "farther
from typical," never "which sign." Identical values across the whole row still render as a solid block rather than
vanishing, keeping "flat" visibly distinct from "no data."
## Why this default [#why-this-default]
Two folds is the default because it needs no training to read approximately; three folds trade learnability for density
and should be reserved for ranges that genuinely span them. The `baseline` is authored, never inferred — a fold origin
is a claim about what "normal" means.
## Accessibility [#accessibility]
The accessible name reads the unfolded series — **"Trending up 900%. Range 2 to 45. Last value 20."** — so screen-reader
users get the true values, not the folded geometry. The interactive readout announces unfolded values per point.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | Series over time. |
| `folds` | `2 \| 3` | Band count — 3 only when the range genuinely spans it. |
| `mode` | `"mirror" \| "offset"` | Mirror flips negatives upward (denser); offset keeps up/down. |
| `baseline` | `number` | Fold origin (e.g. a target level) — authored, never inferred. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Hourglass (/docs/charts/hourglass)
Hourglass answers "how much time is gone *and* how much remains?" — the two-sided story a progress bar can't tell. Sand
fills the top chamber (remaining) and the bottom (elapsed), and both areas are **area-true**: a naive linear-height fill
in a triangular bulb would overstate early progress by up to 2×, so the geometry solves for true proportional area. A
thin stream at the neck marks "running"; it disappears at both ends, because finished and not-started are shape-distinct
states.
```tsx
```
## Install [#install]
```tsx
import { Hourglass } from "@microcharts/react/hourglass";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a deadline or session-expiry read in a sentence, a TTL cell where remaining is the story, or a
time-boxed tab or countdown.
* **Avoid for** — exact percentages (Progress), trends (Sparkline), or non-time fractions.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
## Why this default [#why-this-default]
`value` is the elapsed fraction — the same polarity as `Progress` — so the two compose in one product without
re-learning which way is which. The area-true solve is the load-bearing honesty rule: in an inherently nonlinear
container, the sand areas are exactly proportional to elapsed and remaining. The stream is a binary state mark, not
decoration, and is never animated in the static entry — it simply renders while `0 < value < 1`.
## Accessibility [#accessibility]
The accessible name carries both sides — a glyph above reads **"70% elapsed, 30% remaining."** The interactive entry
cross-fades the sand levels on change (opacity plus a scale-from-the-floor settle, never a path interpolation) and
announces only when the value crosses a documented threshold (50 / 90 / 100%), so a streaming value never spams.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Elapsed fraction 0–1 (like Progress). |
| `stream` | `boolean` | The running-sand cue (default true). |
| `label` | `"none" \| "remaining" \| "elapsed"` | Print the percent that matters to the context. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Hypnogram (/docs/charts/hypnogram)
Hypnogram answers "which discrete state was the system in over time, and how choppy were the transitions?". It refuses
interpolation — there are no diagonals, ever — because a state is a fact that holds until the next one, not a sample of
a continuum a line chart can slope between.
```tsx
`"
>
```
## Install [#install]
```tsx
import { Hypnogram } from "@microcharts/react/hypnogram";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — sleep stages, deploy / machine / incident state over time.
* **Avoid for** — continuous signals (Sparkline) or a single current state (StatusDot).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
A single entry holds its state across the whole domain — one flat run, and the summary names it directly — **"1 state,
no transitions; Deep throughout."** — rather than describing a one-point chart. A state present in the data but missing
from an explicit `states` order is never dropped: it's appended as its own row (with a dev-only console warning), so the
strip always accounts for every state it's given.
## Why this default [#why-this-default]
Right-angle steps with an explicit `states` order are the clinically proven read. Order is meaningful — for ordinal
states (sleep depth, incident severity) pass it explicitly; for nominal states with no rank, use `mode="lanes"` so the
vertical axis never implies one. Any smoothing or easing of the step corners would be a lie about the data, so the
entrance is a left-to-right clip reveal of the finished trace — the steps themselves are never interpolated.
## Accessibility [#accessibility]
The accessible name summarises the shape — **"3 transitions across 4 states; longest run Awake."** The interactive entry
roves the runs; each announces its state and span (**"Light, from 8 to 22."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ t, state }[]` | State holds from t to the next entry. |
| `states` | `string[]` | Row order top→bottom; ordinal semantics live here. |
| `emphasis` | `string` | Accents one state — the decision read. |
| `mode` | `"steps" \| "lanes"` | Lanes for nominal states with no rank. |
| `connectors` | `boolean` | Vertical transition strokes (default true); off for ultra-dense strips. |
| `labels` | `boolean` | Left-gutter state names (default: on when width ≥ 96). |
| `colors` | `string[]` | Per-state lane colours (lanes mode), cycled; overrides --mc-cat-N. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# IconArray (/docs/charts/icon-array)
IconArray answers "how likely is this, really?". A stated rate becomes countable: filled units in a fixed N-unit grid,
with the denominator visible. Two moves kill denominator neglect — the ratio label and the fixed grid. Fill order is
contiguous reading-order (scattered is harder to count), and there are **no partial-unit fills ever**: a sub-unit rate
is flagged, never faked.
```tsx
```
## Install [#install]
```tsx
import { IconArray } from "@microcharts/react/icon-array";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — risk in a sentence, uptake / adoption rates, lay-audience probabilities.
* **Avoid for** — a trend (Sparkline) or a full distribution (QuantileDots).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
A rate of exactly 0 draws every unit hollow. A rate that is real but rounds to 0 whole units (`note: "sub"`) still shows
a hollow grid but the summary says so explicitly — never a fractional unit standing in for "almost none." `value` is
clamped to `[0, 1]` and non-finite input renders as 0, so a bad upstream number never breaks the grid.
## Why this default [#why-this-default]
`total={20}` and `label="ratio"` are the default pairing because "3 in 20" reads faster and more honestly for a lay
audience than "15%" — the denominator stays visible instead of being computed in the reader's head. Units fill in
reading order from the top-left, never scattered: medical-risk-communication research finds scattered fills measurably
harder to count at a glance, and counting is the whole point of this chart. There is no partial-unit fill — a 37% rate
is never drawn as a unit that's 37% full, because a sub-pixel sliver of color is a precision claim this chart doesn't
make.
## Accessibility [#accessibility]
The accessible name states the count and the rate — **"3 in 20. About 15%."** — and degenerate rates say so plainly ("0
in 20", "20 in 20 — all."). The interactive entry roves the grid in reading order with the arrow keys (2-D, row-major)
and announces each unit's state plus the running count: **"Unit 1 of 20 — filled. 3 of 20 filled."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The rate, 0–1. |
| `total` | `10 \| 20 \| 100` | Denominator / grid size (default 20). |
| `label` | `"ratio" \| "percent" \| "none"` | "3 in 20" (default) reads better than "15%" for lay audiences. |
| `shape` | `"square" \| "round" \| "dot"` | Shared cell vocabulary (default square). |
| `positive` | `"up" \| "down"` | Polarity — down (fewer is better) flips the fill to the risk tone. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# All charts (/docs/charts)
One grammar runs through all of them: `data` alone always renders something correct, and a prop name means the same
thing on every chart. So you don't pick by chart — you pick by the **decision** you need read at a glance. The chooser
below is filed exactly that way: start from the question, land on the chart.
- [Browse all charts](/charts)
## Static and interactive [#static-and-interactive]
Nearly every chart ships two entries — a hook-free **static** default (server-component safe, zero client JavaScript)
and an **interactive** twin from a `/interactive` subpath. WindBarb is the lone static-only exception. Every chart page
lists the import paths it has; the [Quickstart](/docs/quickstart#add-interactivity) covers when to reach for which, and
[Accessibility](/docs/accessibility#one-interaction-contract) documents the one interaction contract they all share.
# LikertStrip (/docs/charts/likert-strip)
LikertStrip answers "does the response lean agree or disagree — and how hard?" — the valence + balance read that
SegmentedBar (unvalenced composition) cannot say. The center line is the question; everything else is the answer.
```tsx
```
## Install [#install]
```tsx
import { LikertStrip } from "@microcharts/react/likert-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — survey question rows (share one scale via SparkGroup), sentiment in cards.
* **Avoid for** — more than 7 levels, or exact per-level values (MiniBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
Empty data, or data whose values all resolve to 0, has nothing to divide into a diverging read — no bar draws, and the
accessible name says so plainly: **"No responses."** When every non-zero response lands on the neutral level, the bar is
entirely the center segment and the summary reads **"All responses neutral."** instead of forcing a lean out of no
signal. Negative counts are treated as 0 rather than pushed the wrong direction across the center line — a chart that
lies about which side a count is on is worse than one that drops it — and the static entry logs a dev warning when it
happens.
## Why this default [#why-this-default]
Both neutral conventions are supported honestly: `split` straddles the center (the canonical placement), `omit` removes
neutral from the bar for cleaner pole comparison — but its share is ALWAYS still counted in the total and spoken in the
accessible summary; neutral is never silently dropped. Graded opacity encodes ordinal distance from neutral, never
magnitude.
## Accessibility [#accessibility]
The accessible name is the full valence read — **"62% agree, 24% disagree, 14% neutral. Leans positive."** A |net| under
5 points reads "Balanced." The interactive entry steps levels in data order (**"Disagree: 14%, level 2 of 5."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Ordinal levels, negative → positive. |
| `neutral` | `"split" \| "omit"` | Center-straddle or omit-from-bar (always labeled). |
| `label` | `"ends" \| "net" \| "none"` | Agree/disagree % or one signed score. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MicroBox (/docs/charts/micro-box)
MicroBox answers "what are the p50 and spread of this metric?" — the five-number summary in a table row. Min-max
whiskers by default (Tukey fences imply a normality assumption most product data doesn't meet); `tukey` mode exposes
fence-breakers as dots.
```tsx
```
## Install [#install]
```tsx
import { MicroBox } from "@microcharts/react/micro-box";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — latency percentile rows (p50/p95/p99), spread beside a stat — precomputed `stats` is the production
path.
* **Avoid for** — distribution shape (HistogramStrip) or fewer than 5 observations (renders honest dots).
## Variants [#variants]
```tsx
40 + i), 400, 500];
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Never a box from fewer than 5 observations — dots at the raw values instead. Precomputed `stats` with non-monotonic
values are refused with a dev warning: garbage in must not render a plausible-looking lie. Outlier dots cap at 3 per
side (the furthest).
## Accessibility [#accessibility]
The accessible name is the five-number reading — **"Median 50.5, middle half 45.25 to 55.75, range 40 to 500."** The
interactive entry roves a fixed 5-stop model (min → Q1 → median → Q3 → max), announcing each stat (**"Median: 42."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` | `number[]` | Raw observations (exclusive with stats). |
| `stats` | `{ min; q1; median; q3; max }` | Precomputed server aggregates. |
| `whiskers` | `"minmax" \| "tukey"` | Tukey exposes outliers as dots. |
| `outliers` | `boolean` | Render outlier dots in tukey mode. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MicroDonut (/docs/charts/micro-donut)
MicroDonut answers "roughly what is this made of?" at icon size — an honest, capped concession to a ubiquitous demand.
For any comparative read, use [SegmentedBar](/docs/charts/segmented-bar): a flat bar beats a donut of the same data at
every size we ship. The wedge cap and labeled rollup are non-optional.
```tsx
```
## Install [#install]
```tsx
import { MicroDonut } from "@microcharts/react/micro-donut";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a mix icon beside the printed headline number in a KPI card.
* **Avoid for** — any comparative read (SegmentedBar) — the docs steer there first.
## Variants [#variants]
```tsx
62% Chrome `"
>
{"62% Chrome "}
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
The hole is mandatory — the donut's angle + arc-length double encoding beats the pie's area read, which is why pie stays
unshipped. Never exploded, tilted, or shadowed. A single category renders a full annulus that looks like a 100%
ProgressRing — the summary disambiguates.
## Accessibility [#accessibility]
The accessible name is the full composition (same wording as SegmentedBar). `decorative` marks the chart aria-hidden for
the one sanctioned ornamental use — beside a printed value — and the interactive entry refuses to make a decorative
donut a tab stop.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Parts of the whole. |
| `maxWedges` | `number` | Rollup threshold (default 4). |
| `decorative` | `boolean` | Redundant ornament beside a printed value → aria-hidden. |
| `weight` | `number` | Annulus thickness (shared with ProgressRing). |
| `label` | `"none" \| "total"` | Center total when the hole has room (default "none"). |
| `colors` | `string[]` | Per-wedge colours, cycled; overrides --mc-cat-N. Other stays neutral. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MicroScatter (/docs/charts/micro-scatter)
MicroScatter answers "are these two variables related?" — the relationship story no other micro chart tells. Dots render
at 75% opacity so overplot reads as density instead of lying by occlusion, and duplicates are never jittered: position
is the encoding.
```tsx
{"Latency and error rate "}
{" correlate strongly."}
```
## Install [#install]
```tsx
import { MicroScatter } from "@microcharts/react/micro-scatter";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — correlation in a sentence, two-metric relationships in KPI cards.
* **Avoid for** — more than 60 points (bin instead) or ordered time series (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
Empty data draws just the frame with "No data." as the summary. Under 3 points (or a zero-variance cloud) states the
count and stops — no correlation claim without enough evidence to back it. Coincident points are never jittered apart:
they overlap exactly, and at 75% dot opacity the overlap itself reads as extra density rather than hiding a point.
## Why this default [#why-this-default]
Axes are unlabeled at this scale, so `title` must name both variables, the way the hero demo above does. The summary's
relationship words are a documented heuristic on |r| (≥ 0.7 strong, ≥ 0.4 moderate, ≥ 0.2 weak), and whenever a
relationship word appears, r appears beside it: claim and evidence travel together. Fewer than 3 points, or a
zero-variance cloud, makes no claim at all.
## Accessibility [#accessibility]
The accessible name is the count plus the evidence-backed relationship — **"24 points. Strong positive relationship (r
0.93)."** — or just **"2 points."** when no claim is honest. The interactive entry steps points ordered by x and
announces each formatted pair.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ x; y }[]` | Unordered pairs. |
| `trend` | `boolean` | Least-squares line — linear only, never smoothed. |
| `focal` | `number` | Accent one point — "this one, among all of them". |
| `xDomain` | `[number, number]` | X scale (domain keeps its grammar meaning: y). |
| `domain` | `[number, number]` | Y scale — the shared grammar name, paired with xDomain. |
| `r` | `number` | Dot radius, clamped [1, 3]. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MiniBar (/docs/charts/mini-bar)
MiniBar answers "which category is biggest, and by roughly how much?". Bars are always zero-anchored, and the data's own
order is the default truth — weekday order or funnel order carries meaning that ranking would destroy, so `order` is
explicit, never silent.
```tsx
```
## Install [#install]
```tsx
import { MiniBar } from "@microcharts/react/mini-bar";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — per-row category mix in tables, small comparisons in KPI cards.
* **Avoid for** — more than 8 categories (a full bar chart) or time series (SparkBar).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
The extremes named in the summary follow `format`/`locale` — 12400 reads "12.400" under `de-DE` rather than the English
"12,400".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
A `null` value draws no bar but keeps its band, so the two real bars around it stay exactly as far apart as a fully
populated row — alignment across a column of MiniBars survives missing data. A single category still renders and its
summary says so plainly ("1 category…") rather than degenerating into a bare-bar special case. Past 8 categories the
component still renders every bar (nothing is dropped or truncated) but logs a dev warning — MiniBar is a cell chart,
and a wider comparison belongs in a full bar chart.
## Why this default [#why-this-default]
Unsorted by default: the data's order often IS information. Null values keep their slot as a gap, so a missing category
never shifts its neighbors. Explicit domains are widened to include zero — a bar whose length doesn't start at zero is a
lie, so the component refuses to draw one.
## Accessibility [#accessibility]
The accessible name carries count and extremes — **"4 categories. Highest East 940, lowest North 120."** The interactive
entry announces each bar with its rank: **"East: 940 — 1st of 4."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Categories in meaningful order. |
| `order` | `"data" \| "desc" \| "asc"` | Ranking read vs positional read — data-facing, not styling. |
| `highlight` | `number \| string` | Index or label to emphasize. |
| `orientation` | `"horizontal" \| "vertical"` | Rows for wider, shorter cells. |
| `positive` | `"up" \| "down"` | Engages pos/neg tokens on signed data. |
| `label` | `"none" \| "max"` | Peak-value readout (vertical only; default "none"). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MinimapStrip (/docs/charts/minimap-strip)
MinimapStrip answers "where am I in the whole — and where in the whole is everything else I care about?". A content
thumbnail sits under a viewport window and a lane of annotation ticks, and any region you haven't loaded or crawled is
drawn as fog rather than blank — because absence is not the same as zero. The unknown share travels with the accessible
name.
```tsx
```
## Install [#install]
```tsx
import { MinimapStrip } from "@microcharts/react/minimap-strip";
Math.abs(Math.sin(i / 40)) + Math.abs(Math.sin(i / 150)) * 0.6,
),
window: [520, 660],
marks: [100, 600, 1100],
known: [[0, 1104]],
}}
title="Document position"
/>
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — document or log position, long-timeline navigation.
* **Avoid for** — a single value (Progress) or exact content values (Sparkline).
## Variants [#variants]
```tsx
Math.abs(Math.sin(i / 40)) + Math.abs(Math.sin(i / 150)) * 0.6,
),
window: [300, 440],
marks: [600, 1000],
}}
mode="heat"
/>`"
>
```
## Edge cases [#edge-cases]
```tsx
Math.abs(Math.sin(i / 20))),
window: [40, 90],
known: [[0, 200]],
}}
/>`"
>
```
## Why this default [#why-this-default]
Bars plus a separate mark lane keep "where am I" and "where are the annotations" as two clean reads instead of one
muddle. Fog-of-war is a first-class state — unknown regions get a hatched texture, never a blank stretch that would read
as empty content, and the unknown share is disclosed in the summary. The window maps linearly to the domain; there is no
fisheye to distort where you are.
## Accessibility [#accessibility]
The accessible name places you in the whole — **"Viewing 12% of the whole (300–440 of 1,200); 2 marks."** The
interactive entry is a slider: drag or click to move the window, or nudge it with ←/→ (Shift for a bigger jump).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ content, window, marks?, known? }` | Density series, viewport, ticks, covered regions. |
| `mode` | `"bars" \| "heat"` | Heat is a calmer opacity strip. |
| `markLane` | `boolean` | Dedicated tick lane vs overlaying ticks. |
| `onWindowChange` | `(window: [number, number]) => void` | (interactive) Fires with the new `[start, end]` index range as the brush window is dragged. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MoonPhase (/docs/charts/moon-phase)
MoonPhase answers "how far through the cycle or period are we?" with a form almost everyone reads without a legend: the
illuminated fraction of a disc. The lit **area** equals the value exactly — a closed-form terminator, not the
phase-angle approximation that under-lights mid-cycle — so 50% genuinely lights half the disc. Progress mode fills
monotonically; cycle mode maps the real lunar sequence.
```tsx
```
## Install [#install]
```tsx
import { MoonPhase } from "@microcharts/react/moon-phase";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — sprint or quota progress in a sentence, a billing-period or release-cycle marker, or any 0–1 completion
that reads at a glance.
* **Avoid for** — exact percentages (Progress), trends (Sparkline), or comparisons (MiniBar).
## Variants [#variants]
```tsx
// first quarter
// full moon`"
>
```
## Edge cases [#edge-cases]
```tsx
// clamps to 0 — new, dark disc
// clamps to 1 — full, lit disc`"
>
```
## Why this default [#why-this-default]
Progress mode is the default because period-progress is the common need — a sprint or quota filling from new to full,
monotonically. Pretending the real lunar cycle is monotonic would lie (it waxes then wanes), so `mode="cycle"` is a
separate, explicit data-semantic switch, not a preset. The lit area is always exactly the value via the closed form —
never the phase-angle shortcut. Area is a medium-precision channel, so steer exact reads to `Progress`; MoonPhase is for
the glance.
## Accessibility [#accessibility]
The accessible name states the fraction — **"0% of the cycle complete."** in progress mode, or "0% through the cycle."
in cycle mode. The interactive entry fades the lit region back in on change with a slight bloom (opacity plus a small
scale, never a path interpolation, and skipped entirely under `prefers-reduced-motion`), reveals the exact percent on
hover or focus, and announces changes through a polite live region throttled to at most once a second.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Fraction 0–1 (clamped). |
| `mode` | `"progress" \| "cycle"` | progress = monotonic fill; cycle = true lunar mapping (0 new → 0.5 full → 1 new). |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# MusicStaff (/docs/charts/music-staff)
MusicStaff answers "what's the shape of this short series?" by reading it as a melody: each value is a note, its
**pitch** (vertical position on a five-line staff) is the value, and left-to-right is time. Pitch is the only channel —
there are no clefs, stems, beams, or bar lines, because every other notation convention would be decoration. The read is
in steps (nine to thirteen positions), so exact values steer to `Sparkline` with a label.
```tsx
`"
>
```
## Install [#install]
```tsx
import { MusicStaff } from "@microcharts/react/music-staff";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a weekly-rhythm read in a sentence, the shape of a sprint or short series in a cell, or a per-channel
melody in a tab.
* **Avoid for** — exact values (Sparkline with a label), long series (over sixteen points), or trends where the exact
slope matters.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
A single value draws one note and no contour line — a melody needs at least two notes to have a shape. An all-`null`
series draws the bare staff with no notes at all; the accessible name falls back to `describeSeries`'s own no-data
phrasing. With a `locale`, the trailing `label="last"` value follows that locale's own grouping and decimal marks.
## Why this default [#why-this-default]
The ledger mode is the default because clipping pitch silently is worse than two extra hairline ledger ticks — a note
above or below the staff still reads truly. `mode="staff"` exists for dense cells where ledger lines would collide with
a neighbour; there, out-of-range pitches clamp to the nearest staff line or space instead, trading visual precision for
a collision-free cell — the accessible summary is unaffected either way, since it describes the raw values, not the
clamped pixel positions. Two adjacent equal values are spaced by the time axis, never dodged vertically, because moving
a note vertically would change its pitch — and pitch is the data.
## Accessibility [#accessibility]
MusicStaff reuses the same natural-language summary as `Sparkline`, verbatim — for `[3, 5, 4, 8, 6, 9]`, **"Trending up
200%. Range 3 to 9. Last value 9."** The interactive entry steps the notes with ←/→, announcing each as **"Point 3 of 6:
4."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | The series; null = a rest. |
| `mode` | `"staff" \| "ledger"` | ledger (±2, default) or staff (clamp on-staff). |
| `label` | `"none" \| "last"` | Print the final value after the last note. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# NetFlow (/docs/charts/net-flow)
NetFlow answers "in versus out — and where does that leave us net?". Inflow is an area above a zero baseline, outflow is
mirrored below it on **one shared magnitude scale** (never independently scaled to balance the picture), and the net
line (`in − out`) rides on top to restore the precise decision value. Gross and net answer different questions; the cell
answers both.
```tsx
```
## Install [#install]
```tsx
import { NetFlow } from "@microcharts/react/net-flow";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — cash flow per account row, in vs out (signups vs churn) in a KPI card, any in-vs-out where the net is
the decision.
* **Avoid for** — a single net series (Sparkline) or one period's split (Delta / SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
The net label and accessible summary both follow `format`/`locale` — 38000 reads "38.000" under `de-DE` rather than the
English "38,000".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
A single period always renders as mirrored bars, never an area — an area drawn through one point would imply continuity
the data doesn't have. `in`/`out` are magnitudes: a negative value is invalid input, not a reversed direction, so it's
coerced to `0`. When every period's magnitudes are zero (all periods coerced or genuinely empty), NetFlow draws only the
zero baseline — no areas, no bars, no net line — and the accessible name says so plainly: **"No flow across 1 period."**
## Why this default [#why-this-default]
Mirrored areas plus a net line, because gross and net answer different questions and a cell must answer both. Both
directions share **one** scale — never independently scaled to make the picture look balanced — and both areas anchor at
zero, so their extents are honest. The net line is a plain `in − out`, never smoothed, and its sign is stated in the
label's **text** so direction is never carried by color alone.
## Accessibility [#accessibility]
The accessible name pairs the net with its gross and counts the healthy periods — **"Net +14 last period; in 57 vs out
43; net positive 4 of 6 periods."**. The interactive entry steps the periods and announces inflow, outflow, and the
signed net together.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ in; out }[]` | Periods, oldest first — inflow and outflow magnitudes (both ≥ 0). |
| `mode` | `"area" \| "bars"` | Mirrored areas (default) or discrete columns for few periods. |
| `net` | `boolean` | The net line (in − out). Default true. |
| `positive` | `"up" \| "down"` | Which direction is good — 'down' for debt-paydown contexts. |
| `label` | `"last" \| "none"` | Signed net in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Ohlc (/docs/charts/ohlc)
Ohlc answers "what was the price action per period?" — open, high, low, close, in a cell. Direction is encoded by
valence color AND body geometry, never color alone.
```tsx
`"
>
```
## Install [#install]
```tsx
import { Ohlc } from "@microcharts/react/ohlc";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — watchlist table rows, ticker KPI cards.
* **Avoid for** — a single close series (Sparkline), or more than \~20 periods.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
## Why this default [#why-this-default]
`maxPeriods` renders the most recent N sessions and warns in development past it — data is truncated visibly, never
averaged into fake candles. Bodies keep a minimum 1-unit height so doji sessions stay visible.
## Accessibility [#accessibility]
The accessible name is the run read — **"20 periods. Last close 150.3, up 7.4%; range 136.4 to 156."** The interactive
entry announces each session's four prices (**"Period 18 of 20: open 145.6, high 150.6, low 141.6, close 144.1."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ open; high; low; close }[]` | Periods, oldest first. |
| `mode` | `"candle" \| "bars"` | Candle bodies or open/close ticks. |
| `maxPeriods` | `number` | Renders the most recent N (never averaged). |
| `label` | `"last" \| "none"` | Last close in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# OrbitStatus (/docs/charts/orbit-status)
OrbitStatus answers "how slow and how busy is this dependency right now?" in one small mark. The orbit radius is latency
— a wider orbit is a slower service — and the orbit's dash density is the call rate, denser dashes meaning more calls.
In the interactive entry the satellite orbits at a speed that mirrors that rate, so a busy service visibly spins faster.
Both channels are deliberately low-precision ambient reads; for exact numbers, use a `Sparkline` for latency and a
`Delta` or `MiniBar` for rate.
```tsx
```
## Install [#install]
```tsx
import { OrbitStatus } from "@microcharts/react/orbit-status";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## Motion, and reduced motion [#motion-and-reduced-motion]
The satellite's angular speed is quantized to the same five steps as the static dash density, so the motion and the
still frame decode identically — you learn nothing extra from watching, you just feel the rate. This is the deliberate
idle-loop exception, allowed because the loop rate is the datum. It pauses off-screen and, under
`prefers-reduced-motion`, does not spin at all: the dash density already carries the rate. The satellite's angular
*position* never means anything — only its speed does — so the docs say plainly not to read the angle.
## When to use it [#when-to-use-it]
* **Good for** — a live dependency health dot in a service table, latency and rate together in one small mark, or an
infra status glance.
* **Avoid for** — exact latency (`Sparkline`), exact rate (`Delta`, `MiniBar`), or a trend over time (`Sparkline`).
## Sizing [#sizing]
One `size` prop, in viewBox units, sets the orbit box — it defaults to `20`. There is no separate `width`/`height`: the
glyph is square, and the rendered box widens only by the gutter a `label` reserves. `fontSize` follows `size` unless you
set it.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
The dash-density static encoding exists because a paused satellite says nothing — the still frame had to carry both
variables or the chart would fail the survivor test, so rate lives in the dashes as well as the spin. The rate is
quantized to five ordinal steps in both renderings — five dash counts and five angular periods — and radius and speed
always come from the same domains in the static and interactive frames, so nothing drifts. A lone orbit radius is
meaningless, so the docs insist on explicit `latencyDomain` and `rateDomain` — the same steer as `FatDigits`. At
`threshold`, the satellite doubles and flags the summary, never relying on color alone. Unknown latency or rate goes
gray and stops the spin: an unknown dependency must not look healthy.
## Accessibility [#accessibility]
The accessible name is both variables with units — **"240ms latency at 12 calls/s."** — plus "— above threshold" when
the latency crosses `threshold`, or **"Latency unknown."** when a value is missing. The threshold state is carried by
the satellite's size and the summary, never color alone; motion is gated on `prefers-reduced-motion`; and the live
region announces threshold crossings only.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `latency` (required) | `number` | Orbit radius (weak — pass a domain). |
| `rate` (required) | `number` | Dash density / satellite speed. |
| `latencyDomain` | `[number, number]` | Latency extent (insist on it — a lone radius is meaningless). |
| `rateDomain` | `[number, number]` | Rate extent (default [0, 2·rate]). |
| `threshold` | `number` | Latency threshold: at/above it the satellite doubles + the summary flags it. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PairedBars (/docs/charts/paired-bars)
PairedBars answers "how does actual compare to expected, category by category?". Value and reference always share one
zero-anchored domain — comparing bars on different scales is the cardinal grouped-bar lie — and the reference is muted
by two structural cues (opacity *and* width), never color alone.
```tsx
`"
>
```
## Install [#install]
```tsx
import { PairedBars } from "@microcharts/react/paired-bars";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — budget vs actual per region, target vs result rows.
* **Avoid for** — data without a reference series (MiniBar) or more than 5 pairs.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
The gap named in the summary follows `format`/`locale` — 12000 reads "12.000" under `de-DE` rather than the English
"12,000".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
A pair whose `ref` is `null` draws its value bar alone — no ghost, no zero-width placeholder — and the interactive entry
announces it as **"East: 940, no reference."** rather than pretending a comparison exists. Past 5 pairs the grouped read
blurs into noise, so the component still renders every pair but logs a dev warning.
## Why this default [#why-this-default]
Grouped over overlay by default — overlap hides small over-shoots, so the denser overlay form is the variant you choose
for tight cells, not the analytical default. A pair with a missing reference renders the value bar alone and announces
"no reference"; a chart where *every* reference is missing dev-warns and steers to MiniBar.
## Accessibility [#accessibility]
The accessible name leads with the largest gap — **"2 pairs. Largest gap East: 940 vs 1,200."** The interactive entry
roves pairs and announces each as value vs reference (**"East: 940 vs 1,200."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value; ref }[]` | Value + reference per category. |
| `mode` | `"grouped" \| "overlay"` | Overlay puts the ref as a full-width ghost behind — halves the footprint. |
| `positive` | `"up" \| "down"` | Over/under-reference valence tint. |
| `orientation` | `"horizontal" \| "vertical"` | Rows for wide cells. |
| `locale` | `string \| string[]` | BCP 47 locale(s) for the gap named in the summary, e.g. "de-DE". |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ParetoStrip (/docs/charts/pareto-strip)
ParetoStrip answers "what should we fix first?". It sorts the categories descending and overlays a **cumulative-share
line on a fixed 0–100% scale** that spans the full height and is never rescaled to steepen the curve. The bars up to the
threshold crossing are accent — the **vital few** — and the rest are muted, because the chart's one job is to say where
to stop reading. 80% is a working reference, not a law, and `Other` never re-enters the ranking.
```tsx
```
## Install [#install]
```tsx
import { ParetoStrip } from "@microcharts/react/pareto-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a "fix these three" read in a KPI card, incident causes or support tags in a tab header, any long-tail
composition where a few dominate.
* **Avoid for** — a plain ranking (MiniBar) or parts of a single whole (SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The accent stops at the crossing, because the chart's one job is to say where to stop reading. The cumulative scale is
fixed 0–100% and shares the full plot height — never rescaled to make the curve look steeper — and 80% is a
**reference** default you can move or turn off, never a claimed law. `Other` is rendered honestly at its true size but
always last and never counted among the vital few; if it towers over the top cause, that usually means `maxItems` is set
too aggressively.
## Accessibility [#accessibility]
The accessible name states the vital-few count and its cumulative — **"Top 1 of 3 causes account for 90% of
incidents."** — or, when there is no threshold (or no ranked cause reaches it), the top cause and its share, as in the
rolled-up demo above: **"Timeouts leads at 39%."** The interactive entry steps the bars and announces each bar's share
and running cumulative; **T** jumps to the crossing bar.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Categories with magnitudes — the component sorts descending. |
| `threshold` | `number \| false` | Cumulative reference line % (default 80 — a working reference, not a law). |
| `maxItems` | `number` | Categories beyond maxItems roll up into Other (default 8, always last). |
| `unit` | `string` | Category noun for the summary (default 'causes'). |
| `metric` | `string` | Total-metric noun for the summary (default 'the total'). |
| `label` | `"count" \| "none"` | 'K of N → cum%' in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PartitionStrip (/docs/charts/partition-strip)
PartitionStrip answers "what is the whole made of — and what are the big parts made of — with parentage visible?".
Parents sit on the top row with widths proportional to their share of the whole, and each parent's children tile the
exact x-range beneath it. Two aligned rows beat a treemap because alignment is the comparison channel — and two levels
is a hard limit, because deeper hierarchies become unreadable texture at this size.
```tsx
`"
>
```
## Install [#install]
```tsx
import { PartitionStrip } from "@microcharts/react/partition-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — bundle / storage / budget composition, two-level breakdowns.
* **Avoid for** — deep hierarchies (unreadable at this size) or flat parts (SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Two aligned rows because alignment is the comparison — you can see at a glance that a child sits under its parent and
how much of it that child is. A treemap discards that alignment and a flat SegmentedBar discards the hierarchy entirely.
Grandchildren are ignored with a dev warning, on purpose: three levels of nesting at 24px is texture, not information.
## Accessibility [#accessibility]
The accessible name names the biggest leaf and its parent — **"3 groups, 5 parts; largest JS → react (31% of the
whole)."** The interactive entry roves within a row with ←/→ and moves between a parent and its children with ↑/↓,
announcing each node's share of the whole and of its parent.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, value?, children? }[]` | Two-level hierarchy. |
| `emphasis` | `string` | Accents one node and its lineage. |
| `labels` | `boolean` | Parent-row labels with size drop-out. |
| `colors` | `string[]` | Per-group colours, cycled; overrides --mc-cat-N. Accent/neutral roles keep. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PercentileLadder (/docs/charts/percentile-ladder)
PercentileLadder answers "what does the tail look like — not just the median?". Ticks at the chosen percentiles sit on a
zero-anchored track; graduated height and accent make the **tail** read strongest. Tick distances carry the story, so
the origin is never cropped — and a log transform (for long latency tails) is never silent: an in-chart `log` tag
renders when it applies.
```tsx
i < 130
? 90 + (i % 50)
: i < 180
? 150 + ((i * 7) % 320)
: i < 196
? 480 + ((i * 11) % 900)
: 1500 + ((i * 13) % 800),
)}
title="Request latency"
/>`"
>
```
## Install [#install]
```tsx
import { PercentileLadder } from "@microcharts/react/percentile-ladder";
i < 130
? 90 + (i % 50)
: i < 180
? 150 + ((i * 7) % 320)
: i < 196
? 480 + ((i * 11) % 900)
: 1500 + ((i * 13) % 800),
)}
format={{ style: "unit", unit: "millisecond" }}
title="Request latency"
/>
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — latency SLOs in a sentence, the tail per endpoint in tables, payment-size spread.
* **Avoid for** — the odds of an outcome (QuantileDots) or the full distribution shape (MicroBox).
## Variants [#variants]
```tsx
i < 130
? 90 + (i % 50)
: i < 180
? 150 + ((i * 7) % 320)
: i < 196
? 480 + ((i * 11) % 900)
: 1500 + ((i * 13) % 800),
);
// value labels and the summary format through Intl — "1.661 ms", not "1,661 ms"
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The default linear track is zero-anchored — tick **distances** carry the story, so cropping the origin would distort
every gap between percentiles. Height and accent grow toward the tail because the tail, not the median, is usually the
SLO question. A log transform for long tails is never silent: it only applies when every sample is strictly positive,
and an in-chart `log` tag renders whenever it does, so the axis is never misread as linear.
## Accessibility [#accessibility]
The accessible name states every marked percentile and the tail's multiple of the median — **"p50 124.5, p90 488.1, p99
1,661.13 — the slowest 1% take 13.3× the median."** The interactive entry roves ticks with ←/→ (Home/End jump to the
ends), announcing each tick's value and its multiple of the median (**"p99: 1,661.13 — 13.3× the median."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw sample; quantiles are derived. |
| `ps` | `number[]` | Percentiles to mark (default [50, 90, 99], 2–4). |
| `scale` | `"linear" \| "log"` | Log for long tails (falls back on any value ≤ 0; renders a log tag). |
| `label` | `"ps" \| "values" \| "both" \| "none"` | What the tick labels state. |
| `marks` | `"tick" \| "dot"` | Tick marks (default) or dot marks — dots read calmer over dense text. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PercentileTrace (/docs/charts/percentile-trace)
PercentileTrace answers "is this entity's standing rising or slipping inside the pack?". It traces one **percentile
rank** over time on a scale **locked to 0–100**. Because the axis *is* rank, the population is constant by definition —
the middle-half (**p25–75**) and near-full (**p5–95**) bands are fixed fields, not estimates — so the only line on the
chart is the entity itself, drifting through a known landscape.
```tsx
`"
>
```
## Install [#install]
```tsx
import { PercentileTrace } from "@microcharts/react/percentile-trace";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — one player's or product's rank drifting over time, whether a standing has crossed into the top or
bottom of the pack, a percentile KPI where the population context matters.
* **Avoid for** — a raw value over time (Sparkline) or one absolute number vs a target (Bullet / Delta).
## Variants [#variants]
```tsx
`"
>
```
The endpoint dot carries **valence**: by default a rising standing is good, so it turns positive; set `positive="down"`
when slipping *down* the pack is the win (a support ticket's backlog rank, say). The line already encodes the direction,
so the color is only a redundant cue — never the sole signal.
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`"
>
```
A single reading is a lone endpoint with no line to draw — it still reports its standing and holds the accessible-name
contract. Ranks outside 0–100 are clamped to the axis (a value of `102` reads as `p100`) and warn once in development.
With a `locale`, the percentile label and every announced number follow that locale's own formatting — `de-DE` renders
`p81,5` with a comma.
## Why this default [#why-this-default]
The full 0–100 axis with fixed bands, because a percentile is a standing *within a population* — truncating the axis
would hide how much headroom or floor is left, and re-estimating the bands from the single traced series would be a
fiction. The middle-half and near-full fields are exact by construction, so the chart shows one honest line over a
landscape you can trust, never a thicket of competing series.
## Accessibility [#accessibility]
The accessible name states the current percentile, the change from the first reading, and how the standing moved
relative to the middle half — **"p81 now, up 41 points from the first reading; moved above the middle half."** The
interactive entry steps the readings and announces each one's percentile.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Percentile ranks 0–100, one per reading; out-of-range values are clamped. |
| `showBands` | `boolean` | Draw the fixed p25–75 and p5–95 population fields (default true). |
| `positive` | `"up" \| "down"` | Which direction is good — colors the endpoint dot (default up). |
| `label` | `"last" \| "none"` | Final percentile in a right gutter. |
| `unit` | `string` | (interactive) Reading noun for the interactive announcement (default 'step'). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PhaseTrace (/docs/charts/phase-trace)
PhaseTrace answers "how do two coupled signals move *together*?". Plotting one against the other as a single trajectory
turns lag into loops and regimes into clusters — structure that two separate sparklines hide. Path order carries time,
and the current state is a directed endpoint with an arrowhead, so "now, and how it got here" reads at a glance.
```tsx
{
const t = (i / 40) * Math.PI * 2;
return { x: 55 + Math.cos(t) * 22, y: 110 + Math.sin(t - 0.9) * 40 };
})}
xLabel="CPU"
yLabel="Latency"
title="Phase portrait"
/>`"
>
```
## Install [#install]
```tsx
import { PhaseTrace } from "@microcharts/react/phase-trace";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — coupled-signal phase portraits (CPU × latency, inflation × unemployment).
* **Avoid for** — reading exact values (DualSparkline) or a single series (Sparkline).
## Variants [#variants]
```tsx
{
const t = (i / 40) * Math.PI * 2;
return { x: 55 + Math.cos(t) * 22, y: 110 + Math.sin(t - 0.9) * 40 };
})}
tail={0.4}
startDot
/>`"
>
```
```tsx
`"
>
```
With a `locale`, the accessible summary's coordinates follow that locale's own grouping — "1.800" in German, not
"1,800". In-chart marks carry no text, so only the announced numbers change.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
({ x: 40, y: 40 }))}
xLabel="CPU"
yLabel="Latency"
/>`"
>
```
A single observation draws only the accent endpoint dot — there's no history to trail and no motion for an arrowhead to
point along. When every point in the tail window is coincident (or nearly so, within 0.5% of the combined axis span),
the heading resolves to the fifth, explicit "steady" state rather than a jittery direction from noise.
## Why this default [#why-this-default]
A muted trail with an accent tail because "now, and how it got here" is the question — the eye lands on the recent
motion first, then reads the history. Axis and scale choices are stated, never silently transformed: the axes are named
in the summary, and the domains are always linear. The tail, endpoint, and arrowhead trio may be restyled but never all
removed, because the recoverable time direction is what separates a phase trace from a scatter.
## Accessibility [#accessibility]
The accessible name states the current point and heading, with the y-axis named first — for the hero example above,
**"Latency vs CPU: now 76.729, 75.163; heading down-right."** The interactive entry steps through the trajectory in time
order with ←/→, announcing each point's index and its value on both named axes.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ x, y }[]` | Two synchronized signals, time-ordered. |
| `xLabel / yLabel` | `string` | Axis names — the summary reads them. |
| `xDomain` | `[number, number]` | Fix the x-axis range (default: the data's x-extent). |
| `domain` | `[number, number]` | Fix the y-axis range (default: the data's y-extent). |
| `tail` | `number` | Fraction of points drawn in accent (recent motion). |
| `grid` | `boolean` | Quadrant hairlines for regime reads. |
| `startDot` | `boolean` | Anchor the path's origin for full-journey reads (default false). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PictogramRow (/docs/charts/pictogram-row)
PictogramRow answers "how many of the N units are filled?" — ●●●○○ — counts a human can verify by counting. Filled vs
hollow is a shape difference too, never opacity alone, and unit size is constant: scaling glyphs with value is the
classic pictogram lie this component refuses to tell.
```tsx
{"The coalition holds "}
{" of the seats."}
```
## Install [#install]
```tsx
import { PictogramRow } from "@microcharts/react/pictogram-row";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — seats, slots, and ratings in a sentence; capacity rows in tables.
* **Avoid for** — more than 20 units (counting fails; use Progress) or continuous ratios (Progress).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
A `total` of 0 (or non-finite) draws nothing and the summary is "No data." — there is no meaningful row to count. A
negative value renders every unit hollow, and the summary still states the true number ("-2 of 4.") rather than clamping
to 0. A value past the total fills every unit — never more than `total` units drawn — but the summary keeps the honest
count ("9 of 8.") with a dev-time warning, so the overflow is never hidden.
## Why this default [#why-this-default]
Clip over round — the partial unit is real data, drawn as a true circular segment (not a `clipPath`, which would need
generated ids that break server rendering determinism). Overflow renders all units filled but the summary keeps the true
numbers ("9 of 8.") with a dev warning; the chart never silently caps the figure.
## Accessibility [#accessibility]
The accessible name is the count — **"3 of 5."** — with the context noun coming from your `title` ("Committee seats
held. 3 of 5."). The interactive entry re-announces when the value changes, and roving the units announces each one on
its own: on the 5-of-8 demo at the top of this page, the fourth unit says "Unit 4 of 8 — filled." A unit reads as empty,
filled, or, for a genuinely partial one, the percentage it holds.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Filled units (may be fractional). |
| `total` (required) | `number` | Unit count. |
| `shape` | `"dot" \| "square"` | Squares pack tighter in table cells. |
| `fractional` | `"clip" \| "round"` | Clip shows the true partial unit; round for seat-like units. |
| `renderPoint` | `(unit) => ReactNode` | Custom unit glyph (star ratings) — the one sanctioned customization. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# PolarClock (/docs/charts/polar-clock)
PolarClock answers "what's the shape of the day or week cycle — when is it busy?" Each value in the cycle becomes a
radial bar at its fixed angle, growing outward from an inner baseline; longer bars are busier times. Midnight (or your
week-start) sits at 12 o'clock and the cycle runs clockwise, so the rhythm of a metric across the cycle is immediately
legible.
```tsx
```
## Install [#install]
```tsx
import { PolarClock } from "@microcharts/react/polar-clock";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — the shape of a 24-hour or 7-day cycle, when a metric is busy across the cycle, or a compact seasonal
read in a KPI card.
* **Avoid for** — exact value comparison (unroll the cycle into a `SparkBar`), a non-cyclic trend (`Sparkline`), or more
than a few dozen segments.
## Variants [#variants]
```tsx
(h === 14 ? 312 : h === 4 ? 20 : 80 + h));
const week = [120, 200, 180, 210, 260, 90, 60];
`"
>
```
```tsx
(h === 14 ? 312 : h === 4 ? 20 : 80 + h));
`"
>
```
```tsx
(h === 14 ? 1240 : h === 4 ? 20 : 80 + h));
`"
>
```
## Why this default [#why-this-default]
The channel is radial **length from the inner baseline**, not sector area. Equal-value bars at the rim span more area
than ones near the hub, so an area reading would exaggerate the outer segments — the inner radius is nonzero precisely
to curb that distortion, and the docs ask you to compare lengths, not wedges. Bars are always zero-anchored at that
baseline. For very small sizes where length is hard to judge, `mode="opacity"` switches the channel to a five-step fill
— a radial `ActivityGrid` — which is a deliberate, named change of encoding, not a cosmetic one. A `null` segment leaves
a gap (the baseline ring shows the hole) because missing is not the same as zero.
The four cardinal ticks default **on** because a bare ring of bars is rotationally ambiguous — without a mark for "12
o'clock" there is no way to tell where the cycle starts, so "when is it busy" can't be answered from the static frame
alone. The four ticks are merged into one path (a single node), so the orientation cue is free of the usual
label-drop-out tradeoff — set `labels={false}` only when the shape itself is the whole story and the axis genuinely
doesn't matter.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Accessibility [#accessibility]
The accessible name states the peak and the quiet point of the cycle — **"Peaks at 14:00 (1.240); quietest 04:00."** —
with hour labels for a 24-segment cycle and weekday names for a 7-segment one. The interactive entry lets you arrow
through the segments circularly, each announced with its label and value through a polite live region, and the accented
`now` segment carries position and color, never color alone.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | One value per cycle division (24 hourly, 7 daily, any n). |
| `now` | `number` | Index of the current segment to accent. |
| `inner` | `number` | Inner radius fraction r0 — the zero baseline bars grow from (default 0.35). |
| `mode` | `"length" \| "opacity"` | Radial bars (default) or fixed-length 5-step fill. |
| `origin` | `number` | Index rendered at 12 o'clock (week-start / midnight). |
| `labels` | `boolean` | Hairline cardinal ticks at 0/¼/½/¾ — the at-rest orientation cue. Default true. |
| `segmentFormat` | `(index, n) => string` | Segment index → label (default: HH:00 for n=24, weekday for n=7, else index). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ProgressRing (/docs/charts/progress-ring)
ProgressRing answers "how complete is this?" at icon size, where a linear bar doesn't fit. The start angle is fixed at
12 o'clock and the caps are butt-cut — the two quiet ways rings inflate progress, both removed. It is never a gauge: no
needle, no red zone.
```tsx
`"
>
```
## Install [#install]
```tsx
import { ProgressRing } from "@microcharts/react/progress-ring";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — tab headers, KPI card corners, cooldowns and retry timers (`sweep`).
* **Avoid for** — precise reads (Progress — the % label is the datum there).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Butt caps and a fixed start: rounded caps overstate small fractions, and variable start angles make identical fractions
look different. Past 100% the ring clamps while the center label carries the true percent — same contract as Progress. A
full circle is drawn as two half-arcs (SVG cannot draw a single 360° arc).
## Accessibility [#accessibility]
The accessible name reuses the progress wording — **"68% complete."**, or **"32% remaining."** in sweep mode. The
interactive entry announces only at 25/50/75/100% threshold crossings — a streaming value never spams the screen reader.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The progressed amount. |
| `max` | `number` | Denominator (default 1). |
| `sweep` | `boolean` | Countdown: the REMAINING wedge shrinks. |
| `weight` | `number` | Ring thickness (viewBox units). |
| `label` | `"none" \| "percent"` | Centered figure (≥ 20 px rendered). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Progress (/docs/charts/progress)
Progress answers "how far along is this, exactly?". The bar gives the instant read; the percent label *is* the datum — a
bare bar is decoration. The track never shrinks for the label (the viewBox widens instead), so fractions stay comparable
down a table column.
```tsx
```
## Install [#install]
```tsx
import { Progress } from "@microcharts/react/progress";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — KPI cards, table completion columns, step counts (`segments`).
* **Avoid for** — icon-size slots (ProgressRing) or composition (SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
With `label="percent"` (the default) the accessible name follows `format`/`locale` too — "68 %" under `de-DE` rather
than the English "68%".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
0 — an empty track,
// not an invented fraction
`"
>
```
`max <= 0` (or a non-finite `value`/`max`) renders an empty track with no fill, and the label becomes an em dash ("—")
rather than a fabricated number — the accessible name says "No data." instead of computing a meaningless ratio. A
`value` of `0` renders a real, empty-but-present bar (not the no-data state), so "not started" and "unmeasurable" stay
visually distinct.
## Why this default [#why-this-default]
The percent label is on by default because it is the datum — the bar alone can't say "68%". Past 100% the bar clamps at
full but the label carries the true figure ("112%"): the number and the bar only ever disagree in that one documented
case, and the number wins. `max <= 0` renders an empty track and says "No data." rather than inventing a fraction.
`max` — not `total` — is the deliberate exception across the library: every other denominator prop names a discrete
count (`total` on IconArray, TallyMarks, Honeycomb…), but Progress tracks a continuous goal that can be overshot, so it
keeps the word that says "ceiling," not "count."
## Accessibility [#accessibility]
The accessible name is the completion — **"68% complete."**, or **"3 of 5 steps."** when segmented, or **"32%
remaining."** with `positive="down"`. The interactive entry re-announces through a polite live region, throttled to
whole-percent changes so a streaming value doesn't spam screen readers, and the fill-width transition is gated on
reduced motion.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The progressed amount. |
| `max` | `number` | Denominator (default 1). |
| `segments` | `number` | Discrete-chunk track — the chart says step count, not ratio. |
| `label` | `"percent" \| "value" \| "fraction" \| "none"` | The direct label; percent is the default datum. |
| `positive` | `"up" \| "down"` | down = burn-down wording (summary only; the bar stays factual). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# QuadrantDot (/docs/charts/quadrant-dot)
QuadrantDot answers "where does this item sit in the 2×2 — against the field?". A focal dot is placed by 2-D position, a
hairline cross marks the split (the domain midpoints by default, always overridable but **never hidden**), and the peers
show as tiny muted ghosts. The read is quadrant **membership** first; exact position is the second read — which is why
it lives at glyph scale.
```tsx
```
## Install [#install]
```tsx
import { QuadrantDot } from "@microcharts/react/quadrant-dot";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — the classic prioritization 2×2 (one cell per initiative in a table), an effort-vs-impact read in a KPI
card, any "which quadrant, against the field" decision.
* **Avoid for** — a full scatter plot (MicroScatter) or a single ranked list (MiniBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
When every field value on one axis equals the focal's, that axis's domain collapses to a point — the split line on that
axis is dropped rather than drawn at an arbitrary spot, while the other axis's split still renders. A peer exactly on
top of the focal is still counted (distance 0) and sorts first in the interactive entry's nearest-first order. A
non-finite focal position draws no marks at all — no cross, no tint, no peers — and carries the "No data." summary, the
same degenerate path an empty series takes elsewhere in the library.
## Why this default [#why-this-default]
24×24, because the 2×2 is a **position** read, not a scatter plot — you want to know the quadrant at a glance, and the
exact coordinates only when you look closer. The split is data, not decoration: it renders wherever the split truly is
and is always overridable, never invisible. With the axes unlabeled at glyph size, the `title` and `summary` are
load-bearing — skipping them is the one named anti-pattern for this chart, so always pass `xLabel`, `yLabel`, and a
`title`.
## Accessibility [#accessibility]
The accessible name states the focal position and the axis-relative quadrant — **"Impact 9, effort 3 — in the
high-impact, low-effort quadrant."** — adding how crowded that quadrant is when a `field` is given, or your own words
via `quadrants={["quick win", …]}`. The interactive entry starts on the focal, then cycles peers nearest-first — each
announced with its coordinates and quadrant.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ x; y }` | The focal item's 2-D position. |
| `field` | `{ x; y }[]` | The peer set — omit for a lone glyph. |
| `xDomain` | `[number, number]` | The x-axis range (default: derived from the data); domain stays the y-axis. |
| `domain` | `[number, number]` | The y-axis range (default: derived from the data). |
| `split` | `[number, number]` | The quadrant boundary (default domain midpoints) — never hidden. |
| `quadrants` | `[TL, TR, BL, BR]` | Names in reading order — summaries only, never rendered. |
| `xLabel / yLabel` | `string` | Axis nouns for the summary — pass them, the axes are unlabeled at glyph size. |
| `region` | `boolean` | Faint tint on the focal's quadrant (default true; false for dense grids). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# QuantileDots (/docs/charts/quantile-dots)
QuantileDots answers "what are the odds?" — in **countable** form. It's a quantile dotplot: each dot is an
equal-probability quantile, so a dot is roughly a **1-in-N chance**, not a raw observation. Add a `threshold` and the
plot turns from shape into odds: the dots past the line are re-inked and ringed, and the summary reads **"N in 20"** —
because odds you can count beat odds you must trust, and a frequency reads truer than a bare percentage.
```tsx
```
## Install [#install]
```tsx
import { QuantileDots } from "@microcharts/react/quantile-dots";
`${n} min`} title="Bus wait" />
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a "will we miss the SLA?" read in a sentence, odds you can count in a KPI card, a posterior
communicated as frequency rather than percent.
* **Avoid for** — a precise distribution shape (HistogramStrip) or one estimate's interval (GradedBand).
## Variants [#variants]
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the announced threshold follows that locale's own
grouping and decimal marks.
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
Empty data draws nothing and the summary says so plainly. An all-equal sample collapses every dot into one column —
genuine certainty, not a rendering glitch. A threshold outside the observed range reads as 0 in N (or N in N) — the
count is always honest about what fraction of the field it's actually catching.
## Why this default [#why-this-default]
20 dots, because odds you can count beat odds you must trust — a reader subitizes a handful of highlighted dots faster
than they parse "48%". Each dot is an equal-probability quantile, **not** a raw observation, and the summary always
frames the answer as a frequency ("10 in 20"), never a bare percentage. Dots past the threshold are re-inked **and**
ringed, so the count survives grayscale, print, and forced-colors.
## Accessibility [#accessibility]
The accessible name states the odds in frequency form — **"8 in 15 chances above 15 min."** — or, without a threshold,
"Most likely …; range …". The interactive entry is a probe: hovering or arrowing moves a live threshold and the count
past it recomputes as you go.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw sample or posterior draws — the component derives the quantile dots. |
| `count` | `number` | Number of quantile dots (default 20; 15–20 recommended; capped at 25). |
| `threshold` | `number` | The decision line — turns the plot from shape into odds. |
| `side` | `"above" \| "below"` | Which side of the threshold is the event being counted. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# QueueDepth (/docs/charts/queue-depth)
QueueDepth answers "is the backlog draining or growing?". It draws the queue **depth** as a zero-anchored area — a
stock, not a rate — against a dashed **capacity** hairline. Wherever the depth runs above capacity, the top edge is
re-stroked in the negative ink, so a breach is a change in *shape* as well as color. The endpoint carries the current
value and a trend glyph (▴/▾) fit over the last quarter of the window.
```tsx
```
## Install [#install]
```tsx
import { QueueDepth } from "@microcharts/react/queue-depth";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a support-queue backlog in a KPI card, a work-in-progress stock against its WIP limit, will-it-drain in
a tab header.
* **Avoid for** — a rate rather than a stock (Sparkline) or a single count (Delta).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
With a `locale`, both the endpoint label and the accessible summary's numbers follow that locale's own grouping — the
summary reads **"2.140 queued, 2,1× capacity, growing over the last quarter."** in German, not "2,140 queued, 2.1×
capacity".
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`"
>
```
With no `capacity`, the area still draws and the summary reads **"214 queued, growing over the last quarter."** — no
capacity clause, because there's nothing to compare against. A queue that stays under capacity reports **"30 queued,
within capacity, draining over the last quarter."** and never re-strokes the edge. Gaps (`null`/`NaN`/`±Infinity`) split
the area into separate runs rather than drawing a line across missing periods.
## Why this default [#why-this-default]
The capacity hairline plus the negative re-stroke, because "are we keeping up?" is a threshold question — a breach
should read instantly and survive greyscale, so it changes the edge's *shape*, not only its color. The trend glyph is
deliberately low-precision: it's fit over the last quarter of the window as a direction cue, and the summary states that
window in words ("over the last quarter") rather than implying a forecast the data can't support.
## Accessibility [#accessibility]
The accessible name states the current depth, the capacity relationship, and the trend — **"214 queued, 2.1× capacity,
growing over the last quarter."**. When the depth stays under capacity the middle clause becomes "within capacity"; with
no `capacity` it drops entirely. The interactive entry steps the periods, announcing each depth and whether it's above
capacity (**"t8: 214 queued, above capacity."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Backlog depth per period (≥ 0). null / NaN / ±Infinity are gaps. |
| `capacity` | `number` | Steady-state capacity: a dashed hairline; spans above it re-stroke negative. |
| `label` | `"last" \| "none"` | Endpoint value + trend glyph (▴/▾), default 'last', or nothing. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# RateVolume (/docs/charts/rate-volume)
RateVolume answers "the rate moved — on what volume?". A precise rate line rides over deliberately low-precision **ghost
volume bars** — the denominator. There is no prop to remove the bars: a rate without its denominator is the lie this
type exists to prevent. A 4.1% conversion rate reads very differently on 38 events than on 3,800.
```tsx
```
## Install [#install]
```tsx
import { RateVolume } from "@microcharts/react/rate-volume";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a conversion / error rate with its denominator, a KPI card where the rate is the headline, spotting a
big rate move on thin volume.
* **Avoid for** — volume itself needing a precise read (pair a SparkBar) or a plain series (Sparkline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
`format` and `volumeFormat` both take `Intl.NumberFormatOptions` and share the same `locale` — the rate label reads "4,1
%" and volumes group with a period ("3.800") under `de-DE`, matching that locale's own conventions rather than a
hardcoded English format.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
A period with zero (or non-finite) volume renders no bar and no rate mark, and the line gaps across it instead of
interpolating through a period nothing was measured on — the interactive entry announces that period as **"no events"**
rather than a rate. With no data at all, the chart renders an empty root and the accessible name says **"No data."**
## Why this default [#why-this-default]
The volume bars are muted and unlabeled because they answer "enough?", not "how many?". A rate on **zero** volume is
undefined regardless of the input — the line breaks and no mark is drawn, because a rate nobody generated should never
be plotted. `minVolume` makes a thin denominator visible right at the mark: below it the rate point renders hollow, a
shape cue that survives forced-colors and print.
## Accessibility [#accessibility]
The accessible name always pairs the rate with its volume — **"4.1% on 38 events (low volume); up from 2.3% across 5
periods."**. The interactive entry steps the periods and its live region never states a rate without the volume it
stands on, announcing "no events" for a zero-volume period.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ rate; volume }[]` | Periods, oldest first — each a rate and the volume it was measured on. |
| `minVolume` | `number` | Below it, the rate mark renders hollow — 'insufficient denominator'. |
| `curve` | `"linear" \| "step"` | Step suits per-period aggregate rates. |
| `volumeFormat` | `Intl.NumberFormatOptions \| (n) => string` | Volume has different units than rate; formatted separately. |
| `unit` | `string` | Noun for the volume unit in the summary (default 'events'). |
| `volumeDomain` | `[number, number]` | Volume (bar) domain — defaults to [0, max], zero-anchored. |
| `label` | `"last" \| "none"` | Endpoint rate in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# RetentionCurve (/docs/charts/retention-curve)
RetentionCurve answers "do they stay — and does the curve plateau?". It draws a **step** line (cohort periods are
discrete) on a scale **locked to 0–100%** — the full range is the honest frame for a share, so the floor is never
truncated to manufacture drama. When the curve flattens, a dotted plateau marker appears; a peer curve can ride behind
as a subordinate ghost.
```tsx
`"
>
```
## Install [#install]
```tsx
import { RetentionCurve } from "@microcharts/react/retention-curve";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a cohort retention curve in a KPI card, your decay vs an industry benchmark, spotting whether retention
plateaus (or keeps leaking).
* **Avoid for** — a continuous signal (Sparkline) or one-number retention (Progress / Delta).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`"
>
```
A single period is too short for plateau detection (the window needs at least four finite points) and renders as a
single step with no visible line. `data` accepts either a 0–1 fraction or a 0–100 percent series — whichever the max
value implies — so a raw percent export renders identically to its fraction form without a manual divide. With a
`locale`, the percent label and every announced number follow that locale's own formatting.
## Why this default [#why-this-default]
Step line plus the full 0–100% range, because retention is a discrete share, not a continuous signal — a smoothed curve
implies between-period values that never existed, and a truncated floor exaggerates or hides the drop. The plateau
marker appears **only** when the mean period-over-period change over the tail falls below half a point — never as
decoration — and the benchmark stays a subordinate dashed ghost, never a second competing line.
## Accessibility [#accessibility]
The accessible name states the final retention and, when detected, the plateau — **"38% retained after 10 weeks; curve
plateaus from week 6."**. The interactive entry steps the periods and announces each period's retention alongside the
benchmark's.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Fraction retained per period (0–1 or 0–100); period 0 is typically 1.0. |
| `benchmark` | `number[]` | Peer/industry curve, drawn as a subordinate dashed ghost. |
| `plateau` | `boolean` | Detect + mark a plateau (default true). |
| `curve` | `"step" \| "smooth"` | Step (default — cohorts are discrete) or smooth (editorial). |
| `unit` | `string` | Period noun for the summary (default 'period'). |
| `label` | `"last" \| "none"` | Final retention in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# RubricStrip (/docs/charts/rubric-strip)
RubricStrip answers "how did this thing score per criterion — with each criterion's importance visible — without a fake
composite number?". Each bar's length is its score and its thickness is its weight share, so the things that matter most
are literally the biggest marks. There is no total bar, and none may be added — the type exists to resist collapsing
quality into one number.
```tsx
`"
>
```
## Install [#install]
```tsx
import { RubricStrip } from "@microcharts/react/rubric-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — model / code-review scorecards, weighted vendor comparison.
* **Avoid for** — a single criterion (Bullet) or parts of a whole (SegmentedBar).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
## Why this default [#why-this-default]
Weight-as-thickness keeps "importance" visible where a table hides it — a light criterion that scores well can't
visually outweigh a heavy one that scores poorly. The summary names the extremes, never a weighted average, because the
whole point is that a single number throws away the structure. A `target` draws one honest pass line across every row
instead.
## Accessibility [#accessibility]
The accessible name names the extremes — **"4 criteria; highest Lint (1), lowest Docs (0.5)."** The interactive entry
roves the criteria, announcing each one's score and its weight share of the total (**"Coverage: 0.78, weight 29% of
total."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, score, weight? }[]` | Criteria; weights default equal. |
| `target` | `number` | Pass target — one tick across all rows. |
| `labels` | `boolean` | Criterion names in the left gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# RugStrip (/docs/charts/rug-strip)
RugStrip answers "where do the raw observations actually sit?". Every tick is one real observation — no binning, no
jitter, no thinning. Stacked duplicates darken (tiered opacity), and `markValue` tells the chart's strongest single
story: one raw value against the field.
```tsx
`"
>
{"Your offer sits "}
{" inside the band."}
```
## Install [#install]
```tsx
import { RugStrip } from "@microcharts/react/rug-strip";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — "you are here" in a pay band, raw spread beside a stat, margin composition under a Sparkline.
* **Avoid for** — more than \~400 observations (a rug promises raw marks and never downsamples; use HistogramStrip) or
trends over time (Sparkline).
## Variants [#variants]
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
Empty data draws a quiet centered axis line — not a blank hole — so the strip still reads as "a rug with nothing on it"
rather than a layout gap.
```tsx
`">
```
A single value (or an all-equal field) has a zero-span domain; the tick renders at the strip's midpoint instead of
dividing by zero.
```tsx
`">
```
A `markValue` outside the observed domain clamps to the nearest edge — it never escapes the box or silently rescales the
field around it.
## Why this default [#why-this-default]
Ticks sit at 35% opacity — singles stay visible while stacks read as density. Because a browser paints one path's stroke
as a single operation, coincident ticks are bucketed into opacity tiers (35% / 60% / 85%) rather than relying on overlap
compositing that never happens. Regular ticks are inset one unit so the full-height accent highlight reads "taller"
without escaping the box.
## Accessibility [#accessibility]
The accessible name is the distribution — **"8 values from 42 to 75, median 59.5."** The interactive entry walks
observations in sorted order and announces each with its rank: **"48 — 2nd of 17."**
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw observations — position = value. |
| `markValue` | `number` | One value emphasized against the field. |
| `orientation` | `"horizontal" \| "vertical"` | Vertical rugs sit beside distributions. |
| `domain` | `[number, number]` | Fix the scale across rows (rugs mislead worst under per-row autoscale). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# SegmentedBar (/docs/charts/segmented-bar)
SegmentedBar answers "what is this made of, and in what proportions?". Segments always sum to the full bar; past
`maxSegments` the tail rolls into a labeled "Other" — nothing is silently dropped. A flat bar beats a donut of the same
data at every size we ship.
```tsx
```
## Install [#install]
```tsx
import { SegmentedBar } from "@microcharts/react/segmented-bar";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — traffic mix per table row, composition in KPI cards.
* **Avoid for** — precise cross-row comparison (MiniBar) or negative parts (Waterfall).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
`label="value"` follows `format`/`locale` — 12400 reads "12.400" under `de-DE` rather than the English "12,400".
`label="percent"` is unaffected: the largest-remainder shares are always plain integers, never locale-formatted, because
a percent point is the same everywhere.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
Past `maxSegments` (default 5) the smallest categories merge into a labeled "Other" — nothing is dropped, only rolled
up, and the interactive entry announces its member count. A single category still renders as one full-width, 100%
segment rather than a special-cased bare bar. Negative values can't exist in a part-to-whole, so they're excluded from
the bar entirely (not clamped to zero) and the component logs a dev warning steering toward Waterfall.
## Why this default [#why-this-default]
Data order is the default — inherent sequences (funnel-like stages, weekday mixes) carry meaning that ranking destroys.
Summary percents use largest-remainder rounding so they always sum to exactly 100; negative values are excluded with a
dev warning (a part-to-whole cannot contain negative parts — use Waterfall).
## Accessibility [#accessibility]
The accessible name is the full composition — **"Chrome 62%, Safari 24%, Firefox 9%, Edge 3%, Other 2%."** The
interactive entry roves segments; the Other segment announces its member count (**"Other: 2%, 2 categories."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Parts of the whole. |
| `maxSegments` | `number` | Rollup threshold — the tail becomes a labeled Other. |
| `order` | `"data" \| "desc"` | Preserve inherent sequences or rank the composition. |
| `label` | `"none" \| "percent" \| "value"` | Centered per segment (deterministic drop-out; default percent). |
| `colors` | `string[]` | Per-segment colours, cycled; overrides --mc-cat-N. Other stays neutral. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Seismogram (/docs/charts/seismogram)
Seismogram answers "when did things happen, and how hard?". Ticks read as a seismograph trace — centered on a midline
and flaring symmetrically: presence is density texture, length is intensity. Long series collapse via max-per-bucket
only — the spike is the chart's whole job, so it always survives, and the summary is always computed from the raw
values, never the buckets.
```tsx
`"
>
```
## Install [#install]
```tsx
import { Seismogram } from "@microcharts/react/seismogram";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — error bursts per service, alert density, activity texture in rows.
* **Avoid for** — level tracking (Sparkline) or labeled events (EventTimeline).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the accessible summary's peak number follows that
locale's own grouping ("12.500" in German, not "12,500"). Tick positions and lengths are unaffected; only the announced
peak is localized.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
(i === 210 ? 9 : i % 17 === 0 ? 2 : 0))}
/>`"
>
```
## Why this default [#why-this-default]
A centered seismograph trace over bottom-anchored bars: the symmetric flare reads as event intensity rather than
inviting the bar-to-bar magnitude comparison the strip is too small to support — and it stays visually distinct from
SparkBar. Barcode mode is documented as presence-only — a uniform tick is not intensity. `anomaly` flags spikes at or
above a threshold in the alert token; the flag is redundant with tick length, never color alone. A quiet strip renders
its baseline at rest, never a blank hole.
## Accessibility [#accessibility]
The accessible name counts the events — **"29 events, peak 11."**, or **"No events."** for a quiet strip. The
interactive entry steps slots with position readouts ("Point 2 of 5: 3."), announces quiet slots as "no data", and
Home/End jump to the first/last event.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | Per-slot event intensity; 0/null = quiet. |
| `mode` | `"intensity" \| "barcode"` | Barcode collapses heights — pure occurrence density. |
| `positive` | `"up" \| "down"` | Polarity coloring of signed ticks. |
| `anomaly` | `number` | Flag spikes: \|v\| ≥ threshold flares in the alert token. |
| `domain` | `[number, number]` | Fixed intensity scale across rows. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# ShiftHistogram (/docs/charts/shift-histogram)
ShiftHistogram answers "did the fix actually change the distribution?". It mirrors two histograms around a center line —
**before** upward (muted), **after** downward (accent) — on **shared bin edges**, with the median shift as the precise
takeaway. Bar heights are per-side proportions on one shared scale, so a bigger sample on one side can't fake a shift.
The mirror carries **identity**, not valence (up isn't "good") — that's the whole point of the symmetry.
```tsx
```
## Install [#install]
```tsx
import { ShiftHistogram } from "@microcharts/react/shift-histogram";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a "the fix, proven" read in a KPI card, before/after distributions in an experiments table, showing a
change is real (not just a mean move).
* **Avoid for** — a single distribution (HistogramStrip) or two labelled arms (ABStrips).
## Variants [#variants]
```tsx
120 + (i % 40) - 20);
const after = Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20);
`"
>
```
```tsx
`"
>
```
The median shift label and accessible summary follow `format`/`locale` — the before median above reads "12.400" under
`de-DE` rather than the English "12,400".
## Edge cases [#edge-cases]
```tsx
120 + (i % 40) - 20);
Math.round(n) + " ms"}
title="Awaiting after sample"
/>`"
>
```
```tsx
`">
```
With one side empty the chart still renders the populated side's bins alone, and the summary names the gap directly —
**"Median 116 ms; no after sample."** — never a fabricated shift. With both sides empty nothing is plottable and the
accessible name says **"No data."**
## Why this default [#why-this-default]
Mirror, because before/after symmetry makes the shift a visible displacement. The only normalization allowed is per-side
proportions — each side's counts over its own n — on one shared height scale, so unequal sample sizes never fake a
shift, and the rule is stated. The bins are shared across both sides always, and the medians are never smoothed or
trimmed. Side identity rides on position (up vs before, down vs after) so it survives grayscale and forced-colors; the
summary names the direction and, when the sample sizes differ, carries both.
## Accessibility [#accessibility]
The accessible name states the median shift plainly — **"Median fell from 116 ms to 92 ms."** — appending "On N / M
samples" when the sizes differ. The interactive entry steps the bins and announces each bin's before/after proportions;
**M** jumps to the two median bins.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ before: number[]; after: number[] }` | The two samples — raw observations, shared bin edges are derived. |
| `bins` | `number` | Shared bin count (default auto, Sturges capped at 12). |
| `mode` | `"mirror" \| "overlay"` | Mirror (default) or after-as-outline over before fill. |
| `seriesLabels` | `[string, string]` | Side identities for the summary (default ['before', 'after']). |
| `label` | `"shift" \| "none"` | Signed median shift in a right gutter. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Slope (/docs/charts/slope)
Slope answers "who rose and who fell between two moments — and did the order change?". Lines stay neutral until you
declare `positive`: a rank change is not automatically good or bad. Both columns share one y-domain — per-column
normalization would fake convergence.
```tsx
`"
>
```
## Install [#install]
```tsx
import { Slope } from "@microcharts/react/slope";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — before/after experiments, rank shuffles, two-moment comparisons.
* **Avoid for** — the path between the moments (Sparkline) or more than 7 categories.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`"
>
```
A row missing one side (`from` or `to` as `NaN`) draws a short dashed stub toward the end it does have, rather than a
full line — there's no second point to connect to, so nothing is interpolated. With a `locale`, both column labels and
the announced values follow that locale's own grouping.
## Why this default [#why-this-default]
A two-point line implies nothing about the path between — the docs steer to Sparkline when the journey matters. End
labels drop deterministically, never by measurement: when the rows are denser than the label font (height ÷ count), and
when the reserved label gutters would squeeze the two columns under \~35% of the width — in which case the reclaimed room
goes back to the lines. Endpoints that would collide inside a column are nudged apart to a full glyph pitch instead of
overlapping. A missing end renders a dashed stub and is announced "incomplete", never interpolated.
## Accessibility [#accessibility]
The accessible name counts directions and leads with the biggest mover — **"3 categories: 2 up, 1 down. Largest change
Mid, up 75%."** The interactive entry finds the nearest line under the pointer and roves categories ordered by their
after-value, announcing each slope (**"East: 40 to 47, up 18%."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; from; to }[]` | Two aligned moments per category. |
| `label` | `"none" \| "value" \| "label" \| "both"` | End labels; dropped deterministically when rows collide. |
| `highlight` | `number \| string` | The one-vs-field editorial read. |
| `positive` | `"up" \| "down"` | Direction valence; unset = neutral ink. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# SparkBar (/docs/charts/sparkbar)
SparkBar renders discrete values as compact bars from a zero baseline. In `winloss` mode it collapses each value to its
sign — a streak you can read at a glance: wins above the mid-line, losses below, and a tie (`0`) as a thin neutral dash
on it.
```tsx
```
## Install [#install]
```tsx
import { SparkBar } from "@microcharts/react/sparkbar";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — discrete magnitudes, win–loss streaks, period-over-period counts.
* **Avoid for** — a continuous trend shape (use [Sparkline](/docs/charts/sparkline)) or hundreds of points, where bars
become noise.
## Sizing [#sizing]
`width` and `height` are viewBox units that also set the rendered pixel box. Omit them and drive the width from CSS for
a chart that fills its container — the viewBox keeps the aspect ratio.
## Variants [#variants]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
The endpoint label and accessible summary both follow `format`/`locale` — 8600 reads "8.600" under `de-DE` rather than
the English "8,600".
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
An empty series renders no bars and an accessible name that says so, rather than a flat or invented baseline. A single
value still renders as one zero-anchored bar — `winloss` mode treats it the same way a longer streak would: a lone win,
loss, or tie.
## Why this default [#why-this-default]
Bars, not a line, because SparkBar's job is discrete magnitudes — deploy counts, daily totals — where each period is its
own thing, not a sampled point on a continuous curve. Zero-anchored always: a bar whose length doesn't start at zero
misstates its magnitude. `winloss` mode goes further and discards magnitude entirely on purpose, collapsing each value
to a three-state streak (win above the mid-line, loss below, tie a thin dash on it) because for a streak, sign is the
whole story and a magnitude axis would just be noise. Sign is doubled by position AND color everywhere signed data
appears, so direction survives forced-colors and never depends on color alone. In `bar` mode the endpoint bar takes the
accent — "where did it land" is usually the first question a run of counts answers; in `winloss` mode it keeps its
win/loss/tie color instead, because there the sign is what matters at every position, endpoint included.
## Accessibility [#accessibility]
Announced from the data — the example above reads:
> Deploys per day. Trending up 125%. Range 2 to 9. Last value 9.
Sign is doubled by position (above or below the baseline), never by color alone, so the chart survives forced-colors and
color-blind viewing.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Values; negatives dip below the baseline. |
| `mode` | `"bar" \| "winloss"` | Magnitude bars, or a win/loss/tie streak (sign only). |
| `gap` | `number` | Empty fraction of each slot (0–0.9). |
| `label` | `"none" \| "last"` | Direct endpoint value label. |
| `positive` | `"up" \| "down"` | "up" (default); "down" flips which sign is good. |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `locale` | `string \| string[]` | BCP 47 locale(s) for the endpoint label and summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Sparkline (/docs/charts/sparkline)
Sparkline draws a compact trend over ordered values. Reach for it when direction and shape matter more than exact points
— inline in a sentence, inside a table cell, or as the quiet backbone of a KPI card.
```tsx
```
## Install [#install]
```tsx
import { Sparkline } from "@microcharts/react/sparkline";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — an inline trend, a table-cell trend, a KPI sparkline, dense dashboards.
* **Avoid for** — part-to-whole comparisons or reading exact category values. Use [SparkBar](/docs/charts/sparkbar) for
discrete magnitudes, or [Bullet](/docs/charts/bullet) for a value against a target.
## Sizing [#sizing]
`width` and `height` are viewBox units that also set the rendered pixel box. Omit them and drive the width from CSS for
a chart that fills its container — the viewBox keeps the aspect ratio.
## Variants [#variants]
Shape, fill, a normal-range band, and dots are all props — same data, different read. Flip any of them to the Code tab.
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
A `null` means "no measurement here" — the line breaks at the gap and never interpolates across it, so an outage can't
masquerade as a smooth trend. A single point has no line to draw; it sits centered in the plot, visible as the default
endpoint dot. An all-equal series renders on the vertical mid-line — a zero-span domain maps to the middle of the range,
so flat data reads as level, not as bottomed-out.
Past `maxPoints` (default 200) the drawn line decimates to a min/max-preserving envelope: every spike keeps its true
position and height, gaps stay gaps, and the summary, dots, and hover values still come from the raw data — only the
path gets lighter. Pass `maxPoints={Infinity}` to opt out.
```tsx
i === 1500 ? 98 : 50 + Math.sin(i / 40) * 24 + ((i * 13) % 7),
)}
/>`"
>
```
## Four homes [#four-homes]
The same chart, sized for four real contexts. Each preview is the public component plus `styles.css` — no docs-only
wrappers. Sentence placements use `mc-inline`; see [Composition](/docs/composition) and [Sizing](/docs/sizing).
## Why this default [#why-this-default]
A bare line plus one accent dot on the last point — no fill, no band, no min/max dots. The shape of the series is the
whole story until you ask for more, and the endpoint dot marks where "now" is. Min/max dots, fill, and the band are
opt-in because at word size they compete with the line for the same few pixels; adding them by default would make every
sparkline noisier for the common case that only needs direction and range. Long series decimate to a min/max-preserving
envelope rather than a naive stride sample, so a single spike at point 1,500 of 2,000 still shows up — silently dropping
it would be the more common bug in a naive implementation, and a chart that hides its own spikes is dishonest by
construction.
## Accessibility [#accessibility]
By default the chart is an `img` whose accessible name is generated from the data. The example above is announced as:
> Weekly revenue. Trending up 200%. Range 3 to 9. Last value 9.
Pass a `title` to name it, or `summary={false}` to make it decorative when the surrounding text already says what it
shows. The interactive entry adds a polite live region that reads each focused point as you arrow through it.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | The series. null/NaN are gaps. |
| `curve` | `"linear" \| "smooth" \| "step"` | Line shape. |
| `fill` | `boolean` | Zero-anchored area under the line. |
| `band` | `[number, number]` | Constant normal-range band. |
| `dots` | `"auto" \| "minmax" \| "none"` | Endpoint or min/max dots. |
| `label` | `"none" \| "last" \| "minmax"` | Direct value labels: endpoint, or the extremes. |
| `maxPoints` | `number` | Line-point cap (default 200); longer series decimate min/max-preserving. |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# SpiralYear (/docs/charts/spiral-year)
SpiralYear answers "how did the year breathe?" It winds a calendar series onto an Archimedean spiral: the angle is the
position in the year (January at 12 o'clock, running clockwise) and each turn outward is the next year. The value
becomes a five-step opacity, so busy stretches read as darker arcs of the spiral. This is a **pattern instrument** —
opacity is the weakest ordered channel, so for an exact day's value reach for `ActivityGrid` or `HeatStrip`.
```tsx
```
## Install [#install]
```tsx
import { SpiralYear } from "@microcharts/react/spiral-year";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — the seasonal shape of a year at a glance, spotting a busy season or a quiet stretch, or a compact "the
year in one square".
* **Avoid for** — reading an exact day's value (`ActivityGrid` / `HeatStrip`), a non-cyclic trend (`Sparkline`), or more
than about three years.
## Variants [#variants]
```tsx
{
const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2));
return i === 29 ? 480 : s; // a summer peak in week 30
});
`"
>
```
`locale` doesn't change any in-chart mark — opacity steps carry the value, never a printed number — but it does localize
the peak and low values named in the accessible summary:
```tsx
{
const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2));
return i === 29 ? 1480 : s;
});
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Month ticks are on by default because without a temporal anchor the spiral is just texture — the ticks are what make
"when" readable. Five steps is the ceiling: ordered opacity supports about five discriminable levels, and more would be
false precision. The spiral **radius encodes time only, never value** — an outer mark is simply a later date, not a
bigger number. Two adjacent turns place different years at the same angle; that radial adjacency is calendar alignment,
not similarity, and the chart never asks you to compare across it. A `null` day leaves a gap in the spiral, distinct
from a faint step-one mark, because missing is not the same as low.
## Accessibility [#accessibility]
The accessible name states the count, the peak, and the low — **"52 weeks; peak 1.480 in week 30, low in week 48."**
(the `de-DE` localized demo). The interactive entry lets you arrow along the spiral chronologically, each mark announced
with its period and value through a polite live region. Because the channel is ordinal opacity, the docs and summary
steer any exact read to `ActivityGrid`.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | One value per day or week (cadence inferred from length). |
| `cadence` | `"day" \| "week"` | Data cadence; inferred from length (≈52 → week, else day) when omitted. |
| `startDate` | `string` | ISO date anchoring index 0 to a calendar angle. |
| `steps` | `3 \| 5` | Opacity quantization (default 5). |
| `monthTicks` | `boolean` | Faint radial month ticks (default true). |
| `mark` | `"dot" \| "arc"` | Dots (default) or short arc segments. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# SpreadBand (/docs/charts/spread-band)
SpreadBand answers "which of two series leads, by how much, and since when?" The signed gap between a subject and its
reference is filled and split at every crossing, so who is ahead and when it last flipped both read at a glance. The
reference whispers: dashed, thinner, neutral — direction is carried by the fill sign and the text, never color alone.
```tsx
```
## Install [#install]
```tsx
import { SpreadBand } from "@microcharts/react/spread-band";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — lead-vs-reference in KPI cards, actual-vs-plan where the flip matters.
* **Avoid for** — 3+ series (SparkGroup), or unpaired series / different units (never dual axes).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`"
>
```
The gutter label and the hover readout follow the `locale`'s grouping — the German build reads the current gap as
**+8.000** and the summary as "Organic leads Paid by 8.000; last crossed at point 5."
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
A `null` in either series is a gap in **both** — the gap is undefined where a reading is missing, so the fill and both
lines break at that index rather than inventing a value. Identical series read "The two series are level — no gap." and
a subject that never falls behind reports "never crossed."
## Why this default [#why-this-default]
One shared domain across both series — comparability is the whole point, so there are no dual axes and no per-series
normalization. The gap is filled and split exactly at the interpolated crossings, so the eye lands on the flip without
hunting. The reference whispers and the leader's sign is stated in text, so direction never rides on color alone.
## Accessibility [#accessibility]
The accessible name states the lead, the current gap, and where the lines last crossed — **"Organic leads Paid by 8;
last crossed at point 3."** Identical series read "The two series are level — no gap." The interactive entry announces
the lead at each point (**"Point 12 of 12: Organic +8 over Paid."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ a: number \| null; b: number \| null }[]` | Paired readings — a is the subject, b the reference. |
| `seriesLabels` | `[string, string]` | Names the two series in the summary and label. |
| `positive` | `"up" \| "down"` | Which lead is the good valence; down flips the fill colors. |
| `label` | `"gap" \| "none"` | Current signed gap in a right gutter (default gap). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# SproutRow (/docs/charts/sprout-row)
SproutRow answers "how mature or healthy is each item in a small set?" Each item is one of four growth stages — **seed →
sprout → leaf → bloom** — and the glyph height is strictly monotonic, so taller always means further along and the
ordering reads without the legend. The stages are discrete and there are exactly four; there are no half-stages, because
a growth metaphor must not fake continuity.
```tsx
```
The key, shown once: **seed → sprout → leaf → bloom**.
## Install [#install]
```tsx
import { SproutRow } from "@microcharts/react/sprout-row";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — account or project maturity across a small set, a health column in a portfolio table, or per-item
lifecycle in a KPI card.
* **Avoid for** — continuous values (MiniBar), trends (Sparkline), or more than about twelve items.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
Empty data draws just the frame. A `null` value renders only the soil tick — visibly distinct from a seed, which is
stage 0 and gets its own glyph. Values outside `[0, 3]` round and clamp to the nearest real stage (seed or bloom) rather
than drawing an out-of-bounds or fractional glyph.
## Why this default [#why-this-default]
Labels are off by default because the row usually sits beside its own row label, and at micro scale the glyphs alone
carry the ordering. The one-line key — seed → sprout → leaf → bloom — appears once above the demos, and after that the
height does the work. A `null` value is a real gap: it draws only the soil tick, distinct from a seed, because "no data
yet" is not "just planted". Presets recolor the glyphs but never reshape a stage; the four stages are fixed and never
interpolated.
## Accessibility [#accessibility]
The accessible name summarizes the row — **"4 items; 1 at bloom, 1 at seed."** The interactive entry roves the items
with ←/→ (or hover), announcing each as **"Acme: bloom, stage 4 of 4."** — the stage name plus a 1-of-4 index — and
reads "…: no data." for a missing item. A ring lifts the focused glyph.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, value }[]` | value = stage 0–3. |
| `labels` | `boolean` | Category labels under the slots. |
| `label` | `"none" \| "value"` | Print the stage number above each glyph. |
| `step` | `number` | Horizontal spacing between glyph slots (default 16; widens for labels). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# StackedArea (/docs/charts/stacked-area)
StackedArea answers "how is the composition shifting over time?" Layer thickness is share; the stack always sums to
100%. Three series is the hard cap — beyond that, thickness at 16 px stops being readable.
```tsx
```
## Install [#install]
```tsx
import { StackedArea } from "@microcharts/react/stacked-area";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — traffic/revenue mix in KPI cards, share-shift stories in sentences.
* **Avoid for** — 4+ series, or exact values over time (SparkGroup of Sparklines).
## Variants [#variants]
```tsx
`"
>
```
`format`/`locale` control the share numbers — the accessible summary and endpoint labels follow the locale's own percent
formatting ("66 %" in German, with a space before the sign, not "66%"). The stack itself never changes shape; only the
announced and printed numbers localize.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
A single column still stacks normally — share is well-defined from one point, it's just a vertical slice with no width
to run across. A series pinned at zero collapses to a zero-height layer rather than distorting the others: the remaining
series still sum to 100% of what's left.
## Why this default [#why-this-default]
Values are normalized to shares before stacking, so the stack reads composition — not magnitude and composition tangled
together. `order="asc"` puts the smallest series on top, where curvature distorts thickness the least.
## Accessibility [#accessibility]
The accessible name is the share-shift read — **"3 series over 12 points; Mobile leads at 66% share."** The interactive
entry announces every layer at once (**"Point 8 of 12: Mobile 56%, Web 36%, API 8%."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; values }[]` | ≤ 3 series (hard cap). |
| `mode` | `"stacked" \| "ridge"` | Ridge = same stack, overlapping-crest skin. |
| `order` | `"data" \| "asc"` | "asc" puts the smallest series on top (least distortion). |
| `label` | `"last" \| "none"` | Endpoint share labels per series (deterministic drop-out). |
| `labelAt` | `number` | Column whose shares feed label="last" (default: final column). The interactive entry passes the focused column so the labels track the crosshair. |
| `curve` | `"linear" \| "smooth" \| "step"` | Line interpolation (default linear); ridge forces smooth. |
| `colors` | `string[]` | Per-series colours, cycled; overrides --mc-cat-N. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# StarSpoke (/docs/charts/star-spoke)
StarSpoke answers "what is this entity's profile across a few metrics — and which entity in a set is the odd one out?".
Spokes radiate from the center and their length encodes each value. There is no connecting polygon, ever: the enclosed
area of a radar chart lies about magnitude and axis order, and the research is clear that contour-free wins for spotting
outliers.
```tsx
```
## Install [#install]
```tsx
import { StarSpoke } from "@microcharts/react/star-spoke";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — entity profiles in small multiples, skill / capability comparison.
* **Avoid for** — fewer than 3 metrics (PairedBars) or precise values (MiniBar).
## Variants [#variants]
```tsx
`"
>
```
`locale` doesn't change any in-chart mark — tip labels are metric names, never formatted numbers — but it does localize
the extremes named in the accessible summary:
```tsx
`"
>
```
## Why this default [#why-this-default]
Contour-free is the point: a connecting polygon is decoration that changes the read, exaggerating area for high-variance
profiles and hiding the individual spoke lengths that actually matter for comparison. One shared domain governs every
spoke in a glyph — mixed-domain metrics are the caller's normalization to state, so a set of small multiples stays
honestly comparable.
Every spoke sits at a fixed clock position (first at 12 o'clock, clockwise), so the axis order never shifts between
instances — that fixed order, plus the default-on guide spokes, is what keeps a profile readable even where a metric's
label doesn't seat. Tip labels seat at the rim, not the value tip, so a low-value spoke never drags its label into the
hub. A label is dropped only when its estimated width exceeds the whole glyph; anything narrower is clamped into the
reserved label ring instead, which keeps rim labels at distinct angles rather than dropping them.
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`">
```
## Accessibility [#accessibility]
The accessible name names the extremes — **"5 metrics; highest Speed (0.9), lowest Cost (0.3)."** The interactive entry
rotates focus through the spokes with ←/→, announcing each metric and value.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label, value }[]` | 3–8 metrics on a shared domain. |
| `dots` | `"tips" \| "none"` | `"tips"` draws endpoint dots to sharpen the outlier read. |
| `guides` | `boolean` | Full-length guide spokes (read-back scaffold). |
| `compare` | `number[]` | Muted ghost baseline spokes. |
| `labels` | `boolean` | Spoke labels at the tips (default true; drop out below size 44). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# StationGlyph (/docs/charts/station-glyph)
StationGlyph packs a full point observation into one character, the way a meteorologist's station model does: the center
disc fills with sky cover, a wind barb gives direction and quantized speed, and up to three corner numerals carry
temperature, dew point, and pressure. Five fields, one glyph, no legend — dense enough to tile a map, legible enough to
read in a table cell.
```tsx
```
## Install [#install]
```tsx
import { StationGlyph } from "@microcharts/react/station-glyph";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a dense weather station model, or any multi-field reading that must fit one cell.
* **Avoid for** — a single value (Delta) or a trend over time (Sparkline).
## Variants [#variants]
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
`format`/`locale` flow through every corner numeral (temp, dew point, pressure) and the wind speed and numerals named in
the accessible summary — one formatter, one locale, no per-field overrides.
## Why this default [#why-this-default]
Each field rides its own channel, so nothing is inferred from a shared one: sky cover is disc area, wind is the barb
(direction as angle, speed quantized into barbs so the per-barb quantum is honest), and the numerals are placed where a
forecaster expects them. Wind speed is quantized rather than drawn to scale for the same reason WindBarb quantizes it —
a barb count reads exactly, a tiny length does not. Absent fields simply don't render — no barb, no numeral, and no stop
for the keyboard. The one field with no "absent" state is the disc itself: omitting `cloud` looks and reads exactly like
a clear sky.
## Accessibility [#accessibility]
The accessible name is the whole observation — **"KJFK, wind northwest 45; sky overcast, 4° / 2°, 988."** The
interactive entry roves the *present* fields with ←/→ so a screen-reader user can step through station, wind, sky,
temperature, dew point, and pressure one at a time — an omitted prop is not a stop — and Escape clears the focus back to
the whole-observation reading.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `cloud` | `number` | Sky cover 0–1; fills the disc. |
| `wind` | `{ direction, magnitude }` | Barb direction + speed. |
| `step` | `number` | Wind-barb quantum — each full barb (default 10). |
| `temp` | `number` | Upper-left numeral. |
| `dewpoint` | `number` | Lower-left numeral. |
| `pressure` | `number` | Upper-right numeral. |
| `station` | `string` | Top-left identifier. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# StatusDot (/docs/charts/status-dot)
StatusDot answers "what state is this thing in right now?". Every state pairs a distinct silhouette with a semantic
color — filled circle, triangle, diamond, hollow ring, half-filled circle — so two states can never collapse into the
same mark for the 1-in-12 colorblind readers a colored disc fails.
```tsx
{"The API is "}
{" operational."}
```
## Install [#install]
```tsx
import { StatusDot } from "@microcharts/react/status-dot";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — service lists, inline state in a sentence, monitoring rows.
* **Avoid for** — quantities, trends, or vocabularies past \~6 states (marks stop being memorable).
## The state contract [#the-state-contract]
| status | glyph | token |
| ------- | ------------------ | --------------- |
| `ok` | filled circle | `--mc-positive` |
| `warn` | triangle | `--mc-cat-1` |
| `error` | diamond | `--mc-negative` |
| `off` | hollow ring | `--mc-neutral` |
| `busy` | half-filled circle | `--mc-accent` |
The pairing is a contract: `color` recolors a state but never reshapes it, and a theme must never let two states share a
silhouette.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
## Why this default [#why-this-default]
Shape+color pairing beats a colored disc: the five silhouettes stay distinct in grayscale, print, and forced-colors,
where color-only status dots all become the same gray circle. An unknown status key renders the `off` ring with a dev
warning — never a plausible-looking wrong state.
## Accessibility [#accessibility]
The accessible name is the state's label — **"Status: ok."** — composed with your `title` ("API. Status: ok."). The
interactive entry announces state changes through a polite live region and stays quiet on mount. `pulse` is a CSS halo
that disappears entirely under `prefers-reduced-motion`.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `status` (required) | `string` | Built-in ok \| warn \| error \| off \| busy, or a key of states. |
| `pulse` | `boolean` | Live-now halo (reduced-motion-gated). |
| `states` | `Record` | Extend the vocabulary; the shape+color pairing is preserved. |
| `color` | `string` | Recolors the active state; never reshapes it. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# StreakSpark (/docs/charts/streak-spark)
StreakSpark answers "how long is the current run, and how does it compare to the record?". A sequence of pass/fail
outcomes collapses into **runs** — each run a bar whose width is its length on one shared scale. Streak runs sit low and
translucent, breaks sit thin and saturated, and the **current** run is the loud accent bar at the right. The record
streak wears a small triangle tick, so "are we near our best" reads without counting.
```tsx
`"
>
```
## Install [#install]
```tsx
import { StreakSpark } from "@microcharts/react/streak-spark";
// 1 = passing build, 0 = failing build
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — pass/fail run histories, uptime and incident-free streaks, the current run against a record.
* **Avoid for** — a continuous magnitude (SparkBar) or a single completion ratio (Progress).
## Variants [#variants]
`data` accepts booleans, `0`/`1`, or any numbers (they pass on `> 0`, or on `>= threshold`). A `null` is a gap: it
breaks the run and starts a fresh one. Everything else is a prop.
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
= threshold — e.g. daily uptime %, target 99.5
`"
>
```
```tsx
1)}
format={{ useGrouping: true }}
locale="de-DE"
/>`"
>
```
With a `locale`, the count label and the accessible summary follow that locale's own grouping — a 1,204-build streak
reads **"1.204"** in German. The outcome words come from `strings`, so the whole summary localizes together.
## Edge cases [#edge-cases]
```tsx
`">
```
A lone outcome is one current run of length 1, and it is trivially its own record — the summary reads **"Current run 1
passing, unbroken."**
```tsx
`">
```
When nothing passes there is no completed streak: the strip is one saturated break run, no triangle tick, and the
summary reads **"Current run 5 failing; no completed streak."**
## Why this default [#why-this-default]
Runs, not per-outcome ticks, because the decision is about *length* — "how long have we held" and "how does that compare
to our best". Width encodes run length on one shared scale, so a wide bar is a genuinely long run and the eye compares
them honestly. Height and opacity encode run *type* (streak, break, current) — never magnitude — so the ribbon never
implies a size it doesn't have. The record tick and the accented current bar answer the two questions the raw list
can't, at a glance.
## Accessibility [#accessibility]
The accessible name states the current run, the record, and how often the streak broke — **"Current run 3 passing;
record 9; broke 2 times."** The interactive entry roves runs with ← →, announcing each run's length, outcome, and
whether it is the record (**"Run 3 of 5: 4 passing."**). Direction is carried by height, opacity, and color together,
never color alone.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(boolean \| number \| null)[]` | Outcomes; null is a gap that breaks the run. Numbers pass on > 0. |
| `positive` | `"up" \| "down"` | Which outcome is the streak: pass (up) or fail (down). |
| `threshold` | `number` | With numeric data, values ≥ threshold pass. |
| `label` | `"current" \| "both" \| "none"` | Count labels: the current run, the record too, or neither. |
| `title` | `string` | Accessible name; joins the auto summary. |
| `summary` | `string \| false` | Override or disable the auto summary. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TallyMarks (/docs/charts/tally-marks)
TallyMarks answers "how many?" the way a human actually counts — in four-and-strike clusters of five. The count reads
back exactly: no scale to calibrate, no color to decode. Past `total` the marks stop growing and a `+N` numeral tells
the truth, so a cell never blows out its width. Marks are never resized to fit — width grows with the count until the
cap, then the numeral takes over.
```tsx
```
## Install [#install]
```tsx
import { TallyMarks } from "@microcharts/react/tally-marks";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a small running count in a sentence or cell, a live event or score counter, or an editorial
hand-tallied context.
* **Avoid for** — large magnitudes (MiniBar), trends over time (Sparkline), or proportions (Progress).
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
The ruled pen is the default because product tables want precision first — evenly ruled strokes count back fastest. The
`drawn` pen adds a seeded, deterministic jitter (same count always renders identically, on the server and after
hydration) for the editorial voice, and it only perturbs how each stroke is drawn — the count is never touched. Past
`total`, the default `overflow="numeral"` appends a `+N` so the true total survives even when the marks can't;
`overflow="clamp"` suppresses the numeral for dense columns, and the accessible name still carries the exact count.
## Accessibility [#accessibility]
The accessible name is the exact count — **"17 counted."** — always the true value, even when the marks overflow to a
`+N` numeral or clamp. The interactive entry announces the new total through a polite live region on change and draws
newly added marks in with a brief, reduced-motion-gated sweep. A count has no sub-parts, so focus reads the summary and
there is no cursor to move.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The count. Floored; negatives clamp to 0. |
| `total` | `number` | Marks drawn before overflow (default 25). |
| `overflow` | `"numeral" \| "clamp"` | numeral appends +N; clamp stops drawing. The summary always keeps the true count. |
| `pen` | `"ruled" \| "drawn"` | Hand-drawn jitter for editorial contexts. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TapeGauge (/docs/charts/tape-gauge)
TapeGauge answers "what is it now, is that OK, and where is it heading?" for a single live reading — airspeed,
throughput, temperature, queue depth. The scale scrolls past a fixed center pointer, so the value stays parked where
your eye already is; semantic zones mark the safe, caution, and danger bands; and a stack of chevrons encodes the rate
of change as a channel entirely separate from the level. It is the aviation primary-flight-display tape, shrunk to a
word.
```tsx
```
## Install [#install]
```tsx
import { TapeGauge } from "@microcharts/react/tape-gauge";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a live changing reading with a safe/caution band; value plus trend at a glance.
* **Avoid for** — a history you want to scan (Sparkline) or a single static number (Delta).
## Variants [#variants]
```tsx
// label="none" drops the in-chart readout for an external number
`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, both the in-gauge readout and the accessible summary
follow that locale's own grouping ("14.200" in German, not "14,200").
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The value is pinned at the pointer and the scale moves instead, because on a live instrument the one thing you never
want to hunt for is the current reading. Rate lives on its own channel — chevrons, not position — so a fast climb and a
high value are never confused for one another, the failure mode that sinks a naïve gauge. The visible span is fixed
while the value updates, so a jump reads as motion of the scale rather than a silent rescale that hides how far it
moved.
## Accessibility [#accessibility]
The accessible name states the level, the trend word, and the containing zone — **"Now 142, rising; in the 130–150
zone."** The interactive entry re-announces the reading through a polite live region, throttled so a rapidly changing
value never floods a screen reader, and focus reads the full summary on demand.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The current level; parked at the pointer. |
| `rate` | `number` | Signed units/tick; drives the chevrons. |
| `zones` | `{ from, to, tone }[]` | Semantic bands on the scale. |
| `span` | `number` | Visible scale extent; fixed while live. |
| `rateTiers` | `[number, number]` | Thresholds for 1 and 2 chevrons (default [span/60, span/15]). |
| `orientation` | `"vertical" \| "horizontal"` | Tape direction (default vertical). |
| `announceEvery` | `number` | (interactive) Minimum ms between live-region announcements as the value streams (default 5000). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Thermometer (/docs/charts/thermometer)
Thermometer answers "where does this value sit on a calibrated range, and how close to the goal?" A column fills a
ticked tube; the ticks calibrate the read, which is what buys the precision. The fill always anchors at the bottom of
the range — never re-zeroed, never log — and an optional target line crosses the tube to mark the goal. The bulb is
instrument chrome (always full); it is not data, so its area means nothing.
```tsx
```
## Install [#install]
```tsx
import { Thermometer } from "@microcharts/react/thermometer";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — a fundraising or goal-progress read, a capacity or utilization gauge in a cell, or any value against a
stated range.
* **Avoid for** — trends (Sparkline), proportions of a whole (SegmentedBar), or many series.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`"
>
```
The accessible summary's value, domain, and target all go through the same locale-aware formatter, so "7,234" becomes
"7.234" in German grouping.
## Why this default [#why-this-default]
Vertical with the bulb is the default because the instrument metaphor is the point — a filling tube reads as "toward the
goal" instantly. Horizontal exists for table cells where a vertical tube cannot fit, and `bulb={false}` drops the
reservoir when the metaphor runs too warm for the context. The `domain` defaults to `[0, 100]` because a calibrated
instrument needs a stated range; auto-fitting would silently move the scale under the reader. A value beyond the domain
clamps the fill but the accessible name always reports the true number — the reading is never silently clipped.
## Accessibility [#accessibility]
The accessible name states the value on its scale — **"62 on a 0–100 scale."** — and appends the goal when a target is
set. The interactive entry reveals the exact value on hover or focus, glides the fill to its new level with a
reduced-motion-gated transition, and announces each change through a polite live region.
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | The reading. |
| `target` | `number` | A goal tick across the tube. |
| `domain` | `[number, number]` | The calibrated range (default [0, 100]). |
| `ticks` | `number \| number[]` | Tick count or explicit values. |
| `orientation` | `"vertical" \| "horizontal"` | Horizontal fits table cells. |
| `bulb` | `boolean` | Draw the reservoir bulb (default true). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TimeInRange (/docs/charts/time-in-range)
TimeInRange answers "how much of the period was the metric inside its acceptable corridor — and which side did it miss
on?". Zones sit in a fixed semantic order — below, in, above — so the strip is read by position first and color second.
The in-range percent is the one number that matters.
```tsx
```
## Install [#install]
```tsx
import { TimeInRange } from "@microcharts/react/time-in-range";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — SLO / uptime corridors, glucose-style time-in-range, thermal or budget bands.
* **Avoid for** — ranking parts (SegmentedBar) or a single ratio (Progress).
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Zone order is semantic and immutable — it is never sorted by magnitude, because the position of a miss (too low vs too
high) is half the reading. Severity tiers are drawn with the same hue at greater ink weight, so the strip never relies
on color alone. Percent labels round with largest-remainder rounding so they always sum to 100, and the summary reads
the same integers as the label.
## Accessibility [#accessibility]
The accessible name leads with the headline and then the misses — **"72% in range, 7% below, 15% above, 2% severe low,
4% severe high."** The interactive entry roves the zones; each announces its share (**"in range: 72%."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `TimeInRangeDatum` | Counts or fractions; normalized to 1. |
| `orientation` | `"horizontal" \| "vertical"` | Vertical suits clinical columns and KPI cards. |
| `label` | `"in" \| "all" \| "none"` | The in-range headline, a full audit, or clean. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TokenConfidence (/docs/charts/token-confidence)
TokenConfidence answers "which parts of this generated text should I double-check?". The text itself is the chart:
confidence becomes a typographic underline beneath each token, in three discrete tiers — confident tokens get no mark at
all, so reading stays primary and only the uncertain words draw the eye.
```tsx
`"
>
```
## Install [#install]
```tsx
import { TokenConfidence } from "@microcharts/react/token-confidence";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — LLM answers in chat or transcripts, flagging text to review.
* **Avoid for** — numeric confidence auditing (use CalibrationStrip) or a single score (Delta).
## Sizing [#sizing]
The text is the chart, so it inherits the surrounding font size — there is no `width`/`height`. Set
`style={{ fontSize }}` or let it flow inline with your prose.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
Every token clears the `hi` threshold, so nothing is flagged — the sentence renders as plain, unmarked text. That
absence of marks is the finding: nothing here needs a second look.
```tsx
`">
```
An empty `data` array renders nothing and reports **"No tokens."** to assistive tech.
## Why this default [#why-this-default]
The tiers are discrete — confident, unsure, guessing — never a continuous gradient, because people calibrate
categorically: a smooth colour ramp reads as noise, but "double-check this word" reads instantly. Confident tokens carry
no mark at all, and the two flagged tiers differ in **stroke style** as well as colour — unsure is a solid underline,
guessing a dotted one — so the tier never depends on colour alone. For auditing exact probabilities, reach for
[CalibrationStrip](/docs/charts/calibration-strip) instead.
## Accessibility [#accessibility]
The accessible name is the tier tally — **"4 tokens: 1 confident, 1 unsure, 2 guessing."** The interactive entry gives
each flagged token a roving tab stop; ←/→ move between them (skipping confident tokens), announcing each one's tier and
confidence.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ token, confidence }[]` | Tokens + confidences. |
| `tiers` | `readonly [number, number]` | lo/hi thresholds — the only tuning. |
| `show` | `"flagged" \| "all"` | All also hairlines confident tokens. |
| `legend` | `boolean` | Appends the 1-line inline key. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TraceFold (/docs/charts/trace-fold)
TraceFold answers "where did the latency go — which spans, at which depth, on the path that actually determined the
total?". Each span is a rect placed by its start time, sized by its duration, and stacked by its nesting depth. The
critical path — the chain of spans that bound the total — is accented and everything else muted, so the answer to "which
spans mattered" is immediate.
```tsx
\`\${Math.round(n)} ms\`}
title="Request trace"
/>`"
>
```
## Install [#install]
```tsx
import { TraceFold } from "@microcharts/react/trace-fold";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — request traces, flame charts, p95-exemplar latency breakdowns.
* **Avoid for** — a single duration (Bullet) or a time series (Sparkline).
## Variants [#variants]
```tsx
\`\${Math.round(n)} ms\`}
/>`"
>
```
```tsx
`"
>
```
`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the accessible summary's numbers follow that locale's
own grouping and decimal marks ("14.868 ms" in German, not "14,868 ms"). In-rect text stays the plain span label either
way; only the announced numbers are localized.
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`"
>
```
## Why this default [#why-this-default]
Accenting the critical path because "which spans mattered" is the question a uniform flame strip doesn't answer — the
eye goes straight to the chain that set the total. Widths are durations on one linear shared time scale, never per-row
normalized, so a wide span is genuinely a slow span. The only documented width distortion is a one-unit floor for
zero-duration spans, and the critical-path walk is deterministic (ties resolve to the earliest start).
## Accessibility [#accessibility]
The accessible name names the biggest span and its path status — **"4 spans over 214 ms; longest db.query (86 ms) on the
critical path."** The interactive entry roves spans within a depth with ←/→ and between depths with ↑/↓, announcing each
span's duration, share of the total, depth, and whether it is on the critical path.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `Span[]` | Flat span list; parent = index. |
| `emphasis` | `"critical" \| "none"` | Mute non-critical spans, or uniform. |
| `labels` | `boolean` | Width-gated in-rect labels. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TreeRings (/docs/charts/tree-rings)
TreeRings answers "how did growth accumulate, period over period?" like the rings of a tree: the oldest period is at the
centre and each new period adds a ring outward, whose **thickness** is that period's value. The channel is thickness,
not area — equal thickness at a larger radius spans more area (the ring illusion), so read the thicknesses, not the
wedges; exact per-period reads steer to `SparkBar`.
```tsx
```
## Install [#install]
```tsx
import { TreeRings } from "@microcharts/react/tree-rings";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — account or company age at a glance, a cohort-age marker in a table cell, or a per-period growth story
in a KPI card.
* **Avoid for** — exact per-period reads (SparkBar), many periods (over twenty-four), or non-cumulative series.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
## Why this default [#why-this-default]
Stroke rings are the default because at twenty-four pixels hairlines keep the disc quiet and let the accent ring — the
current period, drawn 1.5× heavier in the accent color (weight and color, never color alone) — stay loud. The channel is
radial **thickness**, and the docs say so explicitly, because equal thickness at a larger radius spans more area.
TreeRings never enforces a minimum visual thickness: a near-zero period looks near-zero, and a zero-value period
collapses its two boundaries onto each other. Pass `total` for the cohort-age story — the disc then fills only
Σdata/total of the radius, so a young account reads as part-grown.
## Accessibility [#accessibility]
The accessible name summarizes the growth — **"8 periods; latest 14, biggest 22 in period 5."** The interactive entry
steps the rings from the centre out with ←/→ (or hover), announcing each period as "Year 5: 22."
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Per-period growth, oldest first. |
| `highlight` | `"last" \| "none" \| number` | Which period's ring to pick out. |
| `total` | `number` | Expected lifetime Σ — the disc fills only Σdata/total. |
| `rings` | `"stroke" \| "fill"` | Boundary rings (default) or filled annuli. |
| `periodWord` | `string` | Singular period noun for the summary (default "period"). |
| `unit` | `string` | Plural period noun for the summary (default "periods"). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# TrendArrow (/docs/charts/trend-arrow)
TrendArrow answers "which way is this moving?" at glyph size. The shape *is* the direction — up, down, or a flat bar —
and color only reinforces it, so the read survives grayscale, print, and forced-colors. When the magnitude matters,
reach for [Delta](/docs/charts/delta) instead.
```tsx
`"
>
{"Latency "}
{" improved again this week."}
```
## Install [#install]
```tsx
import { TrendArrow } from "@microcharts/react/trend-arrow";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — table direction columns, dense dashboards, inline movement cues.
* **Avoid for** — exact magnitudes (Delta) or series shape (Sparkline).
## Sizing [#sizing]
The glyph is a fixed 16-unit box that scales like an image — size it with CSS. With `showValue` the viewBox widens to
reserve a gutter for the number, so text never paints outside the chart.
## Variants [#variants]
Three glyph weights for three densities, an honest noise floor, and the number when the glyph alone is too terse.
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
`format`/`locale` reach `showValue`'s number and the accessible summary's magnitude together — "12,483" becomes "12.483"
in German grouping.
## Why this default [#why-this-default]
The arrow (shaft + head) stays legible at 8 px and in forced-colors where lighter marks blur; the glyph never scales
with magnitude — an arrow twice as long would be a lie at this precision. `flatBand` exists so tiny wiggles don't read
as movement: declare your noise floor instead of letting every ±0.1% look like a trend.
## Accessibility [#accessibility]
The default accessible name is the factual direction — **"Up 12%."**, **"No change."** within the flat band, **"No
data."** for non-finite input. `positive="down"` flips only the color, never the glyph or the words. The interactive
entry announces direction changes through a polite live region and pulses the glyph (gated on reduced motion).
This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Signed change; sign → direction, magnitude only via showValue/summary. |
| `flatBand` | `number` | Noise floor: \|value\| ≤ flatBand renders the flat glyph. |
| `glyph` | `"arrow" \| "triangle" \| "chevron"` | Mark weight: default legibility, dense cells, inline text. |
| `showValue` | `boolean` | Append the formatted value in a right gutter. |
| `positive` | `"up" \| "down"` | Which direction is good (colors only, never the glyph). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# VolumeProfile (/docs/charts/volume-profile)
VolumeProfile answers "at which *level* did activity concentrate — not when?". It is a histogram turned perpendicular to
the usual trend axis: the level runs vertically and bars extend horizontally by the mass of activity at each level. The
modal level — the point of control, or POC — is accented, and the value area (the levels holding most of the activity)
is shaded.
```tsx
`"
>
```
## Install [#install]
```tsx
import { VolumeProfile } from "@microcharts/react/volume-profile";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — volume-at-price, level-of-activity distributions, load by tier.
* **Avoid for** — a time series (Sparkline) or when the timing is what matters.
## Variants [#variants]
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
A perpendicular histogram because "where" is a different question from "when", and no time-axis chart can answer it. The
value area is a stated convention — `valueArea` defaults to `0.7`, and the fraction is stated in the summary — not an
implied confidence interval. The modal bin is the only accented mark, because "where did activity pile up" is the one
question the shape is built to answer.
## Accessibility [#accessibility]
The accessible name states where activity concentrates — **"Activity concentrates at 142.33 (POC); 70% within
140.33–145.67."** (the right-aligned demo's 5-level profile, default 12 bins). An even distribution reads **"Activity is
evenly spread."** The interactive entry roves the levels with ↑/↓, announcing each level's share of activity and
flagging the POC.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ level, weight }[] \| number[]` | Activity mass per level, or raw levels. |
| `valueArea` | `number` | Mass fraction of the shaded value area (0.7). |
| `align` | `"left" \| "right"` | Which way bars grow. |
| `label` | `"poc" \| "none"` | The POC level beside the accent bar. |
| `bins` | `number` | Number of histogram bins (default 12). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Waterfall (/docs/charts/waterfall)
Waterfall answers "how did the deltas compose into the total?" — the P\&L bridge, in a cell. Sign is encoded by vertical
direction from the running level AND by valence color, never color alone.
```tsx
```
## Install [#install]
```tsx
import { Waterfall } from "@microcharts/react/waterfall";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — P\&L bridges in table cells, net-change decomposition in KPI cards.
* **Avoid for** — unordered category comparison (MiniBar), or more than \~8 steps.
## Variants [#variants]
```tsx
`"
>
```
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
## Why this default [#why-this-default]
The zero-anchored total bar stays on by default: a waterfall without a grounded total is unverifiable — floating bars
alone can't be checked against reality. Hairline connectors carry the running level between steps without adding ink.
## Accessibility [#accessibility]
The accessible name states the endpoints and the split — **"From 60 to 87 over 5 steps: +65 gains, −38 losses."** The
interactive entry steps through the bridge (**"Refunds: −12, running 108."**).
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ label; value }[]` | Signed deltas in order. |
| `open` | `number` | Opening level (prior-period close). |
| `totalBar` | `boolean` | Zero-anchored closing total bar (default on). |
| `positive` | `"up" \| "down"` | "down" = decreases are good (cost breakdowns). |
| `label` | `"none" \| "delta"` | "delta" (default) prints each step's signed value in a band below the plot; the biggest movers win when labels would collide. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# Waveform (/docs/charts/waveform)
Waveform answers "what is the shape of a high-frequency signal — where are its spikes and silences — at word width?". It
compresses with max-per-bucket, never a mean, so a single spike survives the squeeze to a sparkline width. The peak is
disclosed in the accessible summary.
```tsx
(i === 126 ? 0.82 : Math.sin(i / 3) * 0.15 + Math.sin(i / 11) * 0.35) *
(1 - Math.abs(i - 100) / 260),
)}
title="Voice memo"
/>`"
>
```
## Install [#install]
```tsx
import { Waveform } from "@microcharts/react/waveform";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — voice-memo / audio scrubbers, high-frequency log or request volume.
* **Avoid for** — exact values (Sparkline) or categorical state (Hypnogram).
## Variants [#variants]
```tsx
Math.sin(i / 3) * 0.4 * (1 - Math.abs(i - 100) / 220))}
mode="envelope"
/>`"
>
```
```tsx
`">
```
With a `locale`, the announced peak follows that locale's own decimal mark — "0,82" in German, not "0.82".
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
A single sample renders as one bucket, top-to-bottom. All-zero data renders every bucket at the shared 0.4-unit "silent"
tick height, and the accessible summary says **"Silent."** rather than reporting a zero peak. A `null` in the data
behaves the same as a true zero for any bucket where it's the only sample — `maxPerBucket` skips non-finite values when
picking the bucket's peak, and a bucket with nothing finite in it falls back to that same silence tick, so a genuine gap
and real silence render identically here (unlike Sparkline's line break).
## Why this default [#why-this-default]
Bars with max-per-bucket are the only compression that can't hide an incident — averaging a bucket would erase the very
spike someone is looking for. The auto domain is symmetric ±max|data|, which reads shape honestly and discloses the peak
in the summary; for absolute loudness comparison across rows, pass an explicit shared `domain` so quiet data is never
silently rescaled to look loud.
## Accessibility [#accessibility]
The accessible name discloses the peak — **"Peak 0.398 at 50% through 200 samples."** Pure silence reads **"Silent."**
rather than blank. The interactive entry roves the buckets, announcing each bucket's position and peak amplitude.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Amplitude samples; negatives allowed. |
| `progress` | `number` | 0–1 played fraction; left buckets tint accent. |
| `mode` | `"bars" \| "envelope"` | Envelope draws the min/max area. |
| `mirror` | `boolean` | Mirror around center; false for magnitude-only. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# WinProbWorm (/docs/charts/win-prob-worm)
WinProbWorm answers "who's winning, and when did the lead flip?". It plots one win-probability series on a **fixed 0–100
axis that is never truncated** — the honesty rule for a probability — and splits the worm at the 50% line: while your
side leads the line reads accent, when it trails it reads neutral, and a dot marks each lead change. Because a
win-probability curve is a modelled read, the summary always says so, "per the supplied model".
```tsx
```
## Install [#install]
```tsx
import { WinProbWorm } from "@microcharts/react/win-prob-worm";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## When to use it [#when-to-use-it]
* **Good for** — live win or election probability where the lead flips, a modelled forecast whose crossings are the
story, and any 0–100 probability you must not truncate.
* **Avoid for** — a raw score or margin (Sparkline), or a value that isn't a bounded 0–100 probability.
## Variants [#variants]
```tsx
`"
>
```
At a taller size the biggest single momentum swing gets a hair connector and a signed delta; drop it with
`markSwing={false}`, and drop the endpoint probability with `label="none"`.
```tsx
`"
>
```
## Edge cases [#edge-cases]
```tsx
`"
>
```
```tsx
`"
>
```
```tsx
`">
```
With a `format` and `locale`, the endpoint label and the summary format their numbers in that locale's own decimal mark
— `[50, 55.5, 61.2, 58.4, 63.5]` in `de-DE` ends at **"63,5%"**. Values outside 0–100 are clamped to the axis and
dev-warn — a probability cannot exceed 100. A constant series has no crossing and no swing: an all-50 series reads
"even… throughout", any other constant reads "…holds X% throughout". A `null` breaks the worm into disconnected runs the
way a gap should; a single point has no line to draw, so it renders just the endpoint dot and its label; `data={[]}`
renders the frame with the "no data" summary.
## Why this default [#why-this-default]
The fixed 0–100 axis is the whole point: a win probability that truncated its own scale would exaggerate a lead, so the
axis never moves and a 2-point edge always looks like a 2-point edge. The 50% split does the reasoning for the reader —
accent while your side leads, neutral while it trails, a dot at each flip — so "who's ahead, and did it ever change?" is
answered before the second glance. The endpoint carries the current number, and the biggest momentum swing is marked
only when there's room for it. The worm is a modelled read, never a fact, so its accessible name is framed "per the
supplied model" and the chart never editorializes the forecast.
## Accessibility [#accessibility]
The accessible name states the current leader, their probability, the number of lead changes, and the biggest momentum
swing — **"Per the supplied model, home leads at 98%; 3 lead changes, biggest swing +17 at point 8."** — or, for a
constant series, "Per the supplied model, even at 50% throughout." The interactive entry roves the points with ←/→
(Home/End jump to the ends), each announcing the leader and probability at that point, with a live readout chip.
The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | A single win-probability series, clamped to 0–100. |
| `sides` | `[string, string]` | Names for the two sides — [>50, <50]. Default ["A", "B"]. |
| `label` | `"last" \| "none"` | Print the current leader's probability at the endpoint (default "last"). |
| `markSwing` | `boolean` | Mark the biggest momentum swing (default true; seat-gated). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
# WindBarb (/docs/charts/wind-barb)
WindBarb answers "which way is it flowing and roughly how hard — in one character?". The shaft points along the bearing
and the barbs count the magnitude: a half barb, a full barb, and a pennant are fixed quanta, so the glyph reads at a
glance without a scale. Quantization is the honesty here, not a limitation — the per-barb quantum is stated next to
every example.
```tsx
```
## Install [#install]
```tsx
import { WindBarb } from "@microcharts/react/wind-barb";
```
Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.
## Reading the barb [#reading-the-barb]
WindBarb has no interactive mode — a single glyph carries the whole reading, so its accessible name is the full
sentence. This gallery is the read-back key: each full barb is one `step`, a half barb is half a `step`, and a pennant
is five.
```tsx
`">
```
```tsx
`">
```
## When to use it [#when-to-use-it]
* **Good for** — wind or current direction + strength, traffic flow, net migration, request routing.
* **Avoid for** — exact magnitude (add `label`) or a time series (Sparkline).
## Variants [#variants]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
## Edge cases [#edge-cases]
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
```tsx
`">
```
`format`/`locale` reach the `label="value"` numeral and the accessible summary's magnitude together — the component
builds one formatter and reuses it for both.
## Why this default [#why-this-default]
Quantized barbs read faster and more honestly than a scaled arrow whose length you'd have to measure against a legend —
the glyph is calibrated to its `step`, so counting barbs gives the magnitude directly. A near-zero magnitude renders the
conventional open circle for calm rather than a zero-length shaft, and a negative magnitude flips the bearing 180° with
a dev warning.
## Accessibility [#accessibility]
The accessible name is the full reading — **"Southwest (225°), magnitude 32."** Calm renders as **"Calm."** Because the
glyph is self-describing, rows of barbs get their interaction from the host table or list, not from the mark.
## Props [#props]
| Prop | Type | Description |
| --- | --- | --- |
| `direction` (required) | `number` | Degrees; 0 = up/north, clockwise. |
| `magnitude` (required) | `number` | Any unit; quantized into barbs. |
| `step` | `number` | Full-barb quantum (each barb = step). |
| `label` | `"value" \| "none"` | Numeric magnitude beside the glyph. |
| `mode` | `"barb" \| "arrow"` | "arrow" swaps quantized barbs for a plain direction arrow + label. |
Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).