# Accessibility (/docs/accessibility) Every chart carries its accessibility with it. The accessible name is generated from your data, so there is no `alt` text to write and no caption to update when the numbers change. ## Generated summaries [#generated-summaries] By default a chart is an `img` whose accessible name is a sentence built from the series. A screen reader announces that sentence, a crawler indexes it, and a model can quote it. ```tsx // accessible name: // "Weekly revenue. Trending up 200%. Range 3 to 9. Last value 9."`" > ``` The name is `title` plus the generated summary, joined into one sentence. The summary is deterministic: the same data always produces the same words, so you can assert on it in a test. Every summary string in these docs is real generator output. ### Empty, single, and flat data [#empty-single-and-flat-data] Empty, single-point, and flat series are where a generated summary usually breaks. Each one gets its own short form. Every string below is the literal output of `describeSeries`: | Data | Accessible summary | | -------------------- | ------------------------------------------------ | | `[]` / all-null | `No data.` | | `[7]` | `Single value 7.` | | `[5, 5, 5, 5]` | `Flat at 5.` | | `[9, 7, 8, 4, 5, 2]` | `Trending down 78%. Range 2 to 9. Last value 2.` | ## Every chart is an `img` [#every-chart-is-an-img] The name is composed deterministically, with no module-level id counters that could desync between server and client and cause a hydration mismatch. Two levels: * **Default** — a chart renders `role="img"` with a computed `aria-label` (and an id-less ``). You wire nothing. * **Explicit `id`** — pass an `id` and the chart upgrades to a `<title>`/`<desc>` pair referenced by `aria-labelledby`, so both the title and the description are addressable. Override the whole sentence with a string when you have better words than the generator: ```tsx <Sparkline data={data} summary="Revenue per week, Q3 — up 40% and accelerating." /> ``` ## Decorative opt-out [#decorative-opt-out] When the surrounding text already describes the data, mark the chart decorative with `summary={false}` and screen readers skip it. That is the common case for a chart set inside a sentence. ```tsx <p> Revenue climbed steadily <Sparkline data={data} summary={false} /> through the quarter. </p> ``` A decorative chart renders `aria-hidden="true"` in place of `role="img"`. It leaves the accessibility tree entirely, and no `<title>` or `<desc>` is emitted. An `/interactive` entry resolves the name the same way: with nothing left to announce, the wrapper drops `role="img"` and `tabIndex={0}` and takes `aria-hidden="true"`, so it stops being a tab stop and can no longer be activated. Static and interactive entries match. `summary={false}` drops the generated *sentence*; the chart leaves the tree only when that leaves it with no name at all. With a `title` still set, the chart keeps `role="img"` and is announced as its title. Drop the `title` too when you want silence. ## One interaction contract [#one-interaction-contract] The `/interactive` entries share one contract across the catalog: learn it on a Sparkline and it holds on an ActivityGrid, a Dumbbell, or a Waterfall. Only the *unit* differs between charts — a point, a bar, a day cell, a segment, a pair — and each chart's page names its own. **Roving focus, one tab stop.** A chart is one focusable element (`role="img"`, `tabIndex={0}`), not a tab trap of one stop per data point. Arrow keys rove between units: **←/↑** move to the previous unit, **→/↓** to the next. Both axes work, so a grid navigates the same way a line does. **Home** and **End** jump to the first and last unit. **Active vs selected.** Hovering or roving makes a unit *active*: a transient read that clears when the pointer leaves or focus blurs. **Enter**, **Space**, a click, or a tap *selects* it and **pins** the readout, so it survives blur and pointer-leave. A keyboard user can move focus elsewhere and the pinned value stays on screen. Selecting the same unit again clears it, and **Escape** clears the selection from anywhere. The two states are drawn differently, so "where I am" and "what I kept" never look the same. **Touch.** A tap pins a unit the way a click does; dragging across the chart scrubs the active unit continuously. A drag always resolves as a scrub, so scrolling past a chart can't pin a value by accident. **Announcements.** Every change of active or selected unit is written into a polite live region, so a keyboard or screen-reader user reads the detail the hover chip shows. The picker fires when the *unit* changes, not on every pointer move, so sweeping across a chart announces once per unit crossed. The callbacks mirror the two states. `onActive` fires as the active unit changes; `onSelect` fires on selection. Both receive a `MicroDatum` (`{ index, value, label?, formatted? }`), or `null` when the chart clears. `index` identifies the unit, `value` is that unit's primary encoded number (`null` when the unit encodes nothing), and `formatted` is the exact string the chart's own readout chip would show. Control the selection with `selectedIndex`, or seed it once with `defaultSelectedIndex`. To render the value outside the chart — a KPI number, a sentence, a cell that updates as you hover — set `readout={false}` to keep the crosshair without the in-chart chip, and render `datum.formatted` from `onActive` yourself. Charts with a single unit (Delta, Progress, StatusDot, Bullet, and the rest of the scalar set) have no second unit to rove to, so they take `onSelect` alone (click, tap, Enter, or Space) and carry no selection state. Hover and focus still reveal the value: a scalar whose mark does not print its own number floats it in the same chip the pickers use, so a ProgressRing arc, an Hourglass, a BreathingDot, or a TapeGauge too small for its numeral still gives you a reading. Six glyphs skip the chip because they already are the number, or a count you read by counting: Delta, FatDigits, TrendArrow, StatusDot, DicePips, TallyMarks. **Values that change on their own.** Fifteen single-value charts built for updating KPI cards take `live` (default `true`): when the value changes, the new reading is re-announced through the polite region without anyone touching the chart. Set `live={false}` to silence it. A few throttle that announcement so a streaming value never floods the buffer: | Chart with `live` | Announcement throttle | | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | Progress | whole-percent steps only | | MoonPhase, FillWord | at most once per second | | Delta, ProgressRing, StatusDot, TrendArrow, Thermometer, Hourglass, FatDigits, TallyMarks, DicePips, Honeycomb, PictogramRow, BalanceBeam | none — every change announces | Two charts throttle without taking `live` at all. **EtaBar** and **TapeGauge** re-announce their reading as it changes, but never more often than `announceEvery` milliseconds: 10 s and 5 s by default, and tunable per instance. Three charts sit outside the contract on purpose. **MinimapStrip** is a viewport-window slider (`role="slider"`, with its own `onWindowChange([lo, hi])` range payload). **TokenConfidence** flows as inline HTML text and moves real focus onto each *flagged* token span; confident tokens carry no mark and are skipped. **WindBarb** ships static-only. Static entries carry no listeners, so all of this is opt-in. ## Color is never the only channel [#color-is-never-the-only-channel] Direction and state are always encoded twice, so a chart still reads with no color at all: * **Delta** pairs a ▲/▼ glyph with its color, and the glyph carries the sign. * **SparkBar** encodes sign by *position* above or below the baseline rather than by hue. * **ActivityGrid** always ships a numeric summary alongside its intensity ramp. The valence tokens are CVD-safe by construction: positive is a bluish-green and negative a vermillion, a split that stays legible to a red-green colorblind reader. The default stroke tokens clear a **4.5:1 contrast** target on the page backgrounds they are designed for, and the dark values are hand-tuned per token rather than inverted, so they hold up in dark mode. Contrast against *your* surface is still yours to check: charts paint no background, so a mark inherits whatever you place it on. ## Internationalization and RTL [#internationalization-and-rtl] Nothing about a chart is hard-coded to English or to left-to-right layout. * **Localized numbers.** Every rendered number and every number in the summary goes through `Intl.NumberFormat`. Pass `locale` and `format` and the digits, grouping, and units follow the reader's locale: ```tsx <Sparkline data={data} locale="de-DE" format={{ style: "currency", currency: "EUR" }} /> ``` See [Formatting & scale](/docs/formatting) for the full `format` / `locale` rules, including the exported `makeFormatter` helper for matching your prose numbers to a chart's. * **Localized words.** The summary templates are a swappable contract (`SummaryStrings`; the English set ships as the exported `EN` dictionary). Spread `EN`, override the keys you're translating, and pass the result as `strings` — static and `/interactive` entries both accept it, so a server-rendered chart localizes without shipping any client JavaScript. Or override the whole sentence with a `summary` string. Either way the accessible name, the live-region announcements, and the readout chip on an interactive entry all use your translation, because all three read from the same dictionary. Six charts don't take `strings` on their static entry: Sparkline, ActivityGrid, and MusicStaff accept it on the `/interactive` entry only, while SparkBar, Delta, and Bullet compose their sentence inline. `summary` is the translation seam there. `summary` and `strings` are the entire text surface, and a guard enforces that. It renders 22 interactive entries, each with a `strings` object of sentinel tokens, and fails on any English left in the output. It targets readouts because they only render in a browser, which is where the claim had stopped being true: readouts had accumulated words like "median", "vs" and "queued" in component code. The generator itself is exported too: `import { describeSeries } from "@microcharts/react"` returns the exact sentence for a series, handy for tests, tooltips, or feeding a model. * **RTL layouts.** A chart is direction-neutral SVG with a localized text name, so it drops into an RTL document as an `img` rather than a run of text. Time-ordered charts keep their conventional oldest-to-newest reading order. What a screen-reader user hears is the summary and the number formatting, and both localize. ## Reduced motion, forced colors, and contrast [#reduced-motion-forced-colors-and-contrast] * **`prefers-reduced-motion`** — entrance and update animations resolve instantly to their final state. Static charts have no `animate` prop and run no JavaScript animation; the one thing that moves inside a static host is `<Marker celebrate>`, whose particle burst is a pure-CSS one-shot that reduced motion also stills. * **`forced-colors`** — data ink maps to system `CanvasText`, accents to `Highlight`, muted marks to `GrayText`, so a Windows High Contrast user sees the chart rather than a blank box. * **`prefers-contrast`** — strokes thicken and bands deepen automatically. ## Verify it yourself [#verify-it-yourself] The summary is deterministic and the markup is plain SVG, so you can assert accessibility in CI instead of running a manual audit: ```tsx import { axe } from "vitest-axe"; const { container } = render(<Sparkline data={data} title="Revenue" />); expect(await axe(container)).toHaveNoViolations(); // and assert the exact announced sentence: expect(container.querySelector("[role=img]")).toHaveAccessibleName( "Revenue. Trending up 200%. Range 3 to 9. Last value 9.", ); ``` # AI-native (/docs/ai) A model can draw a chart mid-sentence. Two properties make that work: every microchart renders from plain `data`, and every chart carries a generated, human-readable summary — the same sentence a screen reader speaks, a crawler indexes, and a model can quote back. A few tokens of text become a real, accessible chart. > This is a live render, not a video: a scripted reply streams in, and the chart syntax it emits becomes shipped microcharts the instant each fence closes. Everything below is the contract behind it. The same components live in a product UI, a rendered report, and inline in docs like these. This page is the model-facing contract; [Where they live](/docs#where-they-live) shows the rest. ## The grammar [#the-grammar] A model emits a chart in one of two forms, both forgiving enough to produce reliably mid-stream. Pick a type and a form to see what it writes, what it renders, and the React it's equivalent to: The grammar has two forms — a fenced `microchart` block for a standalone chart, or an inline `microchart <type> <data>` span inside a sentence. Body is whitespace/comma numbers, or key=value for composites. **Sparkline** — trend over time ```microchart sparkline 132 148 141 165 159 182 176 203 ``` Equivalent React: ```tsx <Sparkline data={[132, 148, 141, 165, 159, 182, 176, 203]} title="Revenue" /> ``` **SparkBar** — magnitude per period (signed values = win/loss) ```microchart sparkbar 6 9 5 11 7 12 8 10 ``` Equivalent React: ```tsx <SparkBar data={[6, 9, 5, 11, 7, 12, 8, 10]} title="Deploys per day" /> ``` **Delta** — one signed change or ratio ```microchart delta +0.184 ``` Equivalent React: ```tsx <Delta value={0.184} title="Week over week" /> ``` **Bullet** — a value against a target (value= target= bands=) ```microchart bullet value=72 target=80 bands=50,90 ``` Equivalent React: ```tsx <Bullet value={72} target={80} bands={[50, 90]} title="Quota attainment" /> ``` **ActivityGrid** — cadence / intensity over a period ```microchart activity 0 2 1 3 4 2 1 3 2 4 3 2 ``` Equivalent React: ```tsx <ActivityGrid data={[0, 2, 1, 3, 4, 2, 1, 3, 2, 4, 3, 2]} layout="strip" title="Commit activity" /> ``` The body is whitespace- or comma-separated numbers for series charts, or `key=value` pairs for composites. These five cover the questions an assistant answers most — *trend, magnitude, change, attainment, cadence* — and the same pattern extends to [any chart in the catalog](/docs/charts): name a type, pick a body convention, map it to that chart's import. ## Wire it up [#wire-it-up] Turning the grammar into components is a parser and a `switch` — no framework, a few lines. ### Parse the stream [#parse-the-stream] Pull fenced blocks and inline spans out of partially-streamed Markdown. A fence that hasn't closed yet stays raw, then morphs the moment it does. ```ts const FENCE = /```microchart (\w+)\n([\s\S]*?)```/g; const INLINE = /`microchart (\w+) ([^`]+)`/g; const nums = (b: string) => b.split(/[\s,]+/).map(Number).filter(Number.isFinite); const kv = (b: string) => Object.fromEntries( b.split(/\s+/).flatMap((t) => { const i = t.indexOf("="); return i > 0 ? [[t.slice(0, i), t.slice(i + 1)]] : []; }), ); ``` ### Map type → component [#map-type--component] One `switch`. Because geometry is pure and charts render from `data` alone, the model's chart is identical to one you'd write by hand. ```tsx import { Sparkline } from "@microcharts/react/sparkline"; import { SparkBar } from "@microcharts/react/sparkbar"; import { Delta } from "@microcharts/react/delta"; import { Bullet } from "@microcharts/react/bullet"; import { ActivityGrid } from "@microcharts/react/activity-grid"; export function ChartBlock({ type, body }: { type: string; body: string }) { switch (type) { case "sparkline": return <Sparkline data={nums(body)} title="Series" />; case "sparkbar": return <SparkBar data={nums(body)} title="Per period" />; case "delta": return <Delta value={Number(body.trim())} title="Change" />; case "bullet": { const p = kv(body); return ( <Bullet value={+p.value} target={p.target ? +p.target : undefined} bands={p.bands?.split(",").map(Number)} title="Attainment" /> ); } case "activity": return <ActivityGrid data={nums(body)} layout="strip" title="Cadence" />; default: return null; // unknown type → leave the text as-is } } ``` ### Render inline vs block [#render-inline-vs-block] An inline chart is decorative — the sentence describes it, so pass `summary={false}`. A standalone chart keeps its summary; give it a `title`. Wrap every inline SVG mark in `className="mc-inline"` — the class ships in `@microcharts/react/styles.css` (imported once in the Quickstart). It seats the mark on the text baseline, so it rides the line like a word in any font. Leave `Delta` bare; it is a text metric and owns its own baseline. ```tsx NVDA closed +3.8%{" "} <span className="mc-inline"> <TrendArrow value={0.038} summary={false} /> </span>{" "} on the session — up <Delta value={0.038} summary={false} /> week over week. ``` ## Prompt the model [#prompt-the-model] A few lines teach the grammar. Start with the technique that matches how your model is driven — structured output and tool calls first, since that's how most agents run today: #### Structured JSON The default for anything production. Have the model return JSON and map it yourself — no parsing of prose. Constrain `type` to an enum and the output is always renderable. ```tsx type ChartSpec = | { type: "sparkline" | "sparkbar" | "activity"; data: number[]; title?: string } | { type: "delta"; value: number; title?: string } | { type: "bullet"; value: number; target?: number; bands?: number[]; title?: string }; // model returns ChartSpec (JSON mode / response_format) → render directly function Chart(spec: ChartSpec) { switch (spec.type) { case "sparkline": return <Sparkline data={spec.data} title={spec.title} />; case "bullet": return <Bullet {...spec} />; // …one arm per type } } ``` #### Tool use Expose a single tool the model calls when a chart helps. Its arguments *are* the chart spec, so there's nothing to parse and the schema enforces the shape. ```ts const renderChart = { name: "render_chart", description: "Render a microchart when a number series helps the reader.", input_schema: { type: "object", required: ["type"], properties: { type: { enum: ["sparkline", "sparkbar", "delta", "bullet", "activity"] }, data: { type: "array", items: { type: "number" } }, value: { type: "number" }, target: { type: "number" }, bands: { type: "array", items: { type: "number" } }, title: { type: "string" }, }, }, }; // on tool_use → <Chart {...toolCall.input} /> ``` #### System prompt For free-form chat, drop this into the system message — enough for a capable model to emit valid blocks unprompted. ```text When a number series would help, emit a chart block instead of prose. Fenced for a standalone chart, inline `microchart …` for a word-sized one: ```microchart sparkline 132 148 141 165 159 182 176 203 ``` Types: sparkline · sparkbar (signed = win/loss) · delta (one signed ratio) · bullet (value= target= bands=) · activity (intensities). Never invent a pie or gauge — they aren't supported. ``` #### Few-shot When you want a specific house style, show one exchange. Models mirror the shape of the example. ```text User: How did deploys trend this week? Assistant: Steady, with a strong Thursday: ```microchart sparkbar 6 9 5 11 7 12 8 10 ``` User: And are we on track for the quota? Assistant: Close — `microchart delta +0.184` week over week puts us here: ```microchart bullet value=72 target=80 bands=50,90 ``` ``` #### Guardrails Never trust generated data blindly. Validate the type, coerce the numbers, and fall back to text when a block is malformed. The generated summary makes text a graceful fallback. ```ts const KNOWN = new Set(["sparkline", "sparkbar", "delta", "bullet", "activity"]); function safeBlock(type: string, body: string) { if (!KNOWN.has(type)) return null; // unknown type → render raw text const data = nums(body); if (type !== "delta" && data.length === 0) return null; // no numbers → skip return { type, body }; // charts handle empty/NaN/±∞ already } ``` ## Where it runs [#where-it-runs] A chart is plain text and JSON, so anything that reads or writes text can emit one and render it back — no SDK, no bridge. That spans the **chat assistants** that answer in prose, the **coding agents and IDEs** that scaffold the components from the typed catalog, and the **frameworks and SDKs** that wire tool-call output straight to charts. None of these needed an integration; the output is text. - **Chat assistants & models** (emit a chart block mid-reply): Claude, ChatGPT, Gemini, Grok, DeepSeek, Kimi, Meta AI, Mistral, Perplexity, Qwen, Poe - **Coding agents & IDEs** (scaffold components from the typed catalog): Cursor, Claude Code, Codex, v0, opencode, Antigravity, Windsurf, Zed, Cline, Replit, Continue, Warp, Amp, Roo Code, StackBlitz, JetBrains AI, Pi, Copilot - **Frameworks & SDKs** (map tool-call output to charts): Vercel AI SDK, OpenAI Agents, LangChain, Anthropic SDK, Hugging Face, Ollama, Pydantic AI, CrewAI ## Integration scenarios [#integration-scenarios] Every path from model output to a rendered chart, with the one thing each needs. #### Streaming chat reply — morph blocks as they close Parse the partial Markdown on every token; render a fence as raw text until it closes, then swap in the chart. Word-sized charts land inside the sentence, standalone ones below it. The live demo at the top of this page is this shape. ```tsx // on each streamed chunk: const nodes = parse(accumulatedText); // text | { type, body, closed } return nodes.map((n) => n.type === "text" ? <Inline text={n.v} /> : n.closed ? <ChartBlock type={n.type} body={n.body} /> : <pre>{n.raw}</pre>, // still streaming → show raw ); ``` #### React Server Components — zero client JavaScript Static entries are hook-free, so an RSC assistant renders the model's charts to HTML on the server. Nothing hydrates; the chart ships as markup. Add `/interactive` only where the reader needs to interrogate a value — hover, keyboard, touch, or click-to-pin — and read it back with `onActive` / `onSelect`. Every chart but WindBarb has an interactive twin; `catalog.json` names it per entry, so an agent never has to guess. ```tsx import { Sparkline } from "@microcharts/react/sparkline"; // no "use client" export default async function Answer({ spec }: { spec: ChartSpec }) { return <ChartBlock type={spec.type} body={spec.body} />; // pure SVG, server-rendered } ``` #### Tool / function-call output — rows straight to charts When a tool returns rows, render them instead of asking the model to describe them in prose — a `Sparkline` per row, a `Bullet` for a KPI against target. ```tsx {rows.map((r) => ( <tr key={r.id}> <td>{r.name}</td> <td><Sparkline data={r.history} summary={false} /></td> <td><Bullet value={r.value} target={r.goal} summary={false} /></td> </tr> ))} ``` #### Programmatic & React-free — outside a component tree Need a chart in a README, an email, or a non-React runtime? Render the static component to an SVG string on the server. Static entries are hook-free and listener-free, so the markup they emit stands on its own. ```ts import { renderToStaticMarkup } from "react-dom/server"; import { Sparkline } from "@microcharts/react/sparkline"; const svg = renderToStaticMarkup(<Sparkline data={data} title="Revenue" />); // → a self-contained <svg> string for anywhere HTML goes ``` #### Validation & safety — malformed data never breaks a render Every chart already handles the degenerate cases — empty, single value, all-equal, nulls, NaN, ±Infinity — with an exact short summary instead of a broken chart. Layer the type/coercion guardrails above and a bad block downgrades to plain text instead of crashing. ## The MCP server [#the-mcp-server] Everything above is a model **emitting** a chart into a surface you control, where your code maps a block to a component. Sometimes there is no such surface — a desktop assistant's reply, a terminal, a draft email. There, the model needs the finished mark. [`@microcharts/mcp`](/docs/mcp) is a Model Context Protocol server, run over stdio on your own machine, that gives an assistant three tools: **find** the chart type that answers a question, **get** its exact props and a valid sample, **render** it to a self-contained SVG with the real generated alt text attached. This library does the rendering, so what comes back is the same mark the component would produce. Point any MCP client at it — Claude Desktop, Claude Code, Cursor, VS Code — with the config it already understands: ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` The same three capabilities also ship as Vercel AI SDK tools, for an assistant you're building yourself. See [MCP server](/docs/mcp) for the tool reference, the per-client setup, and what each call returns. ## Machine-readable surfaces [#machine-readable-surfaces] These routes are generated from the same source as the package, so an agent reads the current API instead of guessing: - [/llms.txt](/llms.txt): A hand-curated index of the docs for LLM tools, with explicit “does not support” notes to head off hallucinations. - [/llms-full.txt](/llms-full.txt): Every doc page concatenated into one text file — drop the whole API into a context window at once. - [/catalog.json](/catalog.json): Machine catalog: each chart’s name, import paths, data shape, and props (plus a shared-grammar block) — generated from the same registry that builds this site, and validated in CI against its JSON Schema. - [*.md mirrors](/docs/ai.md): Append .md to any docs page for a Markdown copy an agent reads without parsing HTML — every component expanded to text and code. The `.md` mirrors are static files — `/docs/ai` has a sibling `/docs/ai.md` — so they resolve on any host with no runtime, rewrite, or middleware. This site also deploys behind a small Cloudflare Worker that runs on `/docs/*` only: send `Accept: text/markdown` and the same doc URL returns the mirror inline, with `Vary: Accept` on both the Markdown and the HTML response. Appending `.md` always works; the negotiation is a convenience on top. `/llms.txt` points at one more surface the cards don't: [`/agent-setup.md`](/agent-setup.md), the paste-and-run setup prompt for a coding agent, extracted from the [Quickstart](/docs/quickstart) so the two can't drift. Or skip the URL entirely: <CopyAgentSetup /> ## The agent cheat sheet [#the-agent-cheat-sheet] Don't make a model memorize the catalog — [All charts](/docs/charts) files every type by the question it answers, and the [Charts index](/charts) shows them at a glance. Everything an agent needs to emit correct microcharts fits on one card. Copy it straight into a system prompt: **Agent cheat sheet.** Grammar: - `microchart sparkline` — 132 148 141 165 159 182 176 203 (trend over time) - `microchart sparkbar` — 6 9 5 11 7 12 8 10 (magnitude per period (signed values = win/loss)) - `microchart delta` — +0.184 (one signed change or ratio) - `microchart bullet` — value=72 target=80 bands=50,90 (a value against a target (value= target= bands=)) - `microchart activity` — 0 2 1 3 4 2 1 3 2 4 3 2 (cadence / intensity over a period) Rules: - Import each chart from its own subpath — `@microcharts/react/<name>`; add `/interactive` only for hover and keyboard. - Static charts are hook-free — valid inside a React Server Component, zero client JavaScript. - Pass `data` and a `title`; the generated summary handles accessibility. Use `summary={false}` only when a chart is decorative inside describing text. - Never add a runtime dependency, and never reach for a pie or gauge — they aren't shipped. Surfaces: /llms.txt · /llms-full.txt · /catalog.json · *.md mirrors For more, point an assistant at [`/llms.txt`](/llms.txt), the fuller [`/llms-full.txt`](/llms-full.txt), or the machine-readable [`/catalog.json`](/catalog.json) — or start from the [Quickstart](/docs/quickstart) and render your first chart. # Annotations (/docs/annotations) Annotations give the reader something to compare the data against: a limit, a normal range, the moment a deploy landed. They're plain children (`<Threshold>`, `<TargetZone>`, `<Marker>`, `<Callout>`) that the host chart draws in its own coordinate space, so the same child lands identically on any chart that accepts it. Reference ink stays quieter than data ink: hairlines stroke at 0.7 opacity, labels take the muted `--mc-neutral` label ink, and a target band uses the same faint `--mc-band` fill the charts do. ```tsx <TargetZone y={[40, 60]} /> <Threshold y={65} label="SLA" /> <Marker x={3} label="deploy" /> </Sparkline>`" > <Sparkline data="[48, 52, 45, 58, 51, 47, 55, 49, 71, 64, 57, 52]" title="p95 latency" width="200" height="48"> <TargetZone y="[40, 60]" /> <Threshold y="65" label="SLA" /> <Marker x="3" label="deploy" /> </Sparkline> ``` ## Install [#install] The annotation layer is its own \~1.5 kB entry. The mark renderers ship with this import, so a host that renders no annotations pays only a small children walker. ```tsx import { Threshold, TargetZone, Marker, Callout } from "@microcharts/react/annotations"; ``` ## The vocabulary [#the-vocabulary] * **`<Threshold y={65} label="SLA" />`** — a dashed reference hairline at a data-space `y`. * **`<TargetZone y={[40, 60]} />`** — a normal-range band, always drawn *beneath* the data ink. It takes a `label` too. * **`<Marker x={8} label="deploy" />`** — a vertical moment mark; `x` addresses the host's primary axis (the data **index** on index-based charts like Sparkline and SparkBar). * **`<Marker x={8} celebrate />`** — six deterministic particles burst once on entrance, for milestone crossings only; under reduced motion they render as a static starburst. * **`<Callout x={5} y={45} label="dip" />`** — a labeled point on an elbow hairline. `label` is required; `y` is optional and defaults to the frame's mid-height. `label` is optional on the other three, and all four take a `color` to override the reference ink on that one mark. A fragment works as a container (`<><Threshold … /><Marker … /></>`); the host's walker unwraps it. ```tsx <Marker x={9} celebrate label="10k!" /> </Sparkline>`" > <Sparkline data="[3, 4, 5, 5, 6, 8, 9, 11, 12, 14, 15, 17]" title="Signups" width="160" height="36"> <Marker x="9" label="10k!" /> </Sparkline> ``` ```tsx <Callout x={3} y={58} label="cache miss" /> </Sparkline>`" > <Sparkline data="[48, 52, 45, 58, 51, 47, 55, 49, 51, 46]" title="Latency" width="160" height="40"> <Callout x="3" y="58" label="cache miss" /> </Sparkline> ``` ## The same child, any host [#the-same-child-any-host] Annotation children carry no chart-specific geometry; they resolve against the host's scale. Move the exact `<TargetZone>` and `<Threshold>` from a line onto bars and they land in the same data-space, unchanged. ```tsx <TargetZone y={[3, 8]} /> <Threshold y={10} label="cap" /> </SparkBar>`" > <SparkBar data="[4, 7, 3, 9, 6, 8, 5, 11, 7, 9]" title="Deploys per day" width="160" height="44"> <TargetZone y="[3, 8]" /> <Threshold y="10" label="cap" /> </SparkBar> ``` ## Every value-series host [#every-value-series-host] Every **value-series** chart hosts annotations: the charts with a continuous value axis and an index / slot / time x-axis, where a data-space `x` and `y` map cleanly onto the frame. That's Sparkline, SparkBar, MiniBar, CyclePlot, CitySkyline, ChangePoint, DualSparkline, SpreadBand, ForecastCone, ControlStrip, QueueDepth, BurnChart, Waterfall, PercentileTrace, RetentionCurve, WinProbWorm, ErrorBudget, NetFlow, Horizon, EnsembleGhosts, PairedBars (vertical), and Slope. The same `<Threshold>` or `<Marker>` lands in each chart's own data-space, unchanged. (MiniBar and PairedBars host in their default **vertical** orientation, where value lives on the y-axis; a horizontal layout flips the axes, so annotation children pass through.) Charts without a single continuous value axis don't host them: there's no shared `y` for a `<Threshold>` to land on. | Why it can't host | Examples | | ----------------------------- | ---------------------------------------- | | Scalar readout — no axis | Delta, FatDigits, StatusDot, Thermometer | | Part-to-whole — y is a share | MicroDonut, SegmentedBar, Funnel | | Value encoded as color | HeatStrip, ActivityGrid, the calendars | | Value against value | QuadrantDot, MicroScatter, BiasStrip | | Pure timeline — no value axis | the event/phase timelines | Most still accept children as a plain pass-through, so an annotation placed inside one renders nothing and warns in dev. A few take no children at all, Delta among them, and passing one is a type error. <AnnotationHostShowcase /> ## Rendering rules [#rendering-rules] * A coordinate outside the host's domain **clamps to the edge at 0.4 opacity**, so it reads as off-scale instead of being dropped. * Annotations never change the auto summary, which describes the data alone. To describe an annotation on a static chart, pass an explicit `summary` string. * `celebrate` particles are seeded from the marker's `x` and the host's size, never `Math.random`, so server and client render the same burst. * A `label` you supply is the one text the containment rule can't pre-reserve a gutter for, so two mechanisms keep it inside the chart. `Threshold`, `TargetZone` and `Callout` **truncate**: a label longer than its available run is cut and given a trailing `…`. `Marker` **flips its anchor** instead, drawing a label near the right edge leftward from the mark, so a moment mark never loses characters. * Reference hairlines draw at a fixed 1-unit weight, exempt from `--mc-density`, so tightening the density never thickens the context ink along with the data. The `Callout` elbow is the exception, drawn a shade lighter at 0.75, because it's a leader line pointing at data rather than a reference the reader measures against. # Composition (/docs/composition) A microchart is built to sit inside an interface rather than in a chart pane of its own. This page has the placement recipe for each context the gallery demos use: the size, the props, and the surface each one wants, plus how to compare several charts at once. ## In a sentence [#in-a-sentence] A chart in running prose scales with the surrounding **font** and sits on the text baseline. Wrap it in `mc-inline` (shipped in `styles.css`) and pass `dots="none"` to drop its endpoint dots so it reads as one glyph. No chart in the catalog draws an axis, so there is nothing else to turn off. Pass `summary={false}` as well, so a screen reader hears the sentence and not a second description of it. ```tsx p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={latency} dots="none" summary={false} width={64} height={16} /> </span>{" "} — trending down. </p>`" > {/* Text as JSX expressions ({"…"}), never bare prose: MDX blockifies multiline bare text inside a JSX <p> into nested <p>s (hydration error). Expressions also survive prettier's md/mdx prose-wrap, which re-expands a single line. */} <p className="max-w-md text-[0.95rem] leading-relaxed"> {"p95 latency this week "} <span className="mc-inline"> <Sparkline data="[48, 45, 44, 40, 38, 36, 33, 31]" dots="none" summary="false" width="64" height="16" /> </span> {" — trending down."} </p> ``` See [Sizing](/docs/sizing#inline-in-a-sentence) for the `em`-based variant that scales with the surrounding type size. ## In a table cell [#in-a-table-cell] In a dense table the row already carries a name and a number, so the chart shows the *shape* between them. Keep it small, turn dots off, pass `summary={false}`, and leave the numeric column on `tabular-nums` so the figures line up. ```tsx <td>checkout-api</td> <td><Sparkline data={latency} dots="none" summary={false} width={64} height={18} /></td> <td className="tabular-nums">31 ms</td> </tr>`" > <table className="w-full max-w-sm text-sm tabular-nums"> <tbody className="[&>tr+tr]:border-t [&>tr+tr]:border-fd-border/60"> {[ { name: "checkout-api", data: [48, 45, 44, 40, 38, 36, 33, 31], now: "31 ms" }, { name: "auth-api", data: [12, 13, 12, 14, 13, 15, 14, 16], now: "16 ms" }, { name: "search-api", data: [80, 78, 82, 79, 81, 80, 79, 78], now: "78 ms" }, ].map((s) => ( <tr key={s.name}> <td className="py-1.5 pr-3 text-fd-muted-foreground">{s.name}</td> <td className="py-1.5"> <Sparkline data={s.data} dots="none" summary={false} width={64} height={18} /> </td> <td className="py-1.5 pl-3 text-right text-fd-muted-foreground">{s.now}</td> </tr> ))} </tbody> </table> ``` ## In a KPI card [#in-a-kpi-card] The number leads and the chart gives it context. Give the chart a little more height, turn `fill` on so it reads as an area, and pass `summary={false}`: the figure and its label already name the value. ```tsx <span className="figure">1,600</span> <span className="unit">concurrent, now</span> <Sparkline data={connections} fill summary={false} width={120} height={32} /> </div>`" > <div> <div className="text-fd-muted-foreground text-xs"> Active connections </div> <div className="mb-1 flex items-end gap-2"> <span className="text-3xl tabular-nums"> 1,600 </span> <span className="mb-1 text-fd-muted-foreground text-xs"> concurrent, now </span> </div> <Sparkline data="[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]" summary="false" width="120" height="32" /> </div> ``` ## In a tab or button [#in-a-tab-or-button] A spark beside a tab label previews what's behind it. The label is the primary read, so the chart goes *tiny*: a small box with no dots. ```tsx CPU <Sparkline data={cpu} dots="none" summary={false} width={40} height={14} /> </button>`" > <div className="flex flex-wrap gap-1.5"> {[ { 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) => ( <span key={m.name} className={`inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm ${ m.on ? "border-fd-primary/40 bg-fd-primary/5 text-fd-foreground" : "border-fd-border text-fd-muted-foreground" }`} > {m.name} <Sparkline data={m.data} dots="none" summary={false} width={40} height={14} /> </span> ))} </div> ``` ## Charts sit on your surface [#charts-sit-on-your-surface] Every chart is transparent and **never paints its own background**. It takes its ink color from `--mc-stroke`, which tracks light and dark for you, and shows through to whatever surface it lands on. The contrast of that surface is your decision. * Put the chart on a surface with enough contrast for a 1.5 px line. The data, valence, and accent inks all clear 4.5:1 on the built-in light and dark themes. (`--mc-neutral`, the muted context ink for baselines and ghost marks, sits at 3.5:1, past the 3:1 bar for non-text and quieter than the data.) * A low-contrast tint stays low-contrast: the chart does not compensate for it. * Need the ink to differ from the page (charts on a colored panel, frosted glass)? Set `--mc-stroke`, and the valence tokens if needed, on the container. See [Theming](/docs/theming#frosted-or-translucent-surfaces). Don't override `.mc-root` paint rules. > The gallery renders each demo inside a panel with a card background and a hairline border. That panel is not part of the library; it's a surface, like your app's cards, table rows, and headers. ## Small multiples: one shared scale [#small-multiples-one-shared-scale] When you place several sparklines in a row to *compare* them, they must share one vertical scale. Auto-scaling each chart to its own range makes a flat line and a spiking one look identical, which is a correctness bug and easy to miss. `SparkGroup` handles it for you. It reads every child's `data`, computes **one** domain across all of them, and applies **one** physical size, so the row is comparable. It's hook-free and RSC-safe, and a child's own explicit `domain`/`width` still wins. ```tsx <Sparkline data={us} summary={false} /> <Sparkline data={eu} summary={false} /> <Sparkline data={apac} summary={false} /> </SparkGroup>`" > <SparkGroup width="80" height="22"> <Sparkline data="[20, 22, 21, 24, 23, 25, 24, 26]" summary="false" /> <Sparkline data="[60, 58, 62, 59, 61, 60, 59, 58]" summary="false" /> <Sparkline data="[5, 8, 6, 12, 10, 15, 14, 18]" summary="false" /> </SparkGroup> ``` Pass `zero` when the group is bars or areas (so the shared domain anchors at zero), or an explicit `domain={[min, max]}` when you want a fixed frame instead of the data's own range (`domain` defaults to `"shared"`). Two more details. Only children that carry a `data` prop get the scale and size injected; plain wrappers, labels, and captions pass through untouched, so you can interleave them without them picking up stray DOM attributes. The group renders one `<div className="mc-group">`, an `inline-flex` row that wraps: its gap is `--mc-gap` scaled by `--mc-density`, and `className`/`style` on `SparkGroup` land on that div if you want a different layout. ## The `summary` decision [#the-summary-decision] Nearly every demo above passes `summary={false}`. Here is when that's the right call: * **Standalone chart** (the one thing in its region): keep the auto-summary on. It's the accessible name a screen reader announces, generated from the data. * **Repeated or captioned chart** (table rows, tabs, a chart next to a number that already states it): pass `summary={false}` to mark it decorative. The surrounding text is the accessible name, and a per-chart summary would make a screen reader read the same thing on every row. `summary` is the only accessibility lever you set by hand. The `role="img"`, the `<title>`, and the interaction contract on interactive entries (roving arrow keys, select-to-pin with **Enter** or **Space**, touch scrub, live announcements) are automatic. See [Accessibility](/docs/accessibility). # Design notes (/docs/design-notes) These are the decisions baked into the defaults, so you rarely have to make them yourself. Each one below explains what the library does and why it does it that way. ## Direct labels beat chrome [#direct-labels-beat-chrome] At word size, an axis, a legend, and a gridline each cost more ink than they return, so there are none. A value sits at the end of the line. A min and a max mark themselves. What's left is almost all data. ## Areas anchor at zero [#areas-anchor-at-zero] A filled Sparkline always measures from zero. An area floating on an arbitrary baseline exaggerates change: the fill grows faster than the numbers do. Anchoring at zero keeps length proportional to magnitude. ## Color encodes, never decorates [#color-encodes-never-decorates] There's one accent per context, and a color change always means a change in meaning. Positive and negative come from a color-blind-safe pair, doubled by a glyph or a position, so a chart still reads with no color at all. A preset restyles a chart without changing what it says. ## Static first, interactivity opt-in [#static-first-interactivity-opt-in] Nearly every chart ships as two entries (WindBarb is static-only). The default is pure SVG: hook-free, listener-free, safe inside a React Server Component, and it renders to HTML with zero client JavaScript. Hover, roving keyboard focus, touch scrub, select-to-pin, and live announcements live in a separate `/interactive` subpath you import only where you need them. The two entries never mix, so you pay for interactivity only where you ask for it, and a static chart can't drag a listener into your server bundle. The interactive entry composes the static one rather than re-implementing it, so the two renderings can't drift. Every multi-unit chart resolves a gesture to a navigable *unit* through the same kernel, so interaction is one thing to learn across the catalog. Single-unit charts have nothing to rove between and take the hover/select half of that contract without the index half. ## One grammar, not an options bag [#one-grammar-not-an-options-bag] `data` alone always renders something correct. A prop name means the same thing on every chart — `title`, `color`, `domain`, `format`, `summary` — so what you learn on one chart transfers to all of them. Variants are props; a genuinely different data shape is a different component rather than a deeper options object. Annotations are children you nest: a threshold, a target zone, a marker. The result is an API you can mostly guess. ## Generated summaries [#generated-summaries] Each chart generates a natural-language sentence from its data, and that one deterministic sentence does three jobs: it is the accessible name a screen reader speaks, the text a crawler indexes, and the string a model can quote back. It degrades on empty, single, and flat data instead of describing a chart that isn't there. See [Accessibility](/docs/accessibility) for the details. ## Small is a design material [#small-is-a-design-material] Composition, contrast, and the auto-summary all have to survive the smallest supported size. At that size there's no room for a mark that isn't carrying information, so decoration never gets in. ## Motion lives in the interactive build [#motion-lives-in-the-interactive-build] Static charts have no `animate` prop and run no animation JavaScript. Two things move inside a static host, both pure CSS and both stilled by reduced motion: `<Marker celebrate>`, a one-shot burst on a milestone crossing, and `StatusDot pulse`, the catalog's one looping animation and its one documented exemption from the rule below. A monitoring dot that pulses reads as a live feed, and the same dot holding still reads as a stopped one, so the loop carries the reading. It's opt-in per instance and off by default. Only the `/interactive` entries play an entrance, and only when you opt in with `animate` (off by default). It runs through the Web Animations API, gated on `prefers-reduced-motion`. Every entrance follows the logic of the data it draws: a line draws left-to-right along its time axis, bars grow from the baseline, a donut fills clockwise from twelve o'clock, and labels arrive last, because they annotate marks that already exist. Marks decelerate into place. They never bounce, loop, or pull the eye away from the data. ## Budgets are build gates [#budgets-are-build-gates] Size is a CI gate. A chart that grows past its gzip budget fails the build, the same way a failing test does, and the SSR benchmark exits non-zero when a chart drops below its render floor. The numbers on the [Performance](/docs/performance) page are never hand-typed: the sizes are re-measured from the built `dist/` on every CI run, and the throughput figures are derived from the checked-in benchmark result, so a claim can't drift from what was measured. ## One package [#one-package] All the chart types ship from a single package, `@microcharts/react`, each behind its own subpath so you only bundle what you import. The catalog's collections — core, decision, expressive, frontier — are metadata for browsing, never package or import boundaries. There's no plugin to install and no engine to configure. The root barrel (`@microcharts/react`, no subpath) carries only the shared pieces, so importing it pulls in no chart code: the `Chart` root wrapper every chart composes — a `role="img"` `<svg>` with the viewBox, `title`, and generated `summary` already wired, there if you ever need to build a custom mark — plus [`SparkGroup`](/docs/composition#small-multiples-one-shared-scale) for shared-scale small multiples, [`MicroProvider`](/docs/theming#a-component-api-if-you-prefer) for theming, and the `describeSeries` / `makeFormatter` / `EN` helpers documented under [Formatting](/docs/formatting) and [Accessibility](/docs/accessibility). ## The bar for a new chart type [#the-bar-for-a-new-chart-type] A new chart earns its place only if it clears a real bar: it reads at word size (roughly 200×60 px), it tells a data story the catalog can't already tell well, it has one honest primary encoding channel, and a first-time reader can read it back without training. This is why the catalog prefers uncommon *questions* over uncommon *shapes*. ## What we don't ship [#what-we-dont-ship] No pie, no needle gauge, no battery, no waffle, no violin, no 3-D, no gradients-as-decoration, no drop shadows on data, no looping animation (with the one exemption noted above, where the loop carries the reading). These aren't theme options you can switch on. They're designed out, because at word size they mislead more than they inform. Where a rejected chart answered a real question, the catalog ships a replacement: [Bullet](/docs/charts/bullet) or [TapeGauge](/docs/charts/tape-gauge) instead of a speedometer, [SegmentedBar](/docs/charts/segmented-bar) for comparative part-to-whole, [MicroDonut](/docs/charts/micro-donut) as a capped icon-size mix when a glyph is specifically wanted, and [MicroBox](/docs/charts/micro-box) instead of a violin. # Formatting & scale (/docs/formatting) Two props decide how a chart writes its numbers: `format` and `locale`. A third, `domain`, decides what value range the geometry is drawn against. All three mean the same thing on every chart, and every chart's page lists them; this page is where their defaults are written down. ## One formatter, three places [#one-formatter-three-places] A chart writes numbers in three places: the **direct labels** it draws, the **accessible summary** a screen reader announces, and, on an interactive entry, the **readout** that follows the pointer. `format` feeds all three from one definition, so a currency chart's spoken summary says "$1,240", not "1240". ```tsx `"> <Sparkline data="[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]" width="170" height="44" label="last" format="{ style: "currency", currency: "USD", maximumFractionDigits: 0 }" title="Revenue" /> ``` `format` takes either **`Intl.NumberFormatOptions`** or a **plain function** `(n) => string`. Formatters are cached, keyed by locale plus options, so hundreds of instances sharing one format shape build one formatter between them. `Intl.NumberFormat` construction is expensive, so never build your own inside a component. The cache holds 64 formatters and empties completely when it fills. One app uses a handful of locale × option shapes, so a bounded cache that occasionally rebuilds costs less than one that grows forever. You can reuse the same formatting outside a chart: a matching KPI label, a table cell. Import the exported `makeFormatter` helper and your prose numbers come out of the same cache as the chart's: ```tsx import { makeFormatter } from "@microcharts/react"; const fmt = makeFormatter({ style: "currency", currency: "USD" }, "en-US"); fmt(42); // "$42.00" ``` The arguments are positional: `makeFormatter(format, locale, defaults?)`. The third is the fallback used when `format` is omitted, which is how a chart supplies its own default (see `Delta` below). ## Percent, compact, decimals [#percent-compact-decimals] Anything `Intl.NumberFormat` supports works: percent, compact notation, fixed decimals, sign display. `Delta` defaults to a locale-aware percent, so a raw ratio reads as a change with no config: ```tsx // → +12.8% <Delta value={4200} format={{ notation: "compact" }} /> // → +4.2K <Delta value={4200} format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }} />`" > <div className="flex flex-wrap items-center gap-6 text-lg"> <Delta value="0.128" summary="false" /> <Delta value="4200" format="{ notation: "compact" }" summary="false" /> <Delta value="4200" format="{ style: "currency", currency: "USD", maximumFractionDigits: 0 }" summary="false" /> </div> ``` ## A custom function [#a-custom-function] When `Intl` can't express the unit (milliseconds, "×" multipliers, a domain-specific symbol), pass a function. It receives the number and returns the string to render. The value is snapped to 12 significant digits first, so a number the chart derived internally (`value - target`) arrives as `-3.6` rather than `-3.5999999999999943`. ```tsx \`\${Math.round(n)} ms\`} />`"> <Sparkline data="[48, 45, 44, 40, 38, 36, 33, 31]" width="170" height="44" label="last" format="(n) => `${Math.round(n)} ms`" title="p95 latency" /> ``` ## The interactive readout [#the-interactive-readout] Import a chart from its `/interactive` subpath and it gains a readout: a small chip that follows the pointer, or the focused datum when you rove with the keyboard. It uses the same `format` and `locale` as everything else, so a currency chart's hover value matches its label and its summary. **The chip is terse.** It names the datum and its value: a Funnel stage reads `Checkout 62% (1,240)`, a Slope line reads `Brazil: 44 → 28`. It doesn't repeat the axis names, the row label already printed beside it, or the grid position the cell already occupies. At these sizes a chip that carried all that would be wider than the chart it annotates. The fuller sentence still exists. Everything the chip leaves out (the comparison, the quadrant name, the running total, the per-cycle breakdown) is announced in the chart's live region, which is what a screen reader reads. ```tsx import { Sparkline } from "@microcharts/react/sparkline/interactive"; // the readout, the endpoint label, and the spoken summary all read "$1,600" <Sparkline data={revenue} label="last" format={{ style: "currency", currency: "USD" }} />; ``` The chip is capped at `max(100%, 14em)` wide and ellipsizes past that, so a long formatted value can't paint across the page. If you see an ellipsis, shorten the value with `format` (`notation: "compact"` usually does it) rather than widening the chart. Its prose comes from `strings`, like every other word the library renders, so a localized bundle localizes the readout too. Its colors come from `--mc-surface`, `--mc-surface-ink` and `--mc-surface-edge`; see [Theming](/docs/theming#the-readout-surface). **Put the value somewhere else.** Every `onActive` / `onSelect` callback receives that same string as `datum.formatted`, built with the chart's own `format` and `locale`. Suppress the chip and render it in your own layout: this is the pattern behind a KPI card whose big number tracks the sparkline you hover. ```tsx const [reading, setReading] = useState("$1,600"); // readout={false} keeps the crosshair, drops the chip; the value lands in your <output> <output>{reading}</output>; <Sparkline data={revenue} format={{ style: "currency", currency: "USD" }} readout={false} onActive={(d) => d && setReading(d.formatted!)} />; ``` `readout={false}` hides only the chip. Hover, keyboard roving, touch scrub, the live-region announcements, and both callbacks keep working. Leave `readout` at its default (`true`) and you get the chip and the callbacks. ## Locale: separators, not language [#locale-separators-not-language] `locale` is a BCP-47 tag (`"de-DE"`, `"ja-JP"`, or an array of fallbacks). It controls how numbers and dates are *written*: the thousands separator, the decimal mark, currency placement. ```tsx // 1,600 <Sparkline data={revenue} label="last" locale="de-DE" /> // 1.600`" > <div className="flex flex-wrap items-center gap-8"> <Sparkline data="[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]" width="150" height="40" label="last" locale="en-US" title="US" /> <Sparkline data="[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]" width="150" height="40" label="last" locale="de-DE" title="DE" /> </div> ``` > `locale` sets number and date **formatting**. It does not translate the summary *sentence* ("Trending up 200%…"); that's the `strings` contract on [Accessibility](/docs/accessibility#internationalization-and-rtl). A fully localized chart needs both: `locale` for the figures, translated `strings` for the prose. ### Server rendering: pass a `locale` explicitly [#server-rendering-pass-a-locale-explicitly] Omit `locale` and `Intl` resolves the *runtime's* default. On a server-rendered page that runtime is the server: Node typically resolves to the host's locale while the visitor's browser resolves to theirs, so the same chart can render `1,600` into the HTML and `1.600` after hydration. React reports that as a hydration mismatch, and does not patch up the mismatched text. This isn't specific to microcharts; it's how `Intl` defaults work. Charts hit it more often than most UI because the number reaches three places at once: the label, the accessible summary, and the readout. The fix is to make the choice explicit: ```tsx // Deterministic: the server and the browser format identically. <Sparkline data={revenue} label="last" locale="en-US" /> ``` Pass the locale your page has already resolved (from the request, a cookie, or your i18n provider) and use the same one on both sides. If a chart is client-only, the ambient default is fine. ## Scale: the `domain` decision [#scale-the-domain-decision] `domain` sets the value range the geometry is drawn against: the vertical frame. Omit it and the chart **auto-fits to its own data**. Pass `[min, max]` to pin a fixed frame, so a 40–60% reading looks like 40–60% of a 0–100 scale rather than a full-height line. ```tsx // auto-fit — fills the height <Sparkline data={cpu} domain={[0, 100]} /> // fixed 0–100% frame`" > <div className="flex flex-wrap items-center gap-8"> <Sparkline data="[48, 52, 49, 55, 53, 58, 54, 60]" width="150" height="44" dots="minmax" title="Auto-fit" /> <Sparkline data="[48, 52, 49, 55, 53, 58, 54, 60]" width="150" height="44" domain="[0, 100]" dots="minmax" title="Fixed 0–100" summary="false" /> </div> ``` Auto-fit works differently per chart. Charts whose marks read as magnitude from a baseline (`Sparkbar`, `MiniBar`, `PairedBars`, and `Sparkline` once you turn on `fill`) anchor the auto-fitted domain at **zero**, because a bar cut off above zero misstates its own length. Line charts without a fill auto-fit to the data alone. Either way, a completely flat series is padded so it still has a band to sit in instead of collapsing to a degenerate scale. The choice changes what a reader can conclude: auto-fit maximizes detail for a single series, and a fixed domain is what makes two charts comparable. To share one scale across several charts at once, use `SparkGroup`: it reads every child's `data`, computes one union domain, and injects it into any child that didn't set its own (`zero` anchors that shared domain at zero). See [Composition](/docs/composition#small-multiples-one-shared-scale). ## Dates and times [#dates-and-times] On calendar and time charts, `format` means what it means everywhere else: it formats the **numbers**. On `EventTimeline` that's a share, defaulting to a locale-aware percent. On `CalendarStrip` it's the day's value in the hover/focus readout, so it lives on the interactive entry only. The static `CalendarStrip` summary counts days instead: `Active 3 of 28 days over 4 weeks.` A count of days is not a number `format` restyles. Date and time *strings* are a separate prop, **`dateFormat`**, taking `Intl.DateTimeFormatOptions` or a `(date) => string` function. It lives on the interactive entries (`@microcharts/react/calendar-strip/interactive`, `@microcharts/react/event-timeline/interactive`), the ones that render a spoken or hovered date. `locale` applies to it the same way. ```tsx import { CalendarStrip } from "@microcharts/react/calendar-strip/interactive"; <CalendarStrip data={days} dateFormat={{ month: "short", day: "numeric" }} locale="de-DE" />; ``` When you pass options rather than a function, the formatter defaults to `timeZone: "UTC"`, so the same input renders identically in any host timezone and a chart never shifts a day because the viewer is in a different region. Your options spread after that default, so `{ timeZone: "America/New_York" }` overrides it. A `(date) => string` function bypasses the default entirely and handles its own timezone. # Full chart libraries (/docs/full-chart-libraries) [Recharts](https://recharts.org) and [Chart.js](https://www.chartjs.org) are full chart libraries. They draw surfaces that are mostly chart: scales, legends, tooltips, plugins, dashboard layouts. microcharts draws a word-sized mark *inside* a surface you already have — a sentence, a table cell, a KPI, a streamed reply. Here is each job in code: ```tsx // Recharts — full surface import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"; <ResponsiveContainer width="100%" height={240}> <LineChart data={rows}> <XAxis dataKey="t" /> <YAxis /> <Tooltip /> <Line type="monotone" dataKey="v" /> </LineChart> </ResponsiveContainer>; // microcharts — word-sized mark import { Sparkline } from "@microcharts/react/sparkline"; <span className="mc-inline"> <Sparkline data={[3, 5, 4, 8, 6, 9]} dots="none" summary={false} width={64} height={16} /> </span>; ``` Neither replaces the other. Read [When to use microcharts](/docs/when-to-use) for the product decision; this page pins the size numbers so they stay accurate. ## Side by side [#side-by-side] | | Full library (Recharts, Chart.js, …) | microcharts | | -------------------- | ------------------------------------ | ----------------------------------------------- | | Surface | The chart *is* the panel | The chart sits in prose / cell / KPI | | Chrome | Axes, legends, tooltips, brush | None by design | | Typical budget | Tens to hundreds of kB | <SizeMarketing /> | | Runtime dependencies | Real dependency trees | Zero (`dependencies: {}`; React is a peer) | | Server Components | Usually a client chart runtime | Default export is hook-free SVG, zero client JS | | Accessible name | You wire it (plugins, tables, aria) | `role="img"` + summary from the data by default | ## Recharts [#recharts] Recharts is a strong default for React dashboards: composable `<LineChart>`, axes, tooltip, responsive container. The gzip figures below are orientation, not a scoreboard — a smaller number doesn't win an app that needs ticks and a brush: <VsRechartsTable /> **Reach for Recharts** when the page is mostly chart — ticks, legends, brush, or a familiar Recharts tree. **Reach for microcharts** when the mark must live in a sentence, a cell, or an RSC with nothing to hydrate. ## Chart.js [#chartjs] Chart.js is Canvas-first, with a large plugin and config ecosystem. In React it is usually wrapped by `react-chartjs-2`. That stack draws dashboard and report canvases, at a size that doesn't fit a table cell. <VsChartJsTable /> **Reach for Chart.js** when you need Canvas, existing Chart.js configs or plugins, or dense series on a chart panel. **Reach for microcharts** when you need inline SVG, accessible-by-default summaries, or static RSC output. Canvas charts need an explicit accessibility path: a hidden table, aria wiring, or a plugin. microcharts makes the accessible name the default instead — see [Accessibility](/docs/accessibility). Chart.js can be made accessible; it just isn't accessible until you do that work. ## Picking one [#picking-one] If the job is a full chart, pick a full library. If the job is a word-sized mark, pick microcharts, or add it alongside the library you already run. Nothing on this page claims microcharts is faster than Recharts for a dashboard, or that it's a lighter Chart.js for the same job. Those are different jobs, and comparing across them wouldn't tell you anything. ## Next [#next] * [When to use microcharts](/docs/when-to-use) * [microcharts vs Recharts](/docs/vs-recharts) · [microcharts vs Chart.js](/docs/vs-chartjs) — the per-library detail * [React sparklines](/docs/react-sparklines) * [Inline charts](/docs/inline-charts) * [Performance](/docs/performance) * [Introduction](/docs) # Introduction (/docs) microcharts renders charts small enough to sit inside a sentence: a trend in a table cell, a bullet in a KPI card, a sparkline in a streamed AI reply. The grammar is small and predictable enough for a model to emit correctly mid-stream, and every chart generates a plain-language description of its own data, so a chart an LLM writes into a chat reply, a document, or a slide is one a person can read and check. The fastest setup is [Set up with AI](/docs/quickstart#set-up-with-an-ai-agent): paste one prompt and an agent installs the package, imports the stylesheet, and verifies with a first chart. Or install manually (both steps are required): The other half of manual setup is one stylesheet import at your app root — see the [Quickstart](/docs/quickstart#set-up-manually). Every chart renders from `data` alone, ships as pure SVG with **zero runtime dependencies**, and carries an accessible description generated from the data itself. ```tsx `" > <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" title="Weekly revenue" /> ``` ## Why microcharts [#why-microcharts] * **AI-native.** A chart is plain `data` plus generated words, so a model can emit one mid-sentence and read one back. [Chat streams, agents, and tool output](/docs/ai) are primary surfaces. One grammar across 106 types means an LLM that has seen one chart can write them all, and an assistant that speaks [MCP](/docs/mcp) can call them directly. * **Zero dependencies.** No chart engine, no D3 — just SVG. CI-enforced, forever. * **Server-component safe.** Static charts are hook-free and listener-free; they render to HTML with zero client JavaScript. Interactivity comes from a separate `/interactive` subpath, where one contract covers the catalog: hover, roving arrow keys, touch scrub, and click-to-pin, read back through `onActive` / `onSelect`. Single-unit charts have nothing to rove between and take `onSelect` alone. * **Accessible by default.** Each chart is an `img` with a natural-language summary built from your data. The summary is generated, so it updates when the numbers do. * **Tiny.** **\~2–7 kB interactive · \~1–4 kB static** gzip each, imported from their own subpath and budget-gated in CI. ## One catalog, one grammar [#one-catalog-one-grammar] The library ships a full catalog of chart types, grouped by the question each one answers. `data` alone always renders something correct, and a prop name means the same thing on every chart, so picking a chart is picking the question you need answered. From sparklines to bump charts, funnels, honeycombs, and calendar strips — a small sample of what ships: A representative slice of the catalog spans trends, comparisons, distributions, and status — see every chart type at [/docs/charts](/docs/charts). Browse them all on the [All charts](/docs/charts) page, which files every type under the question it answers. ## Where they live [#where-they-live] A word-sized chart belongs inside the interface it describes. And because it's plain SVG rendered from `data` alone, "interface" reaches past the browser tab: * **In your product** — a trend in a table cell, a KPI card, a tab header, a dense dashboard. * **In a report or slide** — a figure in a rendered PDF or deck, the same markup that drops into a table cell, with nothing to export. * **In your docs** — inline in Markdown, next to the sentence that explains it. The charts on this page are rendered that way: MDX in, live SVG out. * **In an AI reply** — streamed mid-sentence by a model. [AI-native](/docs/ai) shows a live stream doing it. Here's the same chart doing its job in four of those homes: **In a sentence** ```tsx <p> p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={16} dots="none" summary={false} /> </span>{" "} — trending down. </p> ``` **In a table cell** ```tsx <tr> <td>checkout-api</td> <td> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={18} dots="none" summary={false} /> </td> <td>31 ms</td> </tr> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1,600</span> <span className="unit">concurrent, now</span> <Sparkline data={[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]} width={90} height={28} fill summary={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> CPU <Sparkline data={[62, 65, 61, 68, 70, 66, 72, 75]} width={40} height={14} dots="none" summary={false} /> </button> ``` [Composition](/docs/composition) has the placement recipe for each surface — a table cell, a KPI card, a tab, or inline in a sentence. Unsure whether you need this library or a full chart library? [When to use microcharts](/docs/when-to-use). Ready to render one? The [Quickstart](/docs/quickstart) gets you from install to a live chart in minutes. ## FAQ [#faq] ### What are micro charts? [#what-are-micro-charts] Micro charts — also called sparklines, tiny charts, or inline charts — are small, word-sized data visualizations: a trend line, bar, or dot that fits inside a sentence, table cell, or KPI card without axes or legends. They answer one question in the space of a word. ### What is microcharts? [#what-is-microcharts] microcharts is a React chart library of word-sized SVG charts — sparklines, bars, bullets, and the rest of the catalog — built to sit inside sentences, table cells, KPI cards, and AI replies. Zero runtime dependencies, accessible by default, and server-component safe. ### How is microcharts different from Recharts or Chart.js? [#how-is-microcharts-different-from-recharts-or-chartjs] Recharts and Chart.js are built for surfaces that are mostly chart; in a table cell, their smallest chart is larger than the cell. microcharts is built for the cell itself: word-sized SVG marks, one chart per subpath at **\~2–7 kB interactive · \~1–4 kB static** gzip, no axes or legends, and static entries that render from an RSC with **zero client JavaScript**. The two solve different problems — see [When to use](/docs/when-to-use) and [Full chart libraries](/docs/full-chart-libraries), or the measured per-library pages: [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), and [vs visx](/docs/vs-visx). ### Are microcharts accessible? [#are-microcharts-accessible] Yes. Every chart is `role="img"` with a natural-language summary generated from the data. Interactive entries add keyboard navigation and a polite live region. Direction and state are never color-alone — see [Accessibility](/docs/accessibility). ### Do static microcharts need client JavaScript? [#do-static-microcharts-need-client-javascript] No. Default exports are hook-free pure SVG — RSC-safe with zero client JS. Import the matching `/interactive` subpath only when you need hover, keyboard, touch, or selection. ### Why is there no pie chart, gauge, or waffle? [#why-is-there-no-pie-chart-gauge-or-waffle] They fail at word size: pie angles are unreadable in a table cell, a needle gauge spends its pixels on chrome, a waffle doesn't fit in a word. Each has an in-catalog replacement — [SegmentedBar](/docs/charts/segmented-bar) for pie, [Bullet](/docs/charts/bullet) for gauges, [IconArray](/docs/charts/icon-array) for waffle. The reasoning lives in [Design notes](/docs/design-notes) and [When to use](/docs/when-to-use). ### Why does my first chart render unstyled, or with serif labels? [#why-does-my-first-chart-render-unstyled-or-with-serif-labels] Almost always one of two setup steps: the stylesheet isn't imported at your app root (`import "@microcharts/react/styles.css"`, once), or the surrounding page never sets a `font-family`, so SVG text falls back to a serif — set `--mc-font` or give the page a font. Both are covered in the [Quickstart](/docs/quickstart#set-up-manually). ### Can I match the charts to my brand color? [#can-i-match-the-charts-to-my-brand-color] Yes. Pass one accent to `defineTheme` and it derives a color-blind-safe categorical palette plus matched dark twins, in OKLCH, with zero dependencies; or set the `--mc-*` tokens directly. A theme never changes what the data means: positive and negative keep their hues on every accent and preset. See [Theming](/docs/theming). ### How do I propose a new chart type? [#how-do-i-propose-a-new-chart-type] Open an issue first. A new type must clear the admission bar: a unique data story the catalog can't already answer, one honest encoding channel, and readable at ≤ 200×60 px without training. [CONTRIBUTING.md](https://github.com/ganapativs/microcharts/blob/main/CONTRIBUTING.md) walks through the rest. > Tested and in production use, but not across every stack and edge yet. If you hit something, open an issue on [GitHub](https://github.com/ganapativs/microcharts/issues). ## Made by [#made-by] microcharts is designed and built by [Ganapati V S](https://meetguns.com) — [@ganapativs](https://x.com/ganapativs) on X, [ganapativs](https://github.com/ganapativs) on GitHub. It's free and open source under the MIT license; the code lives on [GitHub](https://github.com/ganapativs/microcharts), where issues, ideas, and stars are all welcome. Want to contribute? [CONTRIBUTING.md](https://github.com/ganapativs/microcharts/blob/main/CONTRIBUTING.md) covers the dev setup, the non-negotiables, and what a PR needs to ship. The screenshot review that keeps all 106 charts looking right in light, dark, and every preset runs on [Argos](https://argos-ci.com?utm_source=ganapativs/microcharts\&utm_campaign=oss), which is free for open source. # Inline charts (/docs/inline-charts) `@microcharts/react` renders charts small enough to sit *inside* the interface you already ship: a trend in a table cell, a sparkline after a number, a bullet in a KPI, a mark in a streamed reply. For a full dashboard canvas, reach for a [full chart library](/docs/full-chart-libraries) instead. <CatalogTotal /> types, one grammar, zero runtime dependencies. Size band: <SizeMarketing />. Static entries ship **zero client JavaScript** from a React Server Component. Every chart is an `img` with a summary generated from the data. ## One mark, four homes [#one-mark-four-homes] **In a sentence** ```tsx <p> p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={16} dots="none" summary={false} /> </span>{" "} — trending down. </p> ``` **In a table cell** ```tsx <tr> <td>checkout-api</td> <td> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={18} dots="none" summary={false} /> </td> <td>31 ms</td> </tr> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1,600</span> <span className="unit">concurrent, now</span> <Sparkline data={[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]} width={90} height={28} fill summary={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> CPU <Sparkline data={[62, 65, 61, 68, 70, 66, 72, 75]} width={40} height={14} dots="none" summary={false} /> </button> ``` ## The surfaces [#the-surfaces] ### In a sentence [#in-a-sentence] Wrap with `mc-inline`, drop endpoint dots, mark decorative when the sentence already carries the meaning: ```tsx p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={latency} dots="none" summary={false} width={64} height={16} /> </span>{" "} — trending down. </p>`" > <p className="max-w-md text-[0.95rem] leading-relaxed"> {"p95 latency this week "} <span className="mc-inline"> <Sparkline data="[48, 45, 44, 40, 38, 36, 33, 31]" dots="none" summary="false" width="64" height="16" /> </span> {" — trending down."} </p> ``` ### In a table cell [#in-a-table-cell] ```tsx <table className="w-full max-w-sm text-sm tabular-nums"> <thead className="sr-only"> <tr> <th scope="col"> Service </th> <th scope="col"> Trend </th> <th scope="col"> Now </th> </tr> </thead> <tbody> <tr className="border-b border-hairline"> <td className="py-1.5 pr-3 text-fd-muted-foreground"> checkout </td> <td className="py-1.5"> <Sparkline data="[40, 38, 36, 34, 32, 31, 30, 31]" width="72" height="16" dots="none" summary="false" /> </td> <td className="py-1.5 pl-3 text-right"> 31ms </td> </tr> <tr> <td className="py-1.5 pr-3 text-fd-muted-foreground"> search </td> <td className="py-1.5"> <Sparkline data="[80, 78, 82, 79, 81, 80, 79, 78]" width="72" height="16" dots="none" summary="false" /> </td> <td className="py-1.5 pl-3 text-right"> 78ms </td> </tr> </tbody> </table> ``` ### In a KPI / tab [#in-a-kpi--tab] Attainment and cadence marks stay small enough for a card or a tab header: [Bullet](/docs/charts/bullet), [Progress](/docs/charts/progress), [Delta](/docs/charts/delta), [StatusDot](/docs/charts/status-dot). Exact placement recipes: [Composition](/docs/composition). Broader fit: [When to use microcharts](/docs/when-to-use). ## Server-component safe [#server-component-safe] Static charts are hook-free and listener-free, so they render to HTML with zero client JavaScript. Interactivity comes from a separate `/interactive` subpath. ```tsx // Server Component — no "use client", nothing to hydrate for the chart import { Sparkline } from "@microcharts/react/sparkline"; export default function Row() { return <Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" />; } ``` ## Start here [#start-here] * [When to use microcharts](/docs/when-to-use) * [React sparklines](/docs/react-sparklines) * [Introduction](/docs) — grammar and catalog * [Quickstart](/docs/quickstart) # MCP server (/docs/mcp) `@microcharts/mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server that gives an assistant three tools: find a chart for a question, get its exact props, render it to SVG. It calls the real library, so what comes back is the mark the component draws. It runs on your machine over stdio. Nothing is hosted, there is no key, and no data leaves the process your client spawns. ## Add it to your client [#add-it-to-your-client] The package ships a `microcharts-mcp` binary, so every client below spawns the same thing — `npx -y @microcharts/mcp`. There is no install step of your own: `npx` fetches it on first run. Open yours for its command, its config format, and where the file lives. ### Command-line agents #### Claude Code **Command line** ```bash claude mcp add microcharts -- npx -y @microcharts/mcp ``` Everything after `--` is the command Claude Code spawns. Add `--scope user` to make it available in every project. **Config file** — `.mcp.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Project scope — check this file in and everyone on the repo gets the server. Docs: https://code.claude.com/docs/en/mcp #### Codex CLI **Command line** ```bash codex mcp add microcharts -- npx -y @microcharts/mcp ``` **Config file** — `~/.codex/config.toml` ```toml [mcp_servers.microcharts] command = "npx" args = ["-y", "@microcharts/mcp"] ``` The ChatGPT desktop app and the Codex IDE extension read this same file, so one entry covers all three. Docs: https://developers.openai.com/codex/mcp #### Gemini CLI **Command line** ```bash gemini mcp add --scope user microcharts npx -y @microcharts/mcp ``` Scope defaults to the current project; `--scope user` writes the home config instead. **Config file** — `~/.gemini/settings.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Docs: https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html #### GitHub Copilot CLI **In the app** ```text /mcp add Name microcharts Type Local (STDIO) Command npx -y @microcharts/mcp Tools * ``` Tab moves between fields, Ctrl+S saves. The server is usable without restarting. **Config file** — `~/.copilot/mcp-config.json` ```json { "mcpServers": { "microcharts": { "type": "local", "command": "npx", "args": ["-y", "@microcharts/mcp"], "tools": ["*"] } } } ``` Docs: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers #### opencode **Config file** — `opencode.json` ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "microcharts": { "type": "local", "command": ["npx", "-y", "@microcharts/mcp"], "enabled": true } } } ``` opencode takes the command as one array rather than command plus args. Docs: https://opencode.ai/docs/mcp-servers/ #### Amp **Command line** ```bash amp mcp add microcharts -- npx -y @microcharts/mcp ``` **Config file** — `~/.config/amp/settings.json` ```json { "amp.mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` The VS Code extension reads the same key from its own settings. Docs: https://ampcode.com/manual#mcp #### Goose **In the app** ```text goose configure > Add Extension > Command-line Extension Name microcharts Command npx -y @microcharts/mcp ``` **Config file** — `~/.config/goose/config.yaml` ```yaml extensions: microcharts: type: stdio name: microcharts enabled: true cmd: npx args: ["-y", "@microcharts/mcp"] envs: {} ``` Goose calls the executable `cmd`, not `command`. Docs: https://block.github.io/goose/docs/getting-started/using-extensions/ ### Editors and IDEs #### VS Code **Config file** — `.vscode/mcp.json` ```json { "servers": { "microcharts": { "type": "stdio", "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` VS Code names the object `servers`, not `mcpServers`. For every workspace, run **MCP: Open User Configuration** instead. **Command line** ```bash code --add-mcp '{"name":"microcharts","command":"npx","args":["-y","@microcharts/mcp"]}' ``` Docs: https://code.visualstudio.com/docs/copilot/customization/mcp-servers #### Cursor **Config file** — `.cursor/mcp.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` `~/.cursor/mcp.json` is the same file for every project. Docs: https://cursor.com/docs/context/mcp #### Windsurf **Config file** — `~/.codeium/windsurf/mcp_config.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Cascade panel → the MCP icon → **Configure** opens this file. Docs: https://docs.windsurf.com/windsurf/cascade/mcp #### Zed **Config file** — `settings.json` ```json { "context_servers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Zed calls MCP servers context servers. Settings → AI → MCP Servers → **Add Local Server** writes the same entry. Docs: https://zed.dev/docs/ai/mcp #### JetBrains IDEs **In the app** ```text Settings | Tools | AI Assistant | Model Context Protocol (MCP) > + > As JSON > paste the block below > Apply ``` **Config file** ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Covers IntelliJ IDEA, WebStorm, PyCharm, and the rest of the family — AI Assistant and Junie share the server list. Docs: https://www.jetbrains.com/help/ai-assistant/configure-an-mcp-server.html #### Kiro **Config file** — `.kiro/settings/mcp.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"], "disabled": false, "autoApprove": ["find_microchart", "get_microchart", "render_microchart"] } } } ``` Workspace config wins over `~/.kiro/settings/mcp.json`. Kiro reloads on save — no restart. Drop `autoApprove` to confirm each call by hand. Docs: https://kiro.dev/docs/mcp/configuration/ #### Antigravity **Config file** — `~/.gemini/config/mcp_config.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` `.agents/mcp_config.json` scopes the same block to one workspace. Docs: https://antigravity.google/docs/mcp #### Visual Studio **Config file** — `.mcp.json` ```json { "servers": { "microcharts": { "type": "stdio", "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` `%USERPROFILE%\.mcp.json` for every solution, `<SOLUTIONDIR>\.mcp.json` for one. Saving the file restarts the agent's servers. Docs: https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers #### Cline **Config file** — `cline_mcp_settings.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"], "disabled": false, "autoApprove": [] } } } ``` MCP Servers icon → **Configure** → **Configure MCP Servers** opens it. The CLI reads `~/.cline/mcp.json`. Docs: https://docs.cline.bot/mcp/configuring-mcp-servers #### Roo Code **Config file** — `.roo/mcp.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Project config wins over the global `mcp_settings.json`. Docs: https://docs.roocode.com/features/mcp/using-mcp-in-roo #### Continue **Config file** — `.continue/mcpServers/microcharts.yaml` ```yaml name: microcharts version: 0.0.1 schema: v1 mcpServers: - name: microcharts type: stdio command: npx args: - "-y" - "@microcharts/mcp" ``` Continue takes one block file per server, and `mcpServers` is a list rather than an object. Docs: https://docs.continue.dev/customize/deep-dives/mcp #### Trae **In the app** ```text Settings > MCP > Add > Add Manually ``` **Config file** ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Docs: https://docs.trae.ai/ide/model-context-protocol ### Desktop and chat apps #### Claude Desktop **Config file** — `claude_desktop_config.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` Settings → Developer → **Edit Config** opens it: `~/Library/Application Support/Claude/` on macOS, `%APPDATA%\Claude\` on Windows. Quit and reopen Claude to load it. Docs: https://modelcontextprotocol.io/docs/develop/connect-local-servers #### ChatGPT desktop **In the app** ```text Settings > MCP servers > Add server Name microcharts Transport STDIO Command npx -y @microcharts/mcp ``` Save, then **Restart**. **Config file** — `~/.codex/config.toml` ```toml [mcp_servers.microcharts] command = "npx" args = ["-y", "@microcharts/mcp"] ``` ChatGPT on the web reads no local file and spawns no local process — it reaches remote connectors only. Docs: https://developers.openai.com/codex/mcp #### LM Studio **Config file** — `~/.lmstudio/mcp.json` ```json { "mcpServers": { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } } ``` The **Program** tab → **Install** → **Edit mcp.json** opens it in the app. Docs: https://lmstudio.ai/docs/app/plugins/mcp #### Warp **In the app** ```text Settings > AI > MCP servers > + Add > CLI Server (command) > paste the block below > Save ``` **Config file** ```json { "microcharts": { "command": "npx", "args": ["-y", "@microcharts/mcp"] } } ``` Warp takes the server entry on its own, without an `mcpServers` wrapper. Docs: https://docs.warp.dev/knowledge-and-collaboration/mcp A client that isn't listed takes the same two values, whatever it names the fields: the command is `npx`, and the arguments are `-y` and `@microcharts/mcp`. A client that reaches remote connectors only — ChatGPT on the web, a hosted agent builder, a browser-only IDE — cannot spawn a local process at all. Point it at [`/llms.txt`](/llms.txt) and [`/catalog.json`](/catalog.json) instead. > Node 20 or newer. The server brings its own copy of `@microcharts/react`, so you do not need microcharts installed to use it, and it does not touch a project that already has it. ## Emit, read, call [#emit-read-call] Three ways a model can end up with a correct chart. They stack; none replaces the others. | Verb | Surface | The model gets | | -------- | ---------------------------------------------------------- | ----------------------------------------------------- | | **Emit** | [The grammar](/docs/ai#the-grammar) | A fenced block your app parses into a component. | | **Read** | [`/llms.txt`](/llms.txt), [`/catalog.json`](/catalog.json) | The true API, so the code it writes compiles. | | **Call** | This server | A rendered mark, plus the sentence that describes it. | Emit is for a surface you control, where you can map a block to a component. Call is for a surface you don't control — a chat reply, a terminal, an email draft — where the model needs the finished mark rather than an instruction to draw one. ## The three tools [#the-three-tools] Call them in order, or one at a time. ### `find_microchart` [#find_microchart] Rank the 106 chart types against a question written in plain language. ```json title="call" { "question": "is the error budget burning down?", "limit": 3 } ``` ```json title="result" [ { "slug": "error-budget", "name": "ErrorBudget", "tagline": "Are we burning the error budget too fast to survive the window?", "dataShape": "number[], budget remaining (0–1) per elapsed step; index 0 = 1.0", "why": "an SLO error budget in a KPI card" }, { "slug": "burn-chart", "name": "BurnChart", "tagline": "Will we finish on time?", "dataShape": "{ plan: number[]; actual: number[] }, remaining work per period", "why": "a sprint burndown in a tab header" } ] ``` `why` is the `bestFor` phrase that matched, quoted back rather than generated. An optional `dataShape` argument filters to the shape you already have. ### `get_microchart` [#get_microchart] Everything needed to wire one chart: import paths, its own props plus the [shared grammar](/docs/quickstart#the-shared-grammar), best/avoid guidance, a copy-runnable example, and `sample` — that same example as JSON props. ```json title="call" { "slug": "bullet" } ``` ```json title="result (abridged)" { "slug": "bullet", "staticImport": "@microcharts/react/bullet", "interactiveImport": "@microcharts/react/bullet/interactive", "dataShape": "value + target + bands", "encoding": { "channel": "position (measure length vs a target tick)", "precision": "high" }, "bestFor": ["progress to goal", "SLA / budget vs target", "KPI with thresholds"], "example": { "code": "<Bullet value={72} target={80} bands={[50, 90]} title=\"Quota\" />" }, "sample": { "value": 72, "target": 80, "bands": [50, 90], "title": "Quota" } } ``` The two example forms serve two readers. `example.code` is JSX, for a human to paste. `sample` is JSON, for the model to hand straight to `render_microchart`. Every chart takes its own shape, and guessing between `number[]`, `{ label, value }[]`, and `{ open, high, low, close }[]` is where a model goes wrong. ### `render_microchart` [#render_microchart] Render to a self-contained SVG plus its generated alt text — for a surface that cannot run React. ```json title="call" { "type": "sparkline", "data": [132, 148, 141, 165, 159, 182] } ``` ```json title="result (svg abridged)" { "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"mc-root mc-spark\" …><style>…</style>…</svg>", "summary": "Trending up 38%. Range 132 to 182. Last value 182.", "mimeType": "image/svg+xml", "width": 80, "height": 20, "library": "0.8.0" } ``` Here is that same call as a component on this page, with the same geometry and the same summary: <Sparkline data="[132, 148, 141, 165, 159, 182]" width="160" height="40" title="Weekly revenue" /> The default `format: "svg"` embeds the stylesheet inside the SVG, so the mark renders correctly on a surface with no CSS of its own. Pass `format: "bare"` when the page already loads `@microcharts/react/styles.css`. The tool does not write `summary`. It reads the chart's own accessible name back out of the rendered markup, the same sentence a screen reader speaks. See [Accessibility](/docs/accessibility) for how it's generated. ## Two resources [#two-resources] Alongside the tools, the server publishes two read-only resources: * **`microcharts://catalog`** — every chart type with its props, data shape, encoding, and the `@microcharts/react` version the snapshot was cut from. * **`microcharts://agent-setup`** — the same paste-and-run setup prompt served at [`/agent-setup.md`](/agent-setup.md), for when the assistant should wire the library into a repo rather than render a one-off. ## What it looks like in practice [#what-it-looks-like-in-practice] ### Render, on a surface that can't run React [#render-on-a-surface-that-cant-run-react] You're talking through last quarter's numbers in a desktop assistant. Instead of describing the trend, it calls `render_microchart` and puts a real sparkline in the reply, with the alt text attached, so the sentence and the mark carry the same reading. Nothing on the client side has to understand the grammar. ### Build, in a coding agent [#build-in-a-coding-agent] You're adding an SLO panel in Cursor. The agent calls `find_microchart("error budget")`, gets `error-budget` and `bullet`, calls `get_microchart("bullet")` for the props and a valid `sample`, then scaffolds the component against the real API. There are no invented props to correct in review. ### Ship, in your own AI app [#ship-in-your-own-ai-app] You're building an assistant with the Vercel AI SDK. The same three capabilities are exported as tools, so your model can produce charts without you writing any wiring: ```ts import { microchartsTools } from "@microcharts/mcp/ai-sdk"; import { streamText } from "ai"; const result = streamText({ model, tools: microchartsTools, // find_microchart · get_microchart · render_microchart prompt: "Summarise this quarter and show the revenue trend as a chart.", }); ``` The `ai` package is an optional peer on that subpath, so nothing pulls it into the stdio server. ## How it behaves [#how-it-behaves] A model mid-reply rarely has clean numbers. Every call therefore ends in one of two places: a rendered mark, or a named error. The server never guesses a value to make a call succeed. ### Messy data still renders [#messy-data-still-renders] Degenerate input is a documented case. The chart renders and the summary reports what the data was. These are the real generated sentences: | You send | You get back | | ----------------------- | ----------------------------------------------------------------------------------- | | `[]` | A chart and `"No data."` | | `[7]` | `"Single value 7."` | | `[5, 5, 5, 5]` | `"Flat at 5."` | | `[3, null, 5, null, 9]` | Nulls are **gaps, never zeros** — `"Trending up 200%. Range 3 to 9. Last value 9."` | | `[NaN, 3, Infinity]` | Non-finite values are dropped — `"Single value 3."` | | `[-4, -2, -8]` | `"Trending down 100%. Range -8 to -2. Last value -8."` | A gap is never filled with an invented number, and the sentence never claims more than the data supports. These are the same guarantees the components make; [Accessibility](/docs/accessibility) has the full matrix. ### Calls it refuses, and what it says [#calls-it-refuses-and-what-it-says] Refusals come back as tool errors with the fix in the message, so the model can correct itself in one turn instead of retrying blind: | Call | Error | | -------------------------------------------- | ---------------------------------------------------------------------------------------------- | | An unknown `type` | `unknown chart "piechart"` | | A required prop missing | ``"dot-plot" needs `data`. Data shape: { label, value }[]. Call get_microchart("dot-plot") …`` | | A prop of the wrong type | `` `data` must be number[], got string `` | | An interactive prop (`animate`, `onActive`) | `` `animate` — interactive-only; this tool renders the static chart `` | | A function prop (`strings`, `xFormat`) | `` `xFormat` — a function, which cannot cross JSON `` | | `children` | `` `children` — annotations … are React children and have no JSON form `` | | More data than a word-sized mark holds | `data exceeds 5000 points`, or a size limit on the payload or the rendered markup | | Props that would produce an unrenderable box | ``"sparkline" produced an invalid -5×20 box — check that `width`/`height` … are positive`` | The size limits are deliberate. A series big enough to produce megabytes of markup is a mistake upstream, and returning it would fill the model's context instead of helping. ### What comes back [#what-comes-back] Every render carries `role="img"` and the generated accessible name, so the mark is usable the moment it lands. * **`format: "svg"` (default)** embeds the stylesheet inside the SVG, so the result stands alone. Because the embedded CSS is the real stylesheet, the mark still answers `prefers-color-scheme`, `forced-colors`, and `prefers-reduced-motion` wherever it lands: a chart pasted into a dark-themed client is drawn for dark. * **`format: "bare"`** drops the embedded CSS for a page that already loads `@microcharts/react/styles.css`. It is much smaller per response, and the right choice inside your own app. * **`mimeType`** is `image/svg+xml` for most charts. `Delta` and `TokenConfidence` are inline **text** marks, so they come back as `text/html` with `width`/`height` of `0`: they size to the font around them, not to a pixel box. * **`library`** is the `@microcharts/react` version that drew it. ### Props that do cross a tool call [#props-that-do-cross-a-tool-call] Anything JSON can express: `data`, `value`, `domain`, `title`, `summary`, `color`, categorical `colors`, `label`, `dots`, `width`, `height`, `className`, `style`, `id`, and `positive`. Two are worth calling out: * **`locale`** — `"de-DE"` gives `"Range 1.200 to 1.800."`. Number formatting is fully available. * **`format`** — the `Intl.NumberFormatOptions` half of the union works: `{ "style": "currency", "currency": "EUR" }` yields `"Range €1,200.00 to €1,800.00."`. The callback half does not (see below). Passing `id` switches the chart from `aria-label` to `<title>`/`<desc>` + `aria-labelledby`, as the component does; `summary` still comes back correct. ## What it can't do [#what-it-cant-do] Reach for the component instead when you need any of these: * **It renders the static chart.** No hover, keyboard roving, touch scrub, or selection, and `animate` has no meaning. Those live on the [`/interactive` entry](/docs/quickstart#add-interactivity); `get_microchart` returns `interactiveImport` so an agent can wire it up in code. * **No annotations.** `Threshold`, `TargetZone`, `Marker`, and `Callout` are [React children](/docs/annotations); JSON has no children. * **No function props.** Formatter callbacks and event handlers can't cross a tool call. Use `locale` and the `format` options object for numbers, or set `summary` to supply the sentence yourself. * **No raster output.** You get an SVG or HTML string, never a PNG. Anything that renders SVG will show it; anything that won't needs a converter on your side. * **stdio only.** Your client spawns the server on your machine. There is no hosted endpoint and no HTTP transport, by design, which is also why nothing you chart leaves the process. * **It doesn't touch your project.** No files written, no dependencies installed. To wire the library into a repo, use the [agent setup prompt](/docs/quickstart#set-up-with-an-ai-agent) instead. * **Chart props are a closed set.** Arbitrary `data-*` or `aria-*` attributes are not forwarded. That is a library decision, not a limit of the transport. ## Versioning and compatibility [#versioning-and-compatibility] The server carries a snapshot of the catalog and the stylesheet, cut from a specific `@microcharts/react` release. That version is stamped on the `microcharts://catalog` resource and returned on every render as `library`, so a chart you generate can always be traced to the code that drew it. The two version lines move independently: `@microcharts/mcp` tracks its own changes, and pulls in a compatible library automatically. Pin the server, not the library, if you need a reproducible render. ## Adding a chart [#adding-a-chart] For contributors: nothing about a new chart is written by hand here. The server's catalog is generated from the same chart registry that drives this site, and a test re-derives it from those sources on every run. A chart that lands in the library without the snapshot being regenerated fails CI instead of shipping a server whose catalog is missing it. The rendered sample for every stable chart is exercised in that same suite. *** The package lives at [`@microcharts/mcp`](https://www.npmjs.com/package/@microcharts/mcp), and is listed in the [Glama MCP registry](https://glama.ai/mcp/servers/ganapativs/microcharts). If your assistant doesn't speak MCP, the [Quickstart](/docs/quickstart) has a paste-and-run prompt that teaches it the library directly, and [AI-native](/docs/ai) covers the grammar it emits. # Performance (/docs/performance) Every figure on this page comes from a command you can run yourself. Each one is measured against the shipped `dist`, re-checked in CI, and regenerated on every release. ## Size [#size] Each chart imports from its own subpath, so a bundle ships only the charts it uses: importing `Sparkline` never pulls in the other 105. Sizes are gzip, measured by `pnpm size` and gated, so a chart that grows past its budget fails the build. The `/interactive` subpath lands between **2.18 kB and 6.94 kB** gzip, with a **median of 5.24 kB**. That buys hover, roving keyboard focus, select-to-pin, touch scrub, and live announcements. Its static twin is pure SVG with zero client JavaScript, and lands between **1.08 kB and 4.25 kB**, with a **median of 2.76 kB**. A page pays for interactivity only where it uses it. About 0.5 kB of the interactive delta is the shared picker kernel, which the per-subpath gate measures standalone in every entry; a page using several interactive charts bundles it once. Thirty-two charts sit above the 3 kB static reference line, none of them by more than 1.25 kB. Sparkline, the most configurable primitive in the set, is the largest. Most of the rest are value-series charts that host annotations (ChangePoint, WinProbWorm, ForecastCone, DualSparkline, BurnChart, SpreadBand, RetentionCurve, ControlStrip, QueueDepth, SparkBar, EnsembleGhosts, NetFlow, Slope), plus Station Glyph, PercentileLadder, PercentileTrace, ShiftHistogram, StackedArea, ABStrips, PolarClock, Constellation, Dumbbell, CyclePlot, Waterfall, TapeGauge, MiniBar, BenchmarkStrip, EventTimeline, VolumeProfile, GradedBand and LikertStrip; the last two crossed the line when their percent labels moved onto a real `Intl` formatter. The annotation hosts carry a small shared walker that resolves `Threshold`, `Marker`, and the rest against the chart's scale, and you pay for it only on those charts. ### Splitting the stylesheet [#splitting-the-stylesheet] `@microcharts/react/styles.css` is one artifact for the whole catalog, and importing it once is the right default: it is small, it caches, and it never grows with the number of charts you use. If you ship one or two charts and want the rules for the rest gone, there is an opt-in split. `@microcharts/react/styles/core.css` carries everything shared, and `@microcharts/react/styles/<slug>.css` carries the rules specific to a single chart. Only the charts with chart-specific CSS have a file; the rest need `core.css` alone. ```ts import "@microcharts/react/styles/core.css"; import "@microcharts/react/styles/sparkline.css"; ``` `core.css` plus the slugs you import is equivalent to the matching subset of `styles.css`: same rules, same `@layer` membership, same cascade order. The split trades bytes and changes no behavior. Import one or the other, not both. ## Server rendering [#server-rendering] Static charts are hook-free and listener-free, so a React Server Component renders them to plain SVG on the server and **zero client JavaScript** reaches the browser. Throughput, measured by `pnpm bench` rendering *N* sparklines to an SVG string: A table of **500 sparklines becomes HTML in under six milliseconds**, streams as part of the document, and hydrates nothing. Across all 106 chart types the median **static** SSR render is about **0.03 ms per chart**. The heavier expressive and frontier instruments cost more geometry; the simple strips cost close to nothing. The summary kernel is just as cheap: `describeSeries` runs at roughly **840,000 calls per second** on a 24-point series, measured by `pnpm bench`. It writes the accessible sentence for every chart, so accessibility is not what slows a render down. ## Why it's fast [#why-its-fast] * **No chart engine.** No D3, no layout runtime, no virtual DOM diff for a static chart. Scales, paths, and stats are a few hundred lines of pure functions, and the output is a handful of SVG nodes, typically six or fewer per chart. * **Zero runtime dependencies.** `dependencies: {}`, enforced by CI, forever. There is no transitive tree to resolve, to audit, or to carry in a lockfile. * **Integer geometry, crisp marks.** Coordinates are rounded at generation and rectilinear marks use `shape-rendering: crispEdges`, so the browser does less work rasterizing them. * **One stylesheet, one cascade layer.** Theming is CSS custom properties in a low-specificity `@layer`, not inline styles recomputed per instance. * **A scrub costs one render per unit crossed, not one per pointer move.** Interactive entries put a single listener on the wrapper and resolve the unit with pure math, then bail out when the unit hasn't changed, so a sweep across a 24-point Sparkline re-renders at most 24 times no matter how many pointer events the browser delivers. ### Re-rendering many charts [#re-rendering-many-charts] Static entries are hook-free, which is what keeps them RSC-safe, so they can't memoize internally: a parent re-render re-runs each chart's geometry. That is cheap per chart, but it adds up in a table of hundreds. If you render many charts inside a component that re-renders for unrelated reasons, wrap them and keep `data` referentially stable: ```tsx const Spark = memo(Sparkline); ``` `memo` only helps when the props are stable. Pass a fresh array literal (`data={[1, 2, 3]}`) or JSX children and it misses every time, and you pay the comparison on top of the render you were going to do anyway. Hoist the array, or skip `memo`. The charts don't ship wrapped in `memo` for that reason: it would tax the common case to speed up a narrow one, and the call is yours to make. ## Caveats [#caveats] * **The bench runs in one process, and that used to skew it.** Measuring 106 components in sequence left the later ones cold, and a median over windows short enough for one GC pause produced bimodal numbers: the same chart read 140 and 35 rows/ms on consecutive runs of identical code. `pnpm bench` now warms each component immediately before timing it and reports the fastest window, since GC and scheduler noise only ever slow a window down. Every chart clears its floor; if one stops, `pnpm bench` exits non-zero and names it. * **Interactive entries do ship JavaScript.** The `/interactive` subpath adds a client component, a small set of pointer, keyboard, and touch listeners on the wrapper, and the shared picker kernel that resolves them to a unit with pure math. There is still never a listener per data point, so a 30-point chart and a 365-cell grid cost the same. It is small but it isn't zero, so reach for the static entry unless a chart needs hover, keyboard, selection, or live announcements. * **These numbers come from one machine and one Node.** They measure *relative* cost and reproduce on your hardware; treat them as a floor to reproduce rather than an absolute to quote. ## Reproduce it [#reproduce-it] Every figure above regenerates from the repo. Nothing is hand-keyed; the docs read the measured output directly. ```bash git clone https://github.com/ganapativs/microcharts pnpm install pnpm build # build dist the numbers are measured against pnpm bench && node scripts/sync-bench.mjs # SSR throughput → bench-summary.json node scripts/sync-sizes.mjs # gzip budgets → chart-sizes.json ``` CI runs `--check` on both synced files, so a stale number fails the build the same way a failing test does. # Quickstart (/docs/quickstart) ## Set up [#set-up] Prefer an agent: paste the prompt once. It installs the package, imports the stylesheet at your app root, records the conventions in your agent-rules file, and verifies with a first chart. Or do the two steps yourself under [Set up manually](#set-up-manually). ### Set up with an AI agent [#set-up-with-an-ai-agent] Hand the prompt below to your coding agent: **Cursor, Claude Code, GitHub Copilot, Codex**, or any model, large or small. Works on a fresh repo or an existing one. It's also served verbatim at [`/agent-setup.md`](/agent-setup.md). <div className="agent-prompt"> ```md title="Paste into your agent" Set up **@microcharts/react** in this repo — word-sized, zero-dependency React charts (React 18 or 19 peer, ESM-only). Complete every step in order; don't stop to ask. ## 1 · Install - If this repo is not already a React 18/19 app, scaffold one first (Vite `react-ts` or Next.js), then continue. - Install `@microcharts/react` with this repo's package manager. - Import the stylesheet once at the app root (both package + stylesheet are required): - Next.js App Router: `app/layout.tsx` - Next.js Pages Router: `pages/_app.tsx` - Vite / other React apps: the entry file (e.g. `src/main.tsx`) `import "@microcharts/react/styles.css"` - Charts inherit the page font. If labels look like a serif fallback, set `:root { --mc-font: system-ui, sans-serif; }`. ## 2 · Docs — fetch small, on demand - https://microcharts.dev/llms.txt — index: every chart + guide links. Read this now. - https://microcharts.dev/catalog.json — full API. Do **not** read the whole file. Find the chart by `slug`, then use `sharedProps` + that chart's `props`. If the chart has `sharedInteractive` / the document has `howToRead`, follow those; otherwise use the chart's `.md` for interactive callbacks. Never invent props. - https://microcharts.dev/docs/charts/<slug>.md — one chart's page (import path, edge cases). Fetch only for charts you use — pick the slug from the index. - https://microcharts.dev/docs/quickstart.md — grammar + interactive contract, if you need more than the rules below. - Optional: if this client speaks MCP, `npx -y @microcharts/mcp` serves the same catalog as tools, plus a renderer — https://microcharts.dev/docs/mcp.md If you can't fetch URLs, skip this step — the rules below are enough to render a first chart correctly. ## 3 · Record the conventions Append the rules below as a `## microcharts` section in `AGENTS.md` (create the file if missing — Cursor, Copilot, and Codex read it). If this repo uses Claude Code, mirror it: `ln -s AGENTS.md CLAUDE.md` on macOS/Linux, or copy the section into `CLAUDE.md` on Windows. - One chart = one subpath import, e.g. `import { Sparkline } from "@microcharts/react/sparkline"`. The default export is static — RSC-safe, zero client JS. Add `/interactive` only when hover, keyboard, touch, or selection is needed. That entry is a client component: in Next.js App Router, import it from a `"use client"` file. WindBarb is static-only. - `data` alone always renders; single-value charts take `value` instead. Shared prop names mean the same thing everywhere — see `sharedProps` in `/catalog.json`. Chart-specific props and callbacks live on the chart's catalog entry and its `.md` page; don't memorize them. - Interactive entries share one contract: hover/arrows activate; click/tap/Enter/Space select; Escape clears. Prefer each chart's `sharedInteractive` list when present (lean scalars may take only `onSelect`; MinimapStrip uses `onWindowChange` in chart `props`). For entrance motion: pass `animate` and import `@microcharts/react/motion` once on the client. - `null`, `NaN`, or non-finite values in `data` are gaps, never zeros. - Accessibility is automatic (`role="img"` + a generated summary). Inline in a sentence: wrap in `<span className="mc-inline">` and pass `summary={false}`. - Theme with `--mc-*` or `data-mc-theme` (omit for default), or `defineTheme` from `@microcharts/react/theme`. Charts paint no background. Prefer tokens; `color` / categorical `colors[]` are valid when the chart's docs say so. - Annotations are children: `Threshold`, `TargetZone`, `Marker`, `Callout` from `@microcharts/react/annotations`. - Never add another charting dependency. No pie or gauge — use `Bullet`, `SegmentedBar`, or `MicroDonut`. ## 4 · Verify Render `<Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" />` in a real page, then run the dev server or build. Self-check: - Chart missing or unstyled → the styles.css import isn't at the root (step 1). - Labels render in a serif font → set `--mc-font` (step 1). Finish with a short report: the installed version, the file importing styles.css, the file holding the conventions, and the page rendering the test chart. ``` <AgentPromptCopy /> </div> The prompt fetches small, targeted files — the [`/llms.txt`](/llms.txt) index first, then one chart's `.md` page at a time — so it stays inside the context window of smaller models and reads today's real API. The rules are inlined too, so it still works when the agent can't reach the network. > **Does your assistant speak MCP?** Add the server instead of pasting the prompt, and it can search the catalog and render charts as tool calls rather than reading docs: `npx -y @microcharts/mcp`. Per-client config and the tool reference are in [MCP server](/docs/mcp). Use both: the prompt wires the library into your repo, the server answers chart questions while you work. ### Set up manually [#set-up-manually] Two steps, once, at your app root: install the package, import the stylesheet. Both are required. **1 · Install.** React 18 or 19 is a peer dependency; there are no other runtime dependencies. **2 · Import the stylesheet once** at the root of your app. It carries every theming token and chart style in a low-specificity cascade layer, so your own styles always win. What counts as the root depends on your framework: anywhere that runs once, before any chart renders. #### Next.js ```tsx title="app/layout.tsx" import "@microcharts/react/styles.css"; ``` #### Vite / CRA / other React apps ```tsx title="src/main.tsx" import "@microcharts/react/styles.css"; ``` > Charts **inherit your page's font**. If the surrounding page never sets a `font-family` (some minimal starters and sandboxes don't), SVG text falls back to a serif and labels look wrong — give your page a font, or set one just for charts: ```css :root { --mc-font: system-ui, sans-serif; } ``` Full detail — why it inherits, and per-scope fonting — in [Theming](/docs/theming#the-font). ## Your first chart [#your-first-chart] Every series chart renders from `data` alone; single-value charts take `value`. Either way it works in a React Server Component with **zero client JavaScript**: it's pure SVG, and its accessible name is generated from the data. ```tsx ; }`" > <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" title="Weekly revenue" /> ``` ### Try it live [#try-it-live] No install needed. Open a full Vite + React app with the package installed in StackBlitz — static and interactive charts, inline and block, four themes, running in your browser. <StackBlitzSandbox /> ## The shared grammar [#the-shared-grammar] One idea runs through the whole catalog: **the same prop name means the same thing on every chart.** Learn these once and they transfer everywhere — a new chart type is a new data shape, never a new vocabulary. The data prop alone always renders something correct; everything else is opt-in. | Prop | Type | What it does | | ---------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | `(number \| null)[] \| per-chart shape` | The series. `null` / `NaN` are gaps, never zeros. The one required prop; each chart documents its own shape when it isn't a plain number array. | | `domain` | `[min, max]` | Fix the value range instead of auto-fitting — see [Formatting & scale](/docs/formatting#scale-the-domain-decision). | | `color` | `string` | Mark color: any CSS color or a token (`"var(--mc-accent)"`). Semantic tokens keep their meaning — see [Theming](/docs/theming). | | `title` | `string` | Names the chart — an SVG `<title>`, so it becomes the accessible name and the hover tooltip, not visible text. | | `summary` | `string \| false` | Override the auto-generated accessible sentence, or pass `false` to drop it — with no `title` that marks the chart decorative — see [Accessibility](/docs/accessibility). | | `format` | `Intl.NumberFormatOptions \| (n) => string` | How numbers render in labels and summaries — see [Formatting & scale](/docs/formatting). | | `positive` | `"up" \| "down"` | Which direction is good, so color and arrows agree with it (lower latency is `"down"`). | | `label` | *per chart* | A short direct label next to the mark, in a reserved gutter — never an axis or legend. | | `dots` | *per chart* | Which individual points to emphasize (e.g. `"minmax"` on a line). | Not every prop applies to every chart: a bar chart has no `dots`, and the single-value charts take `value` in place of `data`. But wherever a concept exists, its prop name and meaning are always these. Each one has a deeper home: [Formatting & scale](/docs/formatting) owns `format` and `domain`, [Theming](/docs/theming) owns `color`, and [Accessibility](/docs/accessibility) owns `summary`. Alongside the grammar sit the layout props: `className` and `style` on every chart, plus `width` and `height` on the charts whose geometry is sized that way (the fixed-glyph ones size from `style` or their own `size` / `cell` knob) and `id`, which opts into `<title>`/`<desc>` + `aria-labelledby` naming; see [Sizing](/docs/sizing). The i18n props are `locale` (BCP-47, for number and date formatting) and `strings` (the summary/label templates), plus `seriesStrings` on the multi-series charts — see [Accessibility](/docs/accessibility#internationalization-and-rtl). Everything else is specific to a chart's own shape and listed in the prop table on its page. To place charts in real UI — or render several on one shared scale with `SparkGroup` — see [Composition](/docs/composition). Annotations are children: [`Threshold`, `TargetZone`, `Marker`, `Callout`](/docs/annotations). ## Add interactivity [#add-interactivity] Need hover, keyboard navigation, touch, or live announcements? Import the same chart from its `/interactive` entry. The rendered output and the accessible name are identical, because the interactive entry composes its static twin. It only **adds** props: the interaction callbacks below, plus `animate`, `strings` on the charts whose static entry doesn't take it, `live` on the single-value charts, and `onWindowChange` on MinimapStrip. The interactive entry **is** a client component: in a Next.js App Router page, render it from a file with `"use client"` at the top (or anywhere inside one). ```tsx "use client"; import { Sparkline } from "@microcharts/react/sparkline/interactive"; <Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" />; ``` ### One interaction contract [#one-interaction-contract] Every interactive chart behaves the same way, so you learn it once. Hover or arrow keys make a unit **active**; a click, tap, **Enter**, or **Space** **selects** it and pins the readout so it survives blur. **Escape** clears, **Home**/**End** jump to the ends, and arrows rove in both axes (←/↑ previous, →/↓ next). On touch, a tap pins and a drag scrubs. What a "unit" is depends on the chart — a point, a bar, a day cell, a segment — and each chart's page names its own. | Prop | Type | What it does | | ---------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ | | `onActive` | `(datum: MicroDatum \| null) => void` | The active unit changed (pointer or keyboard). `null` when the chart clears. | | `onSelect` | `(datum: MicroDatum \| null) => void` | A unit was selected or unselected — click, tap, `Enter`, `Space`, `Escape`. | | `selectedIndex` | `number \| null` | Control the selection yourself. | | `defaultSelectedIndex` | `number \| null` | Seed the selection once, then let the chart own it. | | `readout` | `boolean` | Show the floating value chip on hover (default `true`). `false` hides only the chip. | `MicroDatum` is `{ index, value, label?, formatted? }`. Two of the fields carry the value in different forms, so you pick the one that fits. **`value`** is the raw primary number, for computing (`null` when the unit encodes nothing). **`formatted`** is the ready-to-display string the chart's own readout would show, built with your `format`/`locale`, for rendering. For a plain chart that's just the value (`"$0.53"`); for a two-part unit it's the whole readout (`"48 → 68 (up 42%)"`). `index` identifies the unit (point, bar, cell, segment…), and `label` is its human name where one exists (`"Berlin"`, `"Checkout"`). Same shape on every chart and both callbacks. Pair the two to lift the value out of the chart. Set `readout={false}` to keep the hover crosshair but suppress the in-chart chip, and read `datum.formatted` in `onActive` to render that value wherever you like — a KPI number, a sentence, a table cell — without re-deriving `format`/`locale` yourself: ```tsx const [spend, setSpend] = useState("$0.53"); <Sparkline data={cost} format="$,.2f" readout={false} onActive={(d) => d && setSpend(d.formatted!)} />; ``` Scalar charts with a single unit — Delta, Progress, StatusDot, Bullet and the rest — take `onActive` and `onSelect` too: the one unit is still hovered, focused and activated, so the `readout={false}` pattern above works there as well. What they don't take is `selectedIndex`/`defaultSelectedIndex` — there's no cursor to move and no index to point it at. Fifteen of them also take `live` (default `true`): as the value updates, the new reading is announced through a polite live region, throttled so a streaming number never spams a screen reader. Not all of them do (Bullet's only interaction prop is `onSelect`), so check the chart's own page. Two charts opt out of the picker on purpose: `MinimapStrip` is a range slider with its own `onWindowChange([lo, hi])`, and `TokenConfidence` moves real focus through inline text spans. Full detail, including the live-region behavior, is on [Accessibility](/docs/accessibility#one-interaction-contract). ## Animate on mount (optional) [#animate-on-mount-optional] Interactive entries also take an `animate` prop: the chart draws on instead of appearing instantly. It's off by default, and inert on the server and on hydrated server HTML, so it never fights SSR or flashes over painted content; a fresh client-side mount is what plays it. Wire the engine once, client-side, then opt in per chart: ```tsx "use client"; import "@microcharts/react/motion"; // once, client-side import { Sparkline } from "@microcharts/react/sparkline/interactive"; <Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" animate />; ``` `prefers-reduced-motion` wins unconditionally: a reader who asked for less motion gets the finished chart with no entrance animation. ## Import paths [#import-paths] Import each chart from its own subpath so you only ship what you use. Nearly every chart follows the same two-entry pattern — a static default and an `/interactive` twin (WindBarb is the lone static-only exception): | Chart | Static | Interactive | | ------------ | ---------------------------------- | ---------------------------------------------- | | Sparkline | `@microcharts/react/sparkline` | `@microcharts/react/sparkline/interactive` | | SparkBar | `@microcharts/react/sparkbar` | `@microcharts/react/sparkbar/interactive` | | Delta | `@microcharts/react/delta` | `@microcharts/react/delta/interactive` | | Bullet | `@microcharts/react/bullet` | `@microcharts/react/bullet/interactive` | | ActivityGrid | `@microcharts/react/activity-grid` | `@microcharts/react/activity-grid/interactive` | Every other chart uses the same shape (WindBarb ships static-only, with no `/interactive` twin) — see [All charts](/docs/charts) for the full catalog, each page listing its own import path and props. ## Compatibility [#compatibility] * **React 18 or 19** as a peer dependency — no other runtime dependencies, ever (`dependencies: {}`, CI-enforced). * **ESM-only.** The package ships `"type": "module"`; there's no CommonJS build. * **Server-component and SSR-safe.** Static entries are hook-free and listener-free, so they render to plain SVG on the server with zero client JavaScript — no `"use client"` needed unless you import from `/interactive`. * **Browser floor: evergreen, ES2022.** That includes Safari from version 16.4 — a couple of ES2022 array methods used internally crash on older Safari, so 16.4 is the practical minimum regardless of your `tsconfig`. # React sparklines (/docs/react-sparklines) A **sparkline** is a trend small enough to sit in a sentence: enough line to read direction and shape, with no axes, legend, or panel around it. In React that means a word-sized SVG mark next to a number or inside a table cell. ```tsx <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" title="Weekly revenue" /> ``` This page covers that mark in `@microcharts/react`. Props, variants, and edge cases live on [Sparkline](/docs/charts/sparkline). ## Install [#install] Both steps are required — package and stylesheet: ```tsx import { Sparkline } from "@microcharts/react/sparkline"; import "@microcharts/react/styles.css"; // once, at the app root <Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" />; ``` Measured size for this chart: <ChartSize slug="sparkline" /> gzip (CI-enforced). Catalog-wide band: <SizeMarketing />. The default export is static, so it renders from an RSC with zero client JavaScript. Add `@microcharts/react/sparkline/interactive` when you need hover, keyboard, touch, or selection. ## Where it sits [#where-it-sits] Same component in four homes — product UI, report, docs, and an AI reply: **In a sentence** ```tsx <p> p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={16} dots="none" summary={false} /> </span>{" "} — trending down. </p> ``` **In a table cell** ```tsx <tr> <td>checkout-api</td> <td> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={18} dots="none" summary={false} /> </td> <td>31 ms</td> </tr> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1,600</span> <span className="unit">concurrent, now</span> <Sparkline data={[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]} width={90} height={28} fill summary={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> CPU <Sparkline data={[62, 65, 61, 68, 70, 66, 72, 75]} width={40} height={14} dots="none" summary={false} /> </button> ``` Placement recipes: [Composition](/docs/composition). When this library fits vs a full chart library: [When to use microcharts](/docs/when-to-use). ## About `react-sparklines` [#about-react-sparklines] Many searches still land on [`react-sparklines`](https://www.npmjs.com/package/react-sparklines) (Boris Yankov), which still pulls about **250k downloads** in the week ending **2026-07-21** on a last npm publish of **2017-07-27** (`1.7.0`). It's the same visual idea from a different era of React: class-component patterns, `prop-types`, no generated accessible summary, and no RSC story. The measured side-by-side lives at [microcharts vs react-sparklines](/docs/vs-react-sparklines). microcharts isn't that package rewritten. It's a word-sized catalog of <CatalogTotal /> types under one grammar, and Sparkline is one chart in it: `data` alone renders, accessibility is on by default (`role="img"` plus a summary from the data), the static entry is RSC-safe, and there are **zero runtime dependencies**. If `react-sparklines` still fits your stack, keep it. If you want the contract above, start here. ## When not to use a sparkline [#when-not-to-use-a-sparkline] * Exact category comparison → [MiniBar](/docs/charts/mini-bar) or [SparkBar](/docs/charts/sparkbar) * Value against a target → [Bullet](/docs/charts/bullet) * Part-to-whole → [SegmentedBar](/docs/charts/segmented-bar) or [MicroDonut](/docs/charts/micro-donut) * Full axes, legend, brush, zoom → a [full chart library](/docs/full-chart-libraries) ## Related marks [#related-marks] * [SparkBar](/docs/charts/sparkbar) — discrete magnitudes or win–loss * [DualSparkline](/docs/charts/dual-sparkline) — two series, one scale * [TrendArrow](/docs/charts/trend-arrow) — direction as a glyph * [Delta](/docs/charts/delta) — a single signed change * [Horizon](/docs/charts/horizon) — wide-range series in a slim band ## Next [#next] * [Sparkline API](/docs/charts/sparkline) * [Inline charts](/docs/inline-charts) * [When to use microcharts](/docs/when-to-use) * [Quickstart](/docs/quickstart) # Sizing & scaling (/docs/sizing) Every chart is a single `<svg>` with a `viewBox` and `preserveAspectRatio`. Give it a size in whatever way suits the layout and the geometry follows. There's no `ResizeObserver`, no layout effect, and no client JavaScript: a static chart scales through SVG and CSS alone. ## The three ways to size a chart [#the-three-ways-to-size-a-chart] | You want… | Do this | | ---------------------------- | -------------------------------------------------------- | | A sensible default | Pass nothing — every chart has an intrinsic box. | | An exact pixel size | Set `width` / `height` props (where the chart has them). | | To fill whatever contains it | Add `style={{ width: "100%", height: "auto" }}`. | > Not every chart has `width`/`height`. Grid charts derive their box from the cell: `ActivityGrid`, `CalendarStrip`, `GardenGrid` and `CohortTriangle` take `cell` and `gap`, `Honeycomb` takes `cell`, and `ConfusionGrid` takes `size`. The box then follows from the number of cells, so you tune the cell rather than the frame. Glyph marks (`StatusDot`, `TrendArrow`, `MoonPhase`, `DicePips`, and the rest) have one fixed intrinsic box and are sized with CSS. And `TokenConfidence` isn't an `<svg>` at all: it renders text, so it inherits the font size of whatever it sits in. Everything below about `viewBox` scaling applies to the SVG charts. ## Default: data alone [#default-data-alone] `data` alone renders at the chart's own intrinsic size. Each chart type ships a default box tuned to its shape: a Sparkline defaults to a wide, short 80 × 20, while a RubricStrip has no fixed default height and grows a row per criterion. Use it to drop a mark into a table cell or a sentence without picking dimensions. ```tsx `" > <Sparkline data="[3, 5, 4, 8, 6, 9]" title="Weekly revenue" /> ``` ## Fixed size: width & height [#fixed-size-width--height] `width` and `height` are **viewBox units**. A static chart renders `<svg width={…} height={…}>`, so they also set the rendered pixel box: one pair of numbers controls both the coordinate space the geometry is drawn in and the size on screen. ```tsx `" > <Sparkline data="[3, 5, 4, 8, 6, 9]" width="200" height="48" title="Weekly revenue" /> ``` > Because `width`/`height` set the viewBox, they change how much room the geometry has: a taller RubricStrip gives each criterion a thicker row, a wider Sparkline spreads its points further apart. They are layout dimensions, not a zoom level. ## Fill the container [#fill-the-container] To make a chart fluid, drop its pixel size and let CSS drive the width instead. The `viewBox` keeps the aspect ratio, so `height: auto` follows the width. This is the one-liner for a KPI card, a responsive dashboard tile, or a full-width hero: ```tsx <Sparkline data={data} style={{ width: "100%", height: "auto" }} /> ``` The chart now grows and shrinks with its parent. To constrain it, size the container or give it a `max-width`: ```tsx <div style={{ width: "100%", maxWidth: 320 }}> <Sparkline data={data} style={{ width: "100%", height: "auto" }} /> </div> ``` You can still pass `width`/`height` alongside fluid CSS: they become the chart's intrinsic aspect ratio and its size before CSS overrides the width. Utility classes work the same way (`className="w-full h-auto"`). To give a whole row of charts one identical box rather than sizing each by hand, put `width`/`height` on `SparkGroup`: it enforces them on every series child that didn't set its own, and shares one scale at the same time. See [Formatting & scale](/docs/formatting#scale-the-domain-decision). > `.mc-root` is `display: inline-block` by default, so a chart flows next to text. Filling a block-level container works anyway; if you ever see one refuse to stretch, that's the reason. Give it `display: block` or a flex/grid parent. ## Interactive charts size the same way [#interactive-charts-size-the-same-way] Everything above applies **unchanged** to the `…/interactive` entries. An interactive chart wraps its SVG in a focusable `<span>` that owns the pointer, keyboard, and touch gestures for the whole chart, and that wrapper and the SVG are always the **same box**: the wrapper is an `inline-block` that hugs its child, and the composed SVG inside is pinned to `width: 100%; height: auto`. Your `style` merges over the wrapper's base style and your `className` composes after its base class, so the same recipe fills it: ```tsx import { Sparkline } from "@microcharts/react/sparkline/interactive"; // identical to the static entry — fills its container <Sparkline data={data} style={{ width: "100%", height: "auto" }} />; ``` > Because the wrapper and the SVG share one box, hit-testing is exact at any size: the crosshair, highlight, and readout land under the cursor, a touch drag scrubs the unit beneath the finger, and a pinned selection stays on the right unit when the box reflows. An interactive chart is its static twin plus interaction, at the same size. ## Inline in a sentence [#inline-in-a-sentence] A chart in running prose should scale with the **font** rather than a pixel box. Wrap it in `className="mc-inline"` (shipped in `styles.css`) and size it in `em` so it rides the text: ```tsx <p> Revenue is up this week{" "} <span className="mc-inline"> <Sparkline data={[3, 5, 4, 8, 6, 9]} style={{ height: "1em", width: "auto" }} /> </span>{" "} and holding. </p> ``` `mc-inline` handles vertical alignment; the `em` height ties the mark to the surrounding type size, so it stays proportional wherever the text lands. The alignment is a **baseline seat** rather than a fixed nudge: the wrapper is an `inline-flex` box whose only child is the SVG, so it takes its baseline from the SVG's bottom edge and the mark stands on the text baseline like a glyph. That holds in any typeface. Symmetric glyph marks have no bottom edge to stand on, so `mc-inline` centers those on the cap band instead: `TrendArrow`, `StatusDot`, `MoonPhase`, `DicePips`, the radial dials, and the other glyph shapes read like an icon set in running prose. You don't opt in. Which seat a mark gets is never guessed from its class name. Each chart reports its own plot box, measured in the same viewBox units it draws in, and the stylesheet seats the mark from that. So a chart whose padding changes can't drift out of alignment, and a chart that changes shape with its props carries the right seat for each: `SparkBar` stands on the baseline in bar mode and centers in win-loss, because only bar mode has a floor. Two tokens nudge the result if a particular typeface needs it: `--mc-inline-nudge` for every mark, `--mc-glyph-nudge` for centered ones only. > Leave `<Delta>` bare, with no `mc-inline` wrapper. It renders as inline text and already owns its own baseline. ## Strokes stay crisp at any scale [#strokes-stay-crisp-at-any-scale] Scaling an SVG normally thickens or thins its lines. microcharts sets `vector-effect: non-scaling-stroke` on data marks, so a line drawn at 200 px and the same line at 800 px keep the **same visual stroke weight**: the geometry scales, the ink doesn't. Rectilinear marks also use `shape-rendering: crispEdges`, so bars and grids stay sharp at any size. Stroke weight is a token: set `--mc-stroke-width` (see [Theming](/docs/theming)) to make it heavier or lighter, independent of how big the chart is. For a whole dense context, a table packed with sparklines, use `--mc-density`: one scalar that scales stroke weight, label size, and small-multiple gap together (`< 1` tighter, `> 1` airier). It tunes the ink and type, never the box, so `width`/`height` still own the geometry. # Theming (/docs/theming) microcharts theming is CSS custom properties. There's no theme provider to wire up and no Tailwind requirement: set a `--mc-*` variable at any scope and every chart inside it retunes, with zero re-render and no build step. (Prefer a React component to a raw attribute? [`MicroProvider`](#a-component-api-if-you-prefer) wraps the same contract.) Two ideas carry the system. A **token** is one variable you set; a **scope** is where you set it. The rest of this page is the vocabulary: every color, weight, and timing you can set, plus a studio that hands you any combination as ready-to-paste CSS. ## Override a token [#override-a-token] ```css .brand { --mc-accent: #7c3aed; --mc-stroke-width: 2; } ``` Every chart inside `.brand` now draws with that accent and stroke weight. Precedence runs one way: a **prop** wins over a **CSS-variable scope**, which wins over a **preset**, which wins over the built-in default. ```tsx <Sparkline data={data} color="#7c3aed" /> <Sparkline data={data} color="var(--mc-positive)" />`" > <Sparkline data="[3, 5, 4, 8, 6, 9]" width="110" height="30" title="Default" /> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="110" height="30" color="#7c3aed" title="Custom" /> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="110" height="30" color="var(--mc-positive)" title="Token" /> ``` ## Scope it anywhere [#scope-it-anywhere] Because the tokens cascade, you choose the scope: the whole app, one brand section, a single card, or one chart. Set a token high and every chart below inherits it; override it lower and only that subtree changes. ```tsx // app-wide, once <html style={{ "--mc-accent": "var(--brand-500)" }}> // just this card's charts <div style={{ "--mc-stroke-width": 2, "--mc-gap": "0.15em" }}> <SparkBar data={weekly} /> </div> ``` Bind `--mc-accent` to your existing brand variable and every chart follows your design system without a per-chart prop or a duplicated color value. ## The token set [#the-token-set] That's the whole mechanism; the rest is vocabulary. About two dozen custom properties cover the entire surface, and setting any of them at any scope retunes every chart inside. Here are the color tokens with their built-in light and dark values. Every chip is the shipped default; click one to copy the hex. <TokenSwatches /> Below is the same set as raw CSS: the colors plus the geometry, motion, and readout-surface tokens, ready to paste and override. Want a specific style or accent instead of the bare defaults? The [studio](#copy-tokens) further down builds any combination for you. ```css :root { /* Semantic color */ /* Default ink: lines, bars, labels. Dark mode is hand-tuned, not inverted. */ --mc-stroke: #1a1917; /* Good direction — color-blind-safe viridian; pair with ▲. */ --mc-positive: #0e7a5f; /* Bad direction — color-blind-safe terracotta; pair with ▼. */ --mc-negative: #bd4b2d; /* No-signal marks: baselines, inactive points, empty cells. */ --mc-neutral: #8a8986; /* Emphasis color — rebind to your brand. */ --mc-accent: #1f6091; /* Normal-range shading, derived from the ink. */ --mc-band: color-mix(in oklab, var(--mc-stroke) 8%, transparent); /* MoonPhase lit area — warm amber; mono/print/eink retune it with the ink. */ --mc-moon: #c1922f; /* Categorical palette — matte jewel tones; multi-series only, lightness-ordered. Each has a hand-tuned brighter twin on dark surfaces (same hue, lifted). */ --mc-cat-1: #d2982f; --mc-cat-2: #5b9fd4; --mc-cat-3: #2e8c66; --mc-cat-4: #285788; --mc-cat-5: #bc5138; --mc-cat-6: #a55a89; /* Geometry */ /* Light by default for inline use; bump to ~2 when standalone. */ --mc-stroke-width: 1.5; /* Spacing between grouped marks; em-based so it scales with the font. */ --mc-gap: 0.25em; /* Uniform density scale — compact (<1) vs comfortable (>1). Tunes stroke weight, label size, and small-multiple gap together; the box set by width/height is untouched. */ --mc-density: 1; /* Direct-label text size, relative to the chart's font. */ --mc-label-size: 0.75em; /* Direct-label weight. */ --mc-label-weight: 400; /* `.mc-inline` seats marks on the text baseline (font-independent). This is an optional extra optical shift on top — 0 by default; set it only if a particular face wants marks a hair off the line. */ --mc-inline-nudge: 0em; /* Extra optical shift for centred marks (arrow, dot, moon, dials, strips) on top of the cap-band seat — font-relative, negative lifts. 0em by default: the seat already lands on the true cap midpoint, so there is nothing left to correct unless a display face has an unusual cap ratio. */ --mc-glyph-nudge: 0em; /* Motion & type */ /* Motion duration — readout transitions, value pulses, entrance animation. */ --mc-duration: 300ms; /* Ease-out entrance; no bounce or loop. */ --mc-easing: cubic-bezier(0.22, 1, 0.36, 1); /* Inherits the host UI font + tabular numerals by default. */ --mc-font: inherit; /* Face for figures + labels — tracks --mc-font; point at a brand or mono face to give numbers their own typeface without touching the page font. */ --mc-font-numeric: var(--mc-font); /* Interactive readout chip (hover, focus, or a pinned selection). Defaults to system canvas. */ --mc-surface: Canvas; --mc-surface-ink: CanvasText; --mc-surface-edge: color-mix(in oklab, CanvasText 16%, transparent); } ``` ## Semantic vs categorical color [#semantic-vs-categorical-color] The color tokens split into two groups, and the split decides what a theme is allowed to change. **Semantic tokens keep their meaning.** Swapping `--mc-accent` retints the one emphasis color; `--mc-positive` and `--mc-negative` still mean up and down. A preset can restyle a chart without changing what it says. **Categorical colors are a last resort.** Most microcharts show one series and never touch the palette. When a chart needs several, the six `--mc-cat-*` hues are lightness-ordered for grayscale and color-vision separation. Gold leads because true yellow is too low-contrast on a light surface. To recolor the series on *one* instance without touching the global palette, the categorical charts (`SegmentedBar`, `StackedArea`, `PartitionStrip`, `Hypnogram`, `MicroDonut`) take a `colors` array. It cycles per series and overrides `--mc-cat-*` for that instance only. A rolled-up "Other" segment stays neutral instead of taking a series color. ```tsx `"> <SegmentedBar data="[ { label: "A", value: 34 }, { label: "B", value: 26 }, { label: "C", value: 22 }, { label: "D", value: 18 }, ]" colors="["#2563eb", "#db2777", "#65a30d", "#f59e0b"]" width="220" height="16" title="Custom series colors" /> ``` ## Presets [#presets] A **preset** is a whole look bundled under one name, applied with a `data-mc-theme` attribute. It's pure CSS with zero JavaScript, and it changes how charts look, never what the data means. `modern` is what you get with no attribute at all: it's the base token set rather than a bundle layered on top, so it needs no attribute. ```tsx <Sparkline data={data} dots="minmax" /></div> {/* modern — the default, no attribute */} <div data-mc-theme="editorial"><Sparkline data={data} dots="minmax" /></div> <div data-mc-theme="vivid"><Sparkline data={data} dots="minmax" /></div> <div data-mc-theme="mono"><Sparkline data={data} dots="minmax" /></div>`" > <div> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="100" height="28" dots="minmax" title="modern" /> </div> <div data-mc-theme="editorial"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="100" height="28" dots="minmax" title="editorial" /> </div> <div data-mc-theme="vivid"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="100" height="28" dots="minmax" title="vivid" /> </div> <div data-mc-theme="mono"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="100" height="28" dots="minmax" title="mono" /> </div> ``` What each preset retunes; every other token stays at its default: <PresetDeltas /> The last two, `print` and `eink`, are output-context bundles rather than a look you'd ship on screen. `print` pins a near-black ink, deepens the valence colors for CMYK, and thins the stroke to `1.25`. `eink` drops to grayscale and lets sign ride lightness (positive black, negative gray) with a `2` stroke for a low-refresh e-paper panel. Apply them the same way: `data-mc-theme="print"` on the region you're about to render. Both pin a fixed light-surface ink, so both also ship hand-tuned dark twins. On a dark viewer `eink` flips to white ink on black and `print` lifts to a dark-paper ink, while the stroke weight and band identity carry over. The look presets (`editorial`, `mono`, `vivid`, `print`, `eink`) also answer to `data-mc-preset`, the page-global form: set it once on `<html>` for an app shell, and a nearer `data-mc-theme` still wins by proximity. `dark` is the exception, matching `data-mc-theme="dark"` only, because it's a color-scheme scope rather than a look you'd pin app-wide. > Every preset above carries a **Copy tokens** button, and the [studio below](#copy-tokens) pairs any preset with your own accent and copies the result for both modes. This site's appearance menu (the palette icon in the nav) applies the same bundles live across every page. Picking an accent there re-hues every chart on the site, including the categorical palette, because it derives one with [`defineTheme`](#build-a-theme-from-one-colour) the way the studio does. It's the same token contract you'd use in your own app. ## Copy the tokens [#copy-tokens] The studio puts everything above in one place. Pick a style, add your brand accent, choose a mode, and copy the `--mc-*` block, light and hand-tuned dark together, ready to paste into your app. The preview renders real charts in both modes and names each role it paints, so a choice shows up as you make it: the emphasis line takes your accent, and the categorical bars re-hue to a matched palette **derived from it** by [`defineTheme`](#build-a-theme-from-one-colour), the same engine the nav's appearance menu uses to re-theme the whole site. What you preview is what you copy, and what the library ships. <TokenStudio /> Paste the CSS into any stylesheet and every chart in scope retunes. Prefer the component API? Switch the format to a tokens object and spread it onto [`MicroProvider`](#a-component-api-if-you-prefer), or call [`defineTheme`](#build-a-theme-from-one-colour) directly to derive that matched palette in code. ## The MicroProvider component [#a-component-api-if-you-prefer] Everything above works through plain attributes and CSS variables. If you'd rather set the theme in JSX, `MicroProvider` wraps the same contract in a component. It's hook-free and RSC-safe, so it works in Server Components. ```tsx import { MicroProvider } from "@microcharts/react"; <MicroProvider theme="editorial" tokens={{ "--mc-accent": "var(--brand-500)" }}> <Sparkline data={data} /> </MicroProvider>; ``` It renders a single `<div>` 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. Both forms resolve to the same cascade, so use whichever reads better in your code. ## Build a theme from one color [#build-a-theme-from-one-colour] Setting a token or two by hand is the common case. For a whole brand theme, with a matched categorical palette and hand-tuned dark twins for every color, use `defineTheme` from `@microcharts/react/theme`. Give it your brand accent and it derives the rest in OKLCH: a harmonized, color-blind-safe categorical palette and a lifted dark-mode twin for each token. It never moves the positive/negative hues off their CVD-safe green/vermillion split, so a derived theme encodes direction the way the defaults do. ```tsx import { defineTheme } from "@microcharts/react/theme"; import { MicroProvider } from "@microcharts/react"; const brand = defineTheme({ accent: "#6d28d9" }); // spread the tokens onto one scope… <MicroProvider style={brand.style}> <SegmentedBar data={mix} /> </MicroProvider>; // …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 values `defineTheme` returns: ```tsx <SegmentedBar data={mix} title="Derived palette" /> </MicroProvider>`" > <div style="{ "--mc-accent": "#6d28d9", "--mc-cat-1": "#8b7cc5", "--mc-cat-2": "#9a4f72", "--mc-cat-3": "#bc7548", "--mc-cat-4": "#6d7218", "--mc-cat-5": "#2d9e82", "--mc-cat-6": "#04759f", }" > <SegmentedBar data="[ { label: "A", value: 34 }, { label: "B", value: 26 }, { label: "C", value: 18 }, { label: "D", value: 12 }, { label: "E", value: 10 }, ]" width="220" height="16" title="Derived palette" /> </div> ``` 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. ```css /* a denser table of sparklines */ .metrics-table { --mc-density: 0.85; } ``` One asymmetry: **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 safe. So `--mc-density: 2` gives you double-weight strokes and double gaps, with labels at 1.25×. For larger text, scale the chart with `width`/`height` or `--mc-label-size` instead of leaning on density. ## The font [#the-font] `--mc-font` defaults to `inherit`, so charts adopt the font of whatever contains them. If the surrounding page sets a `font-family`, charts follow it with no configuration. 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 point `--mc-font` at a stack for the charts alone. ```css :root { /* charts only — the rest of the page is untouched */ --mc-font: system-ui, sans-serif; } ``` Numbers always render with `tabular-nums`, 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`) sets label weight the same way. ```css :root { --mc-font: Inter, sans-serif; /* labels */ --mc-font-numeric: "IBM Plex Mono", monospace; /* figures */ } ``` ## The readout surface [#the-readout-surface] The one opaque plane in the 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 the chip adapts to light, dark, and forced-colors with no configuration. Point them at your popover tokens to match a themed surface. 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 break the dark surface. microcharts follows `prefers-color-scheme` by default, and you can force a scope with `data-mc-theme="dark"`. Charts never paint their own background, so they sit on whatever surface you give them. 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 or tinted panel the composited backdrop is quieter, so deepen `--mc-stroke`, `--mc-positive`, `--mc-negative`, and `--mc-neutral` on that surface until contrast holds. Rebind the tokens rather than patching chart CSS. (This docs site does that under `:root` and `.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`. 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. Each fix is below, with a link to the guide page that explains it in full. ## 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 comes from `styles.css`: color, stroke weight, label size, the whole theme. Import it 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, with no styling applied. See [Quickstart](/docs/quickstart#compatibility) for where the import goes 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 render transparent SVG. On a plain light or dark page you never notice, because the built-in light and 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. The tokens don't know that scope looks dark; they only know what the OS or 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 <div data-mc-theme="dark"> <Sparkline data={data} /> </div> ``` 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 if you don't 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: ```tsx // ❌ removed <Sparkline data={data} onPointFocus={(i) => setHovered(i)} /> // ✅ one name, everywhere <Sparkline data={data} onActive={(d) => setHovered(d?.index ?? null)} /> ``` The payload is a `MicroDatum`, `{ index, value, label?, formatted? }`, where `value` is the raw number and `formatted` is the chart's ready-to-display string; it's `null` when the chart clears. `onPointFocus` on Sparkline and SparkBar and StreakSpark's `onRunFocus` map across directly. `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. For 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 this to the console when the engine is missing. Three other reasons `animate` is 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, where an entrance would animate content the reader can already see. The entrance 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 `<svg>`, is `display: inline-block` by default, so it flows next to text instead of stretching. Give it fluid sizing explicitly: ```tsx <Sparkline data={data} style={{ width: "100%", height: "auto" }} /> ``` 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. Interactive entries behave identically: `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, so 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 overrides it, dropping the chart onto its own block-level line. Re-assert the layout you want at the call site: ```tsx <Sparkline data={data} className="inline-block" /> ``` 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 renders SVG. Almost every difference below follows from that one difference, so this page is organized around the renderer rather than a feature list. A dense analyst dashboard or a report full of chart panels is Chart.js territory. The comparison here covers one narrow job: a chart the size of a word, inside a sentence, a table cell, or a KPI card. Broader positioning lives in [When to use microcharts](/docs/when-to-use) and [Full chart libraries](/docs/full-chart-libraries). ## The numbers [#the-numbers] <VsChartJsTable /> 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 dated orientation rather than live measurements. ## What the canvas decides [#what-the-canvas-decides] **There is no markup.** A canvas only exists in the browser, so there is no static-HTML output path and the chart appears after JavaScript runs. microcharts renders SVG, so the mark is real DOM: it exists in the server-rendered HTML, prints correctly, and scales crisply at any zoom. **It hydrates.** `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, and an RSC can render it with zero client JavaScript. **It 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 manual, once per chart. Every microchart defaults to `role="img"` plus a natural-language summary generated from the data. **It repaints through JavaScript.** Inline marks sit next to your text, so they inherit its rhythm. SVG takes CSS custom properties, `currentColor`, and container-relative sizing directly. A canvas has to redraw when the theme flips. ## When Canvas is the right renderer [#when-canvas-is-the-right-renderer] * Dashboard or report canvases with many series and heavy point counts. Above a few thousand points, canvas usually wins * You already run Chart.js configs or depend on its plugin ecosystem * Canvas rendering is a requirement, not an implementation detail ## When the mark is inline [#when-the-mark-is-inline] * It lives in prose, cells, tabs, KPI cards, or 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: <SizeMarketing /> gzip, zero runtime dependencies ## 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 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. This comparison turns on one question: whether your app already ships MUI. Answer it first, because it changes which library is the cheap one. The question bites hardest where 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] <VsMuiXTable /> 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. MUI X Charts (community) is MIT and actively maintained, and these sizes are dated pins from the published package. ## If your app is already on MUI [#if-your-app-is-already-on-mui] `SparkLineChart` is the coherent choice, and this page may talk you out of nothing. You get one vendor for the sparkline and the full charts beside it, the same theme object driving both, and the MUI X tooltip, highlighting, and animation stack you already use elsewhere. The shared charting core (data providers, interaction plugins, vendored d3) is what makes that family consistent, and it is why one sparkline carries tens of kilobytes. Two defaults are worth checking even there: * `SparkLineChart` is marked `'use client'`. The server can emit initial HTML, but the component always hydrates and ships its JavaScript. * As published, the `SparkLineChart` SVG renders with `aria-hidden="true"` and no accessible name, so the chart is invisible to assistive technology until you label it yourself. ## If it is not on MUI [#if-it-is-not-on-mui] The mark then has to justify pulling in MUI, and microcharts is the cheaper answer on four counts: * **Bundle.** microcharts charges per chart subpath: <ChartSize slug="sparkline" /> for Sparkline against a catalog band of <SizeMarketing /> gzip, with zero runtime dependencies and no required peers beyond React. * **Server Components.** The default export is hook-free static SVG with zero client JS; interactivity is a separate `/interactive` import. * **Accessibility.** Every microchart defaults to `role="img"` with a natural-language summary generated from the data, so labeling is not a task per chart. * **Coverage.** <CatalogTotal /> word-sized chart types under one grammar, when you need more shapes than a sparkline. Theming crosses both cases. MUI X charts theme through the MUI theme object, which is 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. ## 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. The package earned its adoption; what follows is what has changed since it was written. ## The numbers [#the-numbers] <VsReactSparklinesTable /> ## The same chart in both [#the-same-chart-in-both] The shapes map directly: ```tsx // react-sparklines import { Sparklines, SparklinesLine } from "react-sparklines"; <Sparklines data={[3, 5, 4, 8, 6, 9]} width={100} height={20}> <SparklinesLine color="steelblue" /> </Sparklines>; // microcharts import { Sparkline } from "@microcharts/react/sparkline"; <Sparkline data={[3, 5, 4, 8, 6, 9]} width={100} height={20} color="steelblue" title="Weekly revenue" />; ``` `title` gives the chart its accessible subject; the summary is generated from the data. Bars map to [SparkBar](/docs/charts/sparkbar), reference lines to [`<Threshold>`](/docs/annotations). ## What changed since 2017 [#what-changed-since-2017] **Maintenance.** `react-sparklines` last published in 2017 (`1.7.0`). It predates hooks and Server Components, so 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 `<svg>`: no `role`, no `<title>`, no accessible name, so 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 and size [#scope-and-size] microcharts is a catalog of <CatalogTotal /> word-sized chart types under 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. Both are small. `react-sparklines` is a single \~7.9 kB gzip package; microcharts charges per chart subpath (<ChartSize slug="sparkline" /> 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 There is no urgency to migrate in those cases. The gaps above matter when accessibility, RSC, or maintenance start to cost you something. ## 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 does not replace it. If your page *is* the chart, with axes, a legend, a tooltip, and a brush, use Recharts. This page compares the two for 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). What follows is the measured detail for the Recharts decision. ## The numbers [#the-numbers] <VsRechartsTable /> Both microcharts columns come from `.size-limit.json`, the CI gate that fails the build when a chart grows past its budget. The Recharts figures are pinned with version, method, and date. They are dated orientation rather than live measurements, and they will age. ## What differs [#what-differs] **Bundle cost per mark.** A tree-shaken Recharts `LineChart` carries the shared Recharts core. That cost is fine amortized over a dashboard, and 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, with 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, and it is sized correctly for that job. microcharts hard-codes them away because word-sized marks cannot afford them. The two libraries 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: <SizeMarketing /> gzip, zero runtime dependencies ## Same app, both libraries [#same-app-both-libraries] Most apps that need both end up here. Recharts on the analytics page, microcharts in the table that links to it: ```tsx // analytics page — Recharts <ResponsiveContainer width="100%" height={240}> <LineChart data={rows}>…</LineChart> </ResponsiveContainer>; // the table that links to it — microcharts import { Sparkline } from "@microcharts/react/sparkline"; <Sparkline data={row.series} width={72} height={16} dots="none" summary={false} />; ``` ## 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 makes the opposite bet: finished, opinionated word-sized marks with the design decisions already made. So this is a fork in the road rather than a competition. Both keep bundles lean through per-package (visx) or per-chart (microcharts) imports. What you are choosing is who does the chart design, you or the library. The sections below list what each choice makes you responsible for. ## The numbers [#the-numbers] <VsVisxTable /> The visx figure is a floor: `LinePath` plus one scale, before curves, tooltips, or axes. It is a dated pin for that one minimal composition, not a benchmark of visx overall, and 16 kB is a reasonable floor for hand-built charts. Each `@visx/*` package you add brings its own dependencies (d3 arrives vendored through `@visx/vendor`). ## Three things you own with visx [#three-things-you-own-with-visx] **The design.** You decide geometry, padding, label placement, empty-data behavior, negative-value handling, and color semantics per chart. That is 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. **Accessibility.** A visx `LinePath` renders a bare `<path>`, so `role`, `<title>`, 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. **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. ## Dependencies [#dependencies] visx keeps React in control of the DOM and slices d3 into per-package imports, which is genuinely lighter than shipping all of d3, and its modularity is well executed. microcharts goes further for its narrower job: scales, paths, easing, color, and summaries are in-house, so `dependencies: {}` is a CI-enforced invariant. ## Build it yourself with visx [#build-it-yourself-with-visx] A bespoke visualization deserves primitives, so reach for visx when: * The visualization is bespoke and 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 ## Take the catalog with microcharts [#take-the-catalog-with-microcharts] * The chart you need already exists: <CatalogTotal /> 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 <SizeMarketing /> gzip per chart ## 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 draws tiny charts for React that sit *inside* an interface: a trend after a number, a bullet in a KPI, a strip in a table cell, a mark in a streamed reply. 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 mostly words and UI with a word-sized mark in them. Use a full library when the page *is* the chart. Use microcharts when the chart sits inside something you were already reading. ## 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 | ## Same app, two jobs [#same-app-two-jobs] Most products need both rows of that table. Use Recharts on the analytics page and a Sparkline in the table that links to it — different import, different budget: ```tsx // dashboard panel — your full chart library <ResponsiveContainer>…</ResponsiveContainer>; // same product, table cell — microcharts import { Sparkline } from "@microcharts/react/sparkline"; <Sparkline data={row.series} width={72} height={16} dots="none" summary={false} />; ``` ## Size [#size] Every chart is imported from its own subpath and budget-gated in CI: <SizeMarketing /> gzip, **zero runtime dependencies**, static SVG from an RSC with nothing to hydrate. Each one is `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). ## What the catalog leaves out [#what-the-catalog-leaves-out] Pie, needle gauges, waffles, and similar shapes fail at word size, so they're excluded on purpose. Each has an in-catalog replacement — for example [SegmentedBar](/docs/charts/segmented-bar), [Bullet](/docs/charts/bullet), and [IconArray](/docs/charts/icon-array). The reasoning is in [Design notes](/docs/design-notes#what-we-dont-ship). ## 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 takes two samples of raw values, a control arm and a test arm, and 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. Whether the two middle halves still overlap is the read: while they do, the gap between the medians isn't a result yet. The overlap number is always in the summary, because a delta without its spread doesn't say whether the two arms differ. The strips are mandatory context: there is no bare-mean-bar mode, and the delta label never renders without the distributions behind it. ```tsx <ABStrips data="{ a: Array.from({ length: 80 }, (_, i) => 130 + ((i * 13) % 44) - 22), b: Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" positive="down" title="Latency A/B" width="260" height="26" /> ``` ## 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); <ABStrips data={{ a: control, b: test }} positive="down" title="Latency A/B" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ABStrips } from "@microcharts/react/ab-strips/interactive"; <ABStrips data={{ a: control, b: test }} positive="down" /> ``` ## When to use it [#when-to-use-it] Use it for an A/B experiment result in a KPI card, control vs test in an experiments table, and any two-sample comparison where the spread matters. For a single distribution use BenchmarkStrip; for more than two arms use small multiples. ## Sizing [#sizing] **labelled arms** ```tsx <ABStrips data={{ a: control, b: test }} seriesLabels={["Control", "Test"]} /> ``` **clearly separated** ```tsx <ABStrips data={{ a: slow, b: fast }} /> ``` ## Variants [#variants] ```tsx 130 + ((i * 13) % 44) - 22); const test = Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22); <ABStrips data={{ a: control, b: test }} seriesLabels={["Control", "Test"]} />`" > <span className="inline-flex flex-col gap-2"> <ABStrips data="{ a: Array.from({ length: 80 }, (_, i) => 130 + ((i * 13) % 44) - 22), b: Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" seriesLabels="["Ctrl", "Test"]" title="Labelled" width="240" height="24" /> <ABStrips data="{ a: Array.from({ length: 80 }, (_, i) => 170 + ((i * 13) % 44) - 22), b: Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" positive="down" title="Clearly separated" width="240" height="24" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <ABStrips data="{ a: [], b: [110, 120, 130] }" title="No data" width="220" height="22" /> ``` ```tsx `" > <ABStrips data="{ a: [100, 130, 145], b: Array.from({ length: 80 }, (_, i) => 116 + ((i * 13) % 44) - 22), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="Small A arm" width="220" height="26" /> ``` Either arm with zero finite values makes the comparison unanswerable: no strips draw, and the accessible name reads **"No data."** An arm with fewer than 8 points still draws. Only its outer band changes meaning, from the 5th–95th percentile to the plain min–max, because tail percentiles need more than a handful of points to estimate. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Latency A/B this week{" "} <span className="mc-inline"> <ABStrips data={{ a, b }} summary={false} /> </span>{" "} — variant B wins by 12 ms on median. </p> ``` **In a table cell** ```tsx <td> <ABStrips data={{ a, b }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">−12 ms</span> <span className="unit">B vs A median</span> <ABStrips data={{ a, b }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <ABStrips data={{ a, b }} /> </button> ``` ## 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%."**. When the middle halves fully overlap the summary says "no clear difference"; when they are disjoint it says "clearly separated". 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) Each cell is one slot in the series, shaded by intensity: the contribution-graph shape, read for cadence, streaks, and seasonality. There are five levels — a faint empty track plus four intensity steps — and the four steps share the exact opacity ramp HeatCell and HeatStrip use, so "how intense" means the same thing across the library. Level 0 is the empty track, because a slot with no activity is empty rather than a bottom-of-scale value. The steps are discrete bins, never a continuous gradient: a continuous ramp implies precision a handful of pixels can't deliver. ```tsx `" > <ActivityGrid data="[0, 1, 2, 1, 3, 4, 2, 0, 1, 3, 2, 4, 3, 1, 0, 2, 4, 3, 2, 1, 3, 0, 2, 3, 4, 1, 2, 0]" cell="11" title="Commit activity" /> ``` ## Install [#install] ```tsx import { ActivityGrid } from "@microcharts/react/activity-grid"; <ActivityGrid data={commits} title="Commits" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ActivityGrid } from "@microcharts/react/activity-grid/interactive"; <ActivityGrid data={/* 35 values */} layout="grid" cell={12} /> ``` ## When to use it [#when-to-use-it] Use it for daily activity, streaks and cadence, and seasonality. Avoid it for exact values or for a precise comparison between two individual cells: color intensity is deliberately approximate, so 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. **default cells** ```tsx // ActivityGrid sizes from cell edge length (default 10) <ActivityGrid data={commits} /> ``` **larger cells** ```tsx // bump every cell — the whole grid scales with it <ActivityGrid data={commits} cell={14} /> ``` **responsive** ```tsx // let CSS drive the width — the viewBox keeps the grid's ratio <div style={{ width: "100%", maxWidth: 320 }}> <ActivityGrid data={commits} style={{ width: "100%", height: "auto" }} /> </div> ``` ## Variants [#variants] `layout="strip"` collapses to a single row for inline use. ```tsx <ActivityGrid data={commits.slice(0, 12)} layout="strip" />`" > <span className="inline-flex flex-col gap-3"> <ActivityGrid data="[0, 1, 2, 1, 3, 4, 2, 0, 1, 3, 2, 4, 3, 1, 0, 2, 4, 3, 2, 1]" cell="12" title="Grid" /> <ActivityGrid data="[0, 1, 2, 1, 3, 4, 2, 0, 1, 3, 2, 4]" layout="strip" cell="12" title="Strip" /> </span> ``` `shape` swaps the cell mark without changing the data or the levels. `"round"` softens the grid for product surfaces; `"dot"` adds space between cells in dense strips. ```tsx <ActivityGrid data={commits.slice(0, 12)} layout="strip" shape="dot" />`" > <span className="inline-flex flex-col gap-3"> <ActivityGrid data="[0, 1, 2, 1, 3, 4, 2, 0, 1, 3, 2, 4, 3, 1, 0, 2, 4, 3, 2, 1]" shape="round" cell="12" title="Round cells" /> <ActivityGrid data="[0, 1, 2, 1, 3, 4, 2, 0, 1, 3, 2, 4]" layout="strip" shape="dot" cell="12" title="Dot strip" /> </span> ``` `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. Weeks start on Monday by default (`weekStart={1}`, matching the ISO calendar); `weekStart={0}` switches to Sunday without touching the data. Cells fill column-major — each column is a week, top to bottom — which is what makes the grid read as a calendar rather than an arbitrary sequence. ```tsx `" > <ActivityGrid data="[2, 3, 1, 4, 2, 0, 1, 3, 2, 4, 3, 1, 0, 2, 4, 3, 2, 1]" anchor="1970-01-01" cell="12" title="Week-aligned" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <ActivityGrid data="[]" title="No history" cell="12" /> ``` ```tsx `"> <ActivityGrid data="[0, 0, 0, 0, 0, 0, 0]" title="Quiet week" cell="12" /> ``` ```tsx `"> <ActivityGrid data="[3]" title="One day" cell="12" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Commit cadence for the last two weeks{" "} <span className="mc-inline"> <ActivityGrid data={commits.slice(0, 14)} layout="strip" cell={7} summary={false} /> </span>{" "} — bursts of shipping, punctuated by two dead-quiet days. </p> ``` **In a table cell** ```tsx <td> <ActivityGrid data={commits.slice(0, 7)} layout="strip" cell={8} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">67</span> <span className="unit">across 35 days, busiest at 4</span> <ActivityGrid data={commits} cell={8} /> </div> ``` **In a tab header** ```tsx <button className="tab"> api <ActivityGrid data={commits.slice(-14)} layout="strip" cell={5} /> </button> ``` ## Accessibility [#accessibility] Intensity is a color channel, so ActivityGrid always pairs it with a numeric summary of its total and peak. That summary survives forced-colors and color-blind viewing. The interactive entry adds 2-D arrow-key navigation, announcing each cell's value as you move. Give it an `anchor` and every cell is a dated day, so the readout names the date rather than a position ("Mar 2: 12"). 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. | | `steps` | `number` | Intensity steps including the zero track (default 5, GitHub-like). Same knob, same name, as CalendarStrip, CoverageStrip, GardenGrid, HeatCell, HeatStrip and SpiralYear. | | `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. | | `gap` | `number` | Gap between cells in viewBox units (default 2). | | `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 takes two values and tilts a beam toward the heavier one, so which side wins reads instantly. Inflow is 620, outflow is 480, and the beam tilts toward inflow. The angle grows with the imbalance, but it **saturates** at `maxTilt` degrees: past that point a steeper beam would imply a precision the eye can't extract, so read the tilt as direction plus rough magnitude. The two weights are area-true — their area, not their width, is proportional to value. For a precise comparison, use `PairedBars` or `Delta`. ```tsx <BalanceBeam data="[ { label: "Inflow", value: 620 }, { label: "Outflow", value: 480 }, ]" label="values" title="Cash flow" width="80" height="30" /> ``` ## Install [#install] ```tsx import { BalanceBeam } from "@microcharts/react/balance-beam"; <BalanceBeam data={[{ label: "Inflow", value: 620 }, { label: "Outflow", value: 480 }]} /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BalanceBeam } from "@microcharts/react/balance-beam/interactive"; <BalanceBeam data={[{ label: "Inflow", value: 620 }, { label: "Outflow", value: 480 }]} /> ``` ## When to use it [#when-to-use-it] Use it 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 point. For exact ratios use PairedBars or Delta, for more than two items MiniBar, and for trends a time-series chart. ## Sizing [#sizing] **round weights + values** ```tsx <BalanceBeam data={pair} shape="round" label="values" /> ``` **balanced reads level** ```tsx <BalanceBeam data={[{ label: "A", value: 500 }, { label: "B", value: 500 }]} /> ``` ## Variants [#variants] `mode="ratio"` is the default, because most two-sided comparisons are share-of-whole questions. `mode="difference"` scales the imbalance by a shared `domain`, so rows on the same scale tilt comparably. Square weights are the default because circles under-read area at this size; `shape="round"` swaps them. ```tsx <BalanceBeam data={[{ label: "A", value: 500 }, { label: "B", value: 500 }]} />`" > <span className="inline-flex gap-6 items-center"> <BalanceBeam data="[ { label: "Inflow", value: 620 }, { label: "Outflow", value: 480 }, ]" shape="round" label="values" width="90" height="34" /> <BalanceBeam data="[ { label: "A", value: 500 }, { label: "B", value: 500 }, ]" width="70" height="28" /> </span> ``` `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 `" > <BalanceBeam data="[ { label: "Inflow", value: 62000 }, { label: "Outflow", value: 48000 }, ]" label="values" format="{ maximumFractionDigits: 0 }" locale="de-DE" width="90" height="34" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <BalanceBeam data="[ { label: "A", value: 500 }, { label: "B", value: 500 }, ]" width="70" height="28" /> ``` ```tsx `"> <BalanceBeam data="[ { label: "A", value: 0 }, { label: "B", value: 0 }, ]" width="70" height="28" /> ``` 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 zero area. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Cash flow this month{" "} <span className="mc-inline"> <BalanceBeam data={[{ label: "A", value: 620 }, { label: "B", value: 480 }]} summary={false} /> </span>{" "} — inflow outweighs outflow, beam tilts right. </p> ``` **In a table cell** ```tsx <td> <BalanceBeam data={[{ label: "A", value: 620 }, { label: "B", value: 480 }]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+140</span> <span className="unit">net inflow</span> <BalanceBeam data={[{ label: "A", value: 620 }, { label: "B", value: 480 }]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Operating <BalanceBeam data={[{ label: "A", value: 620 }, { label: "B", value: 480 }]} /> </button> ``` ## Accessibility [#accessibility] The accessible name states both sides and the winner — **"Inflow 620 vs Outflow 480; Inflow heavier."** — or "…; balanced." when they match. The two channels always agree: the tilt direction and the larger weight area point at the same side. 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. | | `fontSize` | `number` | Type size of the weight numerals, in viewBox units. Defaults from `height`. | | `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 reads one value against a list of peer values: a focal dot on a common scale, placed against the peers' own **empirical** quantile bands, so you can tell whether the value is ordinary for that cohort or out at the tail. The bands come from the measured peers and never from a fitted distribution, so the strip never implies a normal curve the data doesn't back up. There is no axis: the band is the reference frame, and the dot's position in it is the whole read. ```tsx `" > <BenchmarkStrip data="[ 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, ]" value="312" title="Latency vs peers" width="240" height="16" /> ``` ## Install [#install] ```tsx import { BenchmarkStrip } from "@microcharts/react/benchmark-strip"; <BenchmarkStrip data={peerLatencies} value={312} format={{ style: "unit", unit: "millisecond" }} title="Latency vs peers" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BenchmarkStrip } from "@microcharts/react/benchmark-strip/interactive"; <BenchmarkStrip data={peerLatencies} value={312} /> ``` ## When to use it [#when-to-use-it] Use it for a value against its cohort, per-row peer comparison in tables, and SLA context. For a single trend use Sparkline; for two groups head-to-head use ABStrips. ## Sizing [#sizing] **small samples stay honest** ```tsx // n < 8 falls back to min–max — tail quantiles would be fiction <BenchmarkStrip data={[210, 260, 300, 340, 410]} value={300} /> ``` **polarity colors the dot** ```tsx // latency: lower is better → below the median reads positive <BenchmarkStrip data={peerLatencies} value={230} positive="down" /> ``` ## 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, ]; <BenchmarkStrip data={peerLatencies} value={312} label="value" />`" > <span className="inline-flex flex-col gap-1.5"> <BenchmarkStrip data="[210, 260, 300, 340, 410]" value="300" range="minmax" title="Min–max" width="200" height="14" /> <BenchmarkStrip data="[ 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, ]" value="312" label="value" title="Value label" width="200" height="14" /> </span> ``` ```tsx `" > <BenchmarkStrip data="[1080, 1180, 1220, 1360, 1440]" value="1240" label="value" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Localized value" width="200" height="16" /> ``` `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 `" > <BenchmarkStrip data="[210, 260, 300, 340, 410]" value="300" title="5 peers" width="200" height="16" /> ``` ```tsx `"> <BenchmarkStrip data="[50, 50, 50, 50, 50, 50, 50, 50]" value="52" title="Flat cohort" width="200" height="16" /> ``` ```tsx `" > <BenchmarkStrip data="[180, 250, 300, 350, 420]" value="520" domain="[150, 400]" title="Clamped" width="200" height="16" /> ``` Below 8 peers, tail quantiles (p5/p95) are statistically unreliable, so the outer band falls back to the observed min–max automatically; `range` forces either band at any sample size. A value outside the plotted `domain` clamps to the edge and draws a small directional wedge instead of disappearing. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Latency vs peers{" "} <span className="mc-inline"> <BenchmarkStrip data={peerLatencies} value={value} summary={false} /> </span>{" "} — slower than 78% of the cohort. </p> ``` **In a table cell** ```tsx <td> <BenchmarkStrip data={peerLatencies} value={value} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">312</span> <span className="unit">ms · 78th pctile</span> <BenchmarkStrip data={peerLatencies} value={value} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <BenchmarkStrip data={peerLatencies} value={value} /> </button> ``` ## 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) A new device tracks its reference closely and still reads about 2 units high. BiasStrip plots that offset: 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. The y-axis is symmetric about zero, so a systematic offset reads as the whole cloud sitting above or below the line. 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 <span className="text-lg"> {"Device and reference "} <BiasStrip data="Array.from({ length: 20 }, (_, i) => ({ a: i + [1.8, 2.4, 1.5, 2.9, 2.1, 1.2, 2.6, 3.0, 1.9, 2.3, 6.5, 2.0, 1.7, 2.8, 2.2, -1.5, 2.5, 1.6, 2.7, 2.0][i], b: i, }))" title="Device vs reference" style="{ width: "3.4em", height: "1.9em" }" /> {" agree, +2 bias."} </span> ``` ## Install [#install] ```tsx import { BiasStrip } from "@microcharts/react/bias-strip"; <BiasStrip data={pairs} title="Device vs reference" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BiasStrip } from "@microcharts/react/bias-strip/interactive"; <BiasStrip data={pairs} /> ``` ## When to use it [#when-to-use-it] Use it to check whether a new instrument, method, or model agrees with a reference, and for instrument drift in a KPI card. For unpaired samples use MicroScatter; for a single time series use Sparkline. Two methods can correlate perfectly and still disagree by a constant offset. BiasStrip plots the difference, so that offset shows up as distance from the zero line where a correlation plot would hide it along the diagonal. ## Sizing [#sizing] **in a sentence** ```tsx device and reference <BiasStrip data={pairs} style={{ width: "3em", height: "1.6em" }} /> agree, +2 bias ``` **99% limits of agreement** ```tsx <BiasStrip data={pairs} limits={2.58} /> ``` ## Variants [#variants] The band is the classic ±1.96σ limits of agreement. `limits` widens or tightens it (2.58 for about 99%) without changing what any mark means. ```tsx `" > <BiasStrip data="Array.from({ length: 20 }, (_, i) => ({ a: i + [1.8, 2.4, 1.5, 2.9, 2.1, 1.2, 2.6, 3.0, 1.9, 2.3, 6.5, 2.0, 1.7, 2.8, 2.2, -1.5, 2.5, 1.6, 2.7, 2.0][i], b: i, }))" limits="2.58" title="99% limits" style="{ width: 150, height: 84 }" /> ``` ```tsx `"> <BiasStrip data="Array.from({ length: 20 }, (_, i) => ({ a: i + [1.8, 2.4, 1.5, 2.9, 2.1, 1.2, 2.6, 3.0, 1.9, 2.3, 6.5, 2.0, 1.7, 2.8, 2.2, -1.5, 2.5, 1.6, 2.7, 2.0][i], b: i, }))" label="none" title="No caption" style="{ width: 150, height: 84 }" /> ``` ```tsx `"> <BiasStrip data="Array.from({ length: 20 }, (_, i) => ({ a: i + [1.8, 2.4, 1.5, 2.9, 2.1, 1.2, 2.6, 3.0, 1.9, 2.3, 6.5, 2.0, 1.7, 2.8, 2.2, -1.5, 2.5, 1.6, 2.7, 2.0][i], b: i, }))" r="2.5" title="Bigger dots" style="{ width: 150, height: 84 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <BiasStrip data="[]" title="No pairs" style="{ width: 120, height: 64 }" /> ``` ```tsx `"> <BiasStrip data="[ { a: 2, b: 0 }, { a: 3, b: 1 }, { a: 4, b: 2 }, { a: 5, b: 3 }, ]" title="Four pairs" style="{ width: 120, height: 64 }" /> ``` ```tsx `"> <BiasStrip data="[ { a: 20, b: 20 }, { a: 21, b: 21 }, { a: 22, b: 22 }, { a: 23, b: 23 }, { a: 24, b: 24 }, { a: 25, b: 25 }, ]" title="Perfect agreement" style="{ width: 120, height: 64 }" /> ``` 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 chart makes no interval claim at that sample size, and 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 the measured result stays visible. 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Device vs reference{" "} <span className="mc-inline"> <BiasStrip data={pairs} width={90} height={24} summary={false} /> </span>{" "} — +2 bias, 18 of 20 within limits. </p> ``` **In a table cell** ```tsx <td> <BiasStrip data={pairs} width={72} height={28} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+2.0</span> <BiasStrip data={pairs} limits={2.58} width={200} height={48} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Sensor A <BiasStrip data={pairs} label="none" width={44} height={22} /> </button> ``` ## 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 takes a single level between 0 and 1 and shows how strained a system is right now, as an ambient signal you can leave running in a corner of the interface. A core dot colored by band, and a ring whose distance from the core is the level: that is the static frame, and it renders with zero JavaScript. The interactive entry adds the pulse: slow and small when calm, fast and large when strained, so the state is legible without reading a number. This is a low-precision ambient read; for an exact figure use `Progress` or `Sparkline`. ```tsx <BreathingDot value="0.42" title="Load" size="40" /> ``` ## Install [#install] ```tsx import { BreathingDot } from "@microcharts/react/breathing-dot"; <BreathingDot value={0.42} title="Load" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BreathingDot } from "@microcharts/react/breathing-dot/interactive"; <BreathingDot value={0.42} /> ``` ## Motion, and reduced motion [#motion-and-reduced-motion] The pulse **is** the encoding: its rate and amplitude snap to three named bands (calm, elevated, strained). Three, because ambient motion supports about three discriminable states, and more would be false precision. This is the deliberate exception to the no-looping-animation rule, allowed because the loop parameter (the rate) is the datum. The animation is gated twice. 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] Use it 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. For an exact load figure use `Progress` or `Sparkline`; for a trend over time, `Sparkline`. The boundary with `HeartbeatBlip` is the data shape: a continuous level is a BreathingDot, discrete arriving events are a HeartbeatBlip. ## 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 <BreathingDot value={0.42} label="value" />`" > <span className="inline-flex gap-6 items-center"> <BreathingDot value="0.2" size="28" /> <BreathingDot value="0.65" size="28" /> <BreathingDot value="0.92" label="value" size="28" /> <BreathingDot value="null" size="28" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <BreathingDot value="null" size="28" /> ``` ```tsx `"> <BreathingDot value="1.4" label="value" size="28" /> ``` ```tsx `"> <BreathingDot value="Number.NaN" size="28" /> ``` ```tsx `"> <BreathingDot value="0.723" label="value" locale="de-DE" size="28" /> ``` An unknown value (`null` or `NaN`) turns the dot gray, drops its ring, and **never pulses**, so an unknown system never renders as a calm one. `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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> System load right now{" "} <span className="mc-inline"> <BreathingDot value={0.42} summary={false} /> </span>{" "} — moderate strain, pulsing at 42%. </p> ``` **In a table cell** ```tsx <td> <BreathingDot value={0.42} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">42%</span> <span className="unit">current strain</span> <BreathingDot value={0.42} /> </div> ``` **In a tab header** ```tsx <button className="tab"> api <BreathingDot value={0.42} /> </button> ``` ## 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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. | | `size` | `number` | Dot box edge in viewBox units (default 16). | | `fontSize` | `number` | Type size of the percent numeral, in viewBox units. Defaults from `size`. | 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 sizes up a handful of magnitudes against each other: one circle per item, with **area** (not width) proportional to value. That is `r ∝ √value`, with no exceptions, since a linear-radius map would overstate every difference by roughly its square. Area is the weakest common channel, so this is the catalog's low-precision exemplar: **for a precise comparison, use `MiniBar`.** The value numerals are on by default, because a bubble twice the diameter is four times the area and the eye can't reliably invert that. The LOW precision rating and the MiniBar steer are printed in the catalog, in `/catalog.json`, and in this page's header. ```tsx <BubbleRow data="[ { label: "EMEA", value: 1240 }, { label: "AMER", value: 890 }, { label: "APAC", value: 560 }, { label: "LATAM", value: 210 }, ]" title="Market size" height="36" /> ``` ## Install [#install] ```tsx import { BubbleRow } from "@microcharts/react/bubble-row"; <BubbleRow data={regions} title="Market size" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BubbleRow } from "@microcharts/react/bubble-row/interactive"; <BubbleRow data={regions} /> ``` ## When to use it [#when-to-use-it] Use it for a few magnitudes with physical presence in a sentence, a market-size or segment impression in a KPI card, or an editorial callout that prints the number too. For precise comparison use `MiniBar`; for trends, Sparkline. Keep it under about eight items. ## Sizing [#sizing] **the honesty comparison — same data as MiniBar** ```tsx <BubbleRow data={regions} /> // area: low precision // use MiniBar for a precise read ``` **baseline align — weights on a shelf** ```tsx <BubbleRow data={regions} align="baseline" /> ``` ## Variants [#variants] There is no sorting prop. The row renders in your data's order, so sort the data yourself when you want a ranked row. ```tsx <BubbleRow data={regions} label="both" />`" > <span className="inline-flex flex-col gap-4"> <BubbleRow data="[ { label: "EMEA", value: 1240 }, { label: "AMER", value: 890 }, { label: "APAC", value: 560 }, { label: "LATAM", value: 210 }, ]" align="baseline" height="36" /> <BubbleRow data="[ { label: "EMEA", value: 1240 }, { label: "AMER", value: 890 }, { label: "APAC", value: 560 }, { label: "LATAM", value: 210 }, ]" label="both" height="36" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <BubbleRow data="[]" title="No regions" height="30" /> ``` ```tsx `"> <BubbleRow data="[ { label: "EMEA", value: 1240 }, { label: "AMER", value: null }, ]" title="Missing AMER figure" height="30" /> ``` Empty data draws just the frame with "No data." as the summary. A `null` value keeps its slot with a small presence ring and drops its numeral: it is neither removed from the row nor drawn as a zero-radius circle. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Market size by region{" "} <span className="mc-inline"> <BubbleRow data={regions} summary={false} /> </span>{" "} — EMEA leads at 1.24B. </p> ``` **In a table cell** ```tsx <td> <BubbleRow data={regions} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1.24B</span> <span className="unit">market size</span> <BubbleRow data={regions} /> </div> ``` **In a tab header** ```tsx <button className="tab"> EMEA <BubbleRow data={regions} /> </button> ``` ## 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. | | `gap` | `number` | Gap between bubbles in viewBox units (default 2). | | `fontSize` | `number` | Type size of the value numerals, in viewBox units. Defaults from `height`. | | `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) Quota attainment is 72 against a target of 80. Bullet puts the measure, the target tick, and the qualitative bands (poor / okay / good) on one row, for progress, SLAs, budgets, and quotas. The bands sit lowest and graduate in shade by step, because they are context and the measure bar has to read as the loudest mark on the row. The measure 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. ```tsx <Bullet value="72" target="80" bands="[50, 90]" width="200" height="22" title="Quota attainment" /> ``` ## Install [#install] ```tsx import { Bullet } from "@microcharts/react/bullet"; <Bullet value={72} target={80} bands={[50, 90]} title="Quota" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Bullet } from "@microcharts/react/bullet/interactive"; <Bullet value={72} target={80} bands={[50, 90]} /> ``` ## When to use it [#when-to-use-it] Use it for progress to a goal, a value against a target, and KPIs with thresholds. For trends over time use [Sparkline](/docs/charts/sparkline); it is not a distribution chart. ## 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. **default** ```tsx // data alone → an intrinsic 80×16 box <Bullet value={72} target={80} bands={[50, 90]} /> ``` **fixed size** ```tsx // a bullet reads best wide and short <Bullet value={72} target={80} bands={[50, 90]} width={240} height={24} /> ``` **responsive** ```tsx // fills a table cell or card column, aspect ratio preserved <div style={{ width: "100%", maxWidth: 320 }}> <Bullet value={72} target={80} bands={[50, 90]} style={{ width: "100%", height: "auto" }} /> </div> ``` ## Variants [#variants] ```tsx `"> <Bullet value="72" target="80" bands="[50, 90]" width="220" height="22" title="Behind target" /> ``` ```tsx `"> <Bullet value="94" target="80" bands="[50, 90]" width="220" height="22" title="Ahead of target" /> ``` ```tsx `"> <Bullet value="38" target="80" width="220" height="22" title="Measure and target only" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <Bullet value="120" target="80" domain="[0, 100]" width="220" height="22" title="Over the domain" /> ``` ```tsx `" > <Bullet value="NaN" target="80" bands="[50, 90]" width="220" height="22" title="No data" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Q3 quota attainment sits at{" "} <span className="mc-inline"> <Bullet value={72} target={80} bands={[50, 90]} height={14} summary={false} /> </span>{" "} — inside the good band, short of target. </p> ``` **In a table cell** ```tsx <td> <Bullet value={92} target={80} bands={[50, 90]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">72 GB</span> <span className="unit">of an 80 GB volume</span> <Bullet value={72} target={80} bands={[50, 90]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> North <Bullet value={72} target={80} bands={[50, 90]} /> </button> ``` ## 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 target is a tick rather than a second bar: a distinct shape at a distinct position, so the comparison survives grayscale print and never depends on two colors staying apart. 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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 plots one entrant's rank over a run of periods on an inverted ordinal scale, so #1 sits on top and climbing draws upward. Ranks are not values: a Sparkline would treat the gap between #2 and #3 as a real distance. End labels ("#5" → "#1") anchor the ordinal read without an axis, and change dots mark only the periods where rank moved, so flat runs stay quiet and the eye lands on the transitions. ```tsx <BumpStrip data="[5, 5, 4, 4, 4, 3, 2, 2, 3, 2, 1, 1]" title="Category rank" width="220" height="26" /> ``` ## Install [#install] ```tsx import { BumpStrip } from "@microcharts/react/bump-strip"; <BumpStrip data={weeklyRanks} title="Category rank" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BumpStrip } from "@microcharts/react/bump-strip/interactive"; <BumpStrip data={weeklyRanks} /> ``` ## When to use it [#when-to-use-it] Use it for leaderboard rows and category-rank trends in KPI cards. For continuous values use Sparkline, and keep it under about 15 rank levels. ## Sizing [#sizing] **leaderboard rows** ```tsx {products.map((p) => ( <BumpStrip key={p.id} data={p.ranks} maxRank={10} title={p.name} /> ))} ``` **gaps for unranked weeks** ```tsx <BumpStrip data={[2, null, null, 3, 1, 1]} /> ``` ## Variants [#variants] ```tsx `" > <BumpStrip data="[2, null, null, 3, 1, 1]" title="With unranked weeks" width="220" height="26" /> ``` ```tsx `"> <BumpStrip data="[8, 7, 7, 6, 4, 4, 3, 3, 2, 2, 2, 1]" maxRank="10" title="Fixed 10-rank band" width="220" height="26" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <BumpStrip data="[3]" maxRank="5" title="One period" width="140" height="26" /> ``` ```tsx `" > <BumpStrip data="[null, null, null]" title="No ranked periods" width="140" height="26" /> ``` ```tsx `" > <BumpStrip data="[3, 3, 3, 3, 3]" title="Steady at #3" width="140" height="26" /> ``` A single ranked period draws its two end labels with no visible line, because there is no trajectory yet. An all-`null` series renders the frame with neither line nor labels, and the accessible name reports that directly. A flat run draws a level line with zero change dots, because `dots="changes"` marks only the periods where rank moved. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Our blend's Coffee bestseller rank{" "} <span className="mc-inline"> <BumpStrip data={weeklyRanks} width={70} height={16} summary={false} /> </span>{" "} — from #5 to #1 in twelve weeks. </p> ``` **In a table cell** ```tsx <td> <BumpStrip data={[3, 3, 4, 4, 3, 3, 2, 3, 4, 4, 5, 5]} label="none" dots="none" width={70} height={16} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">#1</span> <span className="unit">up from #5, 12 weeks ago</span> <BumpStrip data={weeklyRanks} width={90} height={24} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Coffee <BumpStrip data={weeklyRanks} label="none" width={40} height={14} /> </button> ``` ## 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) Six days into an eleven-day sprint, 19 points are done against 20 planned. BurnChart 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 `max(2, ⌈actual/3⌉)` actual points. History is solid and exact; the projection is dotted and muted, and its method is stated rather than smoothed into an optimistic curve. The gap label states the signed schedule landing. ```tsx `" > <BurnChart data="{ plan: [40, 36, 32, 28, 24, 20, 16, 12, 8, 4, 0], actual: [40, 35, 31, 27, 24, 21] }" label="gap" title="Sprint 12" width="260" height="30" /> ``` ## Install [#install] ```tsx import { BurnChart } from "@microcharts/react/burn-chart"; <BurnChart data={{ plan, actual }} title="Sprint 12" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { BurnChart } from "@microcharts/react/burn-chart/interactive"; <BurnChart data={{ plan, actual }} /> ``` ## When to use it [#when-to-use-it] Use it for a sprint burndown in a tab header, will-we-finish in a KPI card, and plan vs actual with a projected landing. For a single progress number use Progress; for a plain series use Sparkline. ## Sizing [#sizing] **plan vs actual only (retrospective)** ```tsx <BurnChart data={{ plan, actual }} projection={false} /> ``` **burn-up toward scope** ```tsx <BurnChart data={{ plan, actual }} mode="up" /> ``` ## Variants [#variants] ```tsx <BurnChart data={{ plan: [0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40], actual: [0, 5, 9, 13, 16, 19] }} mode="up" />`" > <span className="inline-flex flex-col gap-1.5"> <BurnChart data="{ plan: [40, 36, 32, 28, 24, 20, 16, 12, 8, 4, 0], actual: [40, 35, 31, 27, 24, 21] }" projection="false" title="Retrospective" width="220" height="28" /> <BurnChart data="{ plan: [0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40], actual: [0, 5, 9, 13, 16, 19] }" mode="up" title="Burn-up to scope" width="220" height="28" /> </span> ``` ```tsx `" > <BurnChart data="{ plan: [4000, 3200, 2400, 1600, 800, 0], actual: [4000, 3400, 2800, 2300] }" locale="de-DE" title="Localized point counts" width="220" height="28" /> ``` 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 `" > <BurnChart data="{ plan: [], actual: [40, 35, 31, 27, 24, 21] }" title="No plan recorded" width="220" height="28" /> ``` ```tsx `" > <BurnChart data="{ plan: [40, 36, 32, 28, 24, 20], actual: [40, 40, 40, 40] }" title="Stalled" width="220" height="28" /> ``` ```tsx `"> <BurnChart data="{ plan: [40, 36, 32, 28, 24, 20], actual: [40] }" title="Day one" width="220" height="28" /> ``` With no plan (`plan: []`), the projection still runs, since the fitted slope only needs `actual`, but there is no plan to compare against, so the summary drops the "vs planned" clause. A stalled burn (`actual` flat) never reaches zero, so `finishes` is `false` and the summary says so outright ("not finishing at the current pace") rather than extrapolating a landing date. A single `actual` point draws no projection at all: the fit needs at least two. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Sprint 12 burndown{" "} <span className="mc-inline"> <BurnChart data={{ plan, actual }} summary={false} /> </span>{" "} — on track, 18 points left with 4 days to go. </p> ``` **In a table cell** ```tsx <td> <BurnChart data={{ plan, actual }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">18</span> <span className="unit">points remaining</span> <BurnChart data={{ plan, actual }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Sprint 12 <BurnChart data={{ plan, actual }} /> </button> ``` ## 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) Each cell is one dated day, sitting in its real week column: four weeks by default, starting Monday, so one glance covers a month of weekday rhythm without scrolling history. Weekday rhythm is the read. ```tsx <CalendarStrip data="Array.from({ length: 18 }, (_, i) => ({ date: `2026-06-${String(4 + i).padStart(2, "0")}`, value: i % 4 === 3 ? 0 : (i % 7) + 1, }))" end="2026-07-01" title="Deploy cadence" style="{ width: 150, height: 84 }" /> ``` ## Install [#install] ```tsx import { CalendarStrip } from "@microcharts/react/calendar-strip"; <CalendarStrip data={days} end="2026-07-01" title="Deploy cadence" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CalendarStrip } from "@microcharts/react/calendar-strip/interactive"; <CalendarStrip data={days} end="2026-07-01" /> ``` ## When to use it [#when-to-use-it] Use it for habit or deploy cadence in KPI cards and week-aligned recent activity. For long ordinal histories use ActivityGrid; for exact per-day values use MiniBar. ## Sizing [#sizing] **habit rows** ```tsx {habits.map((h) => ( <CalendarStrip key={h.id} data={h.days} end={today} weeks={2} title={h.name} /> ))} ``` **dot cells for dense cards** ```tsx <CalendarStrip data={days} end={today} shape="dot" /> ``` ## Variants [#variants] ```tsx `"> <CalendarStrip data="Array.from({ length: 18 }, (_, i) => ({ date: `2026-06-${String(4 + i).padStart(2, "0")}`, value: i % 4 === 3 ? 0 : (i % 7) + 1, }))" end="2026-07-01" weeks="2" title="Two weeks" style="{ width: 150, height: 42 }" /> ``` ```tsx `"> <CalendarStrip data="Array.from({ length: 18 }, (_, i) => ({ date: `2026-06-${String(4 + i).padStart(2, "0")}`, value: i % 4 === 3 ? 0 : (i % 7) + 1, }))" end="2026-07-01" weekStart="0" title="Sunday start" style="{ width: 150, height: 84 }" /> ``` All date math is UTC, so the same input renders identically in any host timezone and SSR output never depends on where the server runs. `end` defaults to today; pin it wherever determinism matters. `cell`/`gap` bump the edge length and spacing directly — grid-sibling parity with ActivityGrid and GardenGrid. ```tsx `"> <CalendarStrip data="Array.from({ length: 18 }, (_, i) => ({ date: `2026-06-${String(4 + i).padStart(2, "0")}`, value: i % 4 === 3 ? 0 : (i % 7) + 1, }))" end="2026-07-01" cell="10" gap="2" title="Larger cells" style="{ width: 200, height: 112 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <CalendarStrip data="[]" end="2026-07-01" title="No records" style="{ width: 150, height: 84 }" /> ``` ```tsx `"> <CalendarStrip data="Array.from({ length: 14 }, (_, i) => ({ date: new Date(Date.UTC(2026, 5, 18) + i * 86400000).toISOString().slice(0, 10), value: 0, }))" end="2026-07-01" weeks="2" title="Quiet fortnight" style="{ width: 150, height: 42 }" /> ``` Empty is not zero here. A day with no record renders as a faint outline; a day with value 0 renders as a filled track cell, so a gap in the feed never reads as a quiet day. Future days are blank, never extrapolated. ## Four homes [#four-homes] **In a sentence** ```tsx <p> api shipped on 11 of the last 24 tracked days{" "} <span className="mc-inline"> <CalendarStrip data={days} end="2026-07-01" cell={4} summary={false} /> </span>{" "} — quiet on three. </p> ``` **In a table cell** ```tsx <td> <CalendarStrip data={days} end="2026-07-01" weeks={2} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">11</span> <span className="unit">of 24 tracked days</span> <CalendarStrip data={days} end="2026-07-01" /> </div> ``` **In a tab header** ```tsx <button className="tab"> production <CalendarStrip data={days} end="2026-07-01" weeks={2} cell={5} /> </button> ``` ## 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 draws one dot per bin at its predicted probability and its observed frequency, read against the identity diagonal, with a support lane underneath. Bins with too little data render open and faded, so a confident-looking dot is never backed by three samples. The support lane is always on and the count can't be disabled: a reliability read without support disclosure is the failure this chart exists to prevent. There is deliberately no single-number calibration score (ECE) rendered or announced, because one number would hide the per-bin structure. ```tsx <CalibrationStrip data="[ { predicted: 0.05, observed: 0.05, count: 100 }, { predicted: 0.15, observed: 0.16, count: 90 }, { predicted: 0.25, observed: 0.24, count: 80 }, { predicted: 0.35, observed: 0.36, count: 70 }, { predicted: 0.45, observed: 0.44, count: 60 }, { predicted: 0.55, observed: 0.56, count: 50 }, { predicted: 0.65, observed: 0.63, count: 40 }, { predicted: 0.7, observed: 0.52, count: 30 }, { predicted: 0.85, observed: 0.83, count: 8 }, { predicted: 0.95, observed: 0.9, count: 5 }, ]" title="Model calibration" width="300" height="44" /> ``` ## Install [#install] ```tsx import { CalibrationStrip } from "@microcharts/react/calibration-strip"; <CalibrationStrip data={reliability} title="Model calibration" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CalibrationStrip } from "@microcharts/react/calibration-strip/interactive"; <CalibrationStrip data={reliability} /> ``` ## When to use it [#when-to-use-it] Use it for classifier reliability and trust, and for probability-forecast auditing. For a single accuracy number use Delta; for where the errors go use [ConfusionGrid](/docs/charts/confusion-grid). ## Sizing [#sizing] **eval-table cell** ```tsx <CalibrationStrip data={row.reliability} width={80} height={24} /> ``` **deviation bars** ```tsx <CalibrationStrip data={reliability} mode="bars" /> ``` ## Variants [#variants] ```tsx `" > <CalibrationStrip data="[ { predicted: 0.1, observed: 0.08, count: 90 }, { predicted: 0.3, observed: 0.36, count: 70 }, { predicted: 0.5, observed: 0.44, count: 55 }, { predicted: 0.7, observed: 0.52, count: 30 }, { predicted: 0.9, observed: 0.85, count: 8 }, ]" mode="bars" title="Deviation" width="300" height="40" /> ``` ```tsx `" > <CalibrationStrip data="[ { predicted: 0.2, observed: 0.21, count: 40 }, { predicted: 0.5, observed: 0.44, count: 30 }, { predicted: 0.7, observed: 0.52, count: 30 }, ]" locale="de-DE" title="Localized probabilities" width="200" height="36" /> ``` 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 `" > <CalibrationStrip data="[ { predicted: 0.2, observed: 0.21, count: 25 }, { predicted: 0.4, observed: 0.39, count: 25 }, { predicted: 0.6, observed: 0.63, count: 25 }, { predicted: 0.8, observed: 0.79, count: 25 }, ]" title="Well calibrated" width="200" height="36" /> ``` ```tsx `" > <CalibrationStrip data="[ { predicted: 0.3, observed: 0.2, count: 3 }, { predicted: 0.6, observed: 0.7, count: 4 }, { predicted: 0.9, observed: 0.6, count: 3 }, ]" title="Sparse bins" width="200" height="36" /> ``` `minSupport` defaults to `max(10, 2% of total)`. Bins below it render open and faded, and the summary counts them, so a strip where every bin is sparse reads as one with no reliable bins rather than as a calibrated model. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Model calibration{" "} <span className="mc-inline"> <CalibrationStrip data={reliability} summary={false} /> </span>{" "} — well-calibrated above 0.6 predicted probability. </p> ``` **In a table cell** ```tsx <td> <CalibrationStrip data={reliability} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.92</span> <span className="unit">ECE score</span> <CalibrationStrip data={reliability} /> </div> ``` **In a tab header** ```tsx <button className="tab"> v2.1 <CalibrationStrip data={reliability} /> </button> ``` ## 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) An error rate sat at 30 for two weeks, then sat at 48. ChangePoint shades the two regimes, draws a mean hairline through each, and marks the break between them, so a reader gets "errors stepped up here" rather than "errors are high". The detector is a **documented heuristic, not statistics**: 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. Pass your own break indices and detection switches off — the chart becomes pure annotation. ```tsx `" > <ChangePoint data="[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48]" label="delta" title="Error rate" width="200" height="24" /> ``` ## Install [#install] ```tsx import { ChangePoint } from "@microcharts/react/change-point"; <ChangePoint data={errors} label="delta" title="Error rate" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ChangePoint } from "@microcharts/react/change-point/interactive"; <ChangePoint data={errors} label="delta" /> ``` ## When to use it [#when-to-use-it] Use it for context around an anomaly, an error rate / latency / cost that stepped to a new level, or annotating a known deploy or incident with explicit `breaks`. Skip it for a gradual trend (Sparkline) or a plain time series with no regime question. ## Sizing [#sizing] **annotate a known deploy (explicit break)** ```tsx <ChangePoint data={errors} breaks={[14]} /> ``` **a gradual ramp has no level shift** ```tsx <ChangePoint data={ramp} /> ``` ## Variants [#variants] ```tsx <ChangePoint data={[20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48]} />`" > <span className="inline-flex gap-4 items-center"> <ChangePoint data="[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48]" breaks="[14]" width="150" height="22" /> <ChangePoint data="[20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48]" width="150" height="22" /> </span> ``` This is a mean-shift test, not a trend test. A steady ramp is still fitted as two regimes: the second variant above reports a shift around point 10. Pass `breaks` explicitly when you already know the series is trending rather than stepping. ## Edge cases [#edge-cases] ```tsx `" > <ChangePoint data="[3000, 3000, 3000, 3000, 3000, 3000, 3000, 5200, 5200, 5200, 5200, 5200, 5200]" label="delta" locale="de-DE" title="Localized regime means" width="200" height="24" /> ``` ```tsx `" > <ChangePoint data="[10, 10, 10, 50, 50, 50]" title="Six points" width="150" height="22" /> ``` ```tsx `"> <ChangePoint data="[42]" title="One point" width="100" height="22" /> ``` 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, because 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Error rate this week{" "} <span className="mc-inline"> <ChangePoint data={errors} summary={false} /> </span>{" "} — step-up on day 14, regime break at 2.1%. </p> ``` **In a table cell** ```tsx <td> <ChangePoint data={errors} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">2.1%</span> <span className="unit">post-change regime</span> <ChangePoint data={errors} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <ChangePoint data={errors} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the shift, the break, the regime means, and what followed: **"Level shifted up 60% around point 14 (mean 30 → 48); stable since."** With no detected shift it reads "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 compares a handful of groups on two variables at once: how big each one is, and how activated it is. **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 as a number. Omit `lit` everywhere and you get a plain bar row. ```tsx `" > <CitySkyline data="[{ label: "Platform", value: 46, lit: 0.7 }, { label: "Core", value: 32, lit: 0.5 }, { label: "Web", value: 28, lit: 0.9 }, { label: "API", value: 40, lit: 0.3 }, { label: "Data", value: 18, lit: 0.6 }]" unit="teams" title="Team sizes" bw="16" gap="6" height="40" /> ``` ## Install [#install] ```tsx import { CitySkyline } from "@microcharts/react/city-skyline"; <CitySkyline data={teams} unit="teams" title="Team sizes" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CitySkyline } from "@microcharts/react/city-skyline/interactive"; <CitySkyline data={teams} /> ``` ## When to use it [#when-to-use-it] Use it 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. For a single variable reach for `MiniBar`; skip it when you need a precise activation read, or when you have more than about eight groups. ## Sizing [#sizing] **omit lit for a plain bar row** ```tsx <CitySkyline data={teams.map(({ label, value }) => ({ label, value }))} /> ``` **labelled, the two-variable read** ```tsx <CitySkyline data={teams} labels /> ``` ## Variants [#variants] Windows appear whenever `lit` is present. Heights stay zero-anchored bars either way, and nothing is encoded in building width: width, roof, and ground are constants. ```tsx ({ label, value }))} /> <CitySkyline data={teams} label="value" />`" > <span className="inline-flex gap-8 items-end"> <CitySkyline data="[ { label: "A", value: 46 }, { label: "B", value: 32 }, { label: "C", value: 40 }, ]" bw="14" gap="5" height="30" /> <CitySkyline data="[ { label: "A", value: 46, lit: 0.7 }, { label: "B", value: 32, lit: 0.4 }, ]" label="value" bw="16" gap="6" height="34" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <CitySkyline data="[ { label: "A", value: 30, lit: 0.8 }, { label: "B", value: 30, lit: 0.4 }, { label: "C", value: 30, lit: 0 }, ]" title="Flat skyline" bw="16" gap="6" height="36" /> ``` ```tsx `" > <CitySkyline data="[ { label: "big", value: 40, lit: 0.8 }, { label: "tiny", value: 2, lit: 0.9 }, ]" title="Secondary channel drops out first" bw="16" gap="6" height="36" /> ``` All-equal values render as a flat roofline; no variation is added to make the row look busier. Lit windows are quantized to the window count and filled bottom-up, so activation reads as a fill level. The secondary channel drops out before the primary: a building too short for one window row renders as a solid tower, and its `lit` still shows in the per-building readout on hover or keyboard focus. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Team sizes across eng{" "} <span className="mc-inline"> <CitySkyline data={teams} summary={false} /> </span>{" "} — Platform is largest at 46, 70% activated. </p> ``` **In a table cell** ```tsx <td> <CitySkyline data={teams} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">46</span> <span className="unit">heads · 70% lit</span> <CitySkyline data={teams} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Platform <CitySkyline data={teams} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives the count and the tallest: **"3 groups; tallest A at 46."** The interactive entry roves the buildings with ←/→ or hover and announces each one. On the team demo at the top of this page, Platform reads "Platform: 46; 70% lit." 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). | | `gap` | `number` | Gap between buildings in viewBox units (default 3). | | `fontSize` | `number` | Type size of the value numerals, in viewBox units. Defaults from `height`. | | `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) At month 1, the March vintage retains worst: 47%. Reading down a column compares every cohort at the **same maturity**, which is the question this chart is for. CohortTriangle stacks cohorts as rows and ages as columns, shading each cell by a discrete retention level. Newer vintages have been observed for fewer ages, which gives the block its **triangle** shape. ```tsx <CohortTriangle data="[ { label: "Jan", values: [1, 0.62, 0.48, 0.41, 0.38, 0.37] }, { label: "Feb", values: [1, 0.58, 0.44, 0.38, 0.35] }, { label: "Mar", values: [1, 0.47, 0.36, 0.31] }, { label: "Apr", values: [1, 0.55, 0.42] }, { label: "May", values: [1, 0.52] }, ]" cell="16" unit="month" title="Monthly retention" /> ``` ## Install [#install] ```tsx import { CohortTriangle } from "@microcharts/react/cohort-triangle"; <CohortTriangle data={cohorts} unit="month" title="Monthly retention" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CohortTriangle } from "@microcharts/react/cohort-triangle/interactive"; <CohortTriangle data={cohorts} cell={12} unit="month" /> ``` ## When to use it [#when-to-use-it] Use it for monthly or weekly cohorts side by side, spotting which vintage decays worst, or reading retention at equal maturity in a KPI card. Color intensity is deliberately approximate, so pair it with a number when you need exact per-cell values. For a single cohort, [RetentionCurve](/docs/charts/retention-curve) draws the decay 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. **labelled grid** ```tsx <CohortTriangle data={cohorts} unit="month" /> ``` **highlight a vintage** ```tsx // ring one cohort to anchor the equal-maturity comparison <CohortTriangle data={cohorts} highlight="Mar" /> ``` **dense (no labels)** ```tsx <CohortTriangle data={cohorts} labels={false} cell={8} /> ``` ## Variants [#variants] Rows are cohorts in input order and columns are age. Row labels are on by default; turn them off for a denser block, or `highlight` one vintage to anchor the equal-maturity comparison. No worst-vintage flag is painted onto the grid — the summary names the laggard in words instead. ```tsx <CohortTriangle data={cohorts} labels={false} cell={9} />`" > <span className="inline-flex flex-col gap-3"> <CohortTriangle data="[ { label: "Jan", values: [1, 0.62, 0.48, 0.41, 0.38, 0.37] }, { label: "Feb", values: [1, 0.58, 0.44, 0.38, 0.35] }, { label: "Mar", values: [1, 0.47, 0.36, 0.31] }, { label: "Apr", values: [1, 0.55, 0.42] }, { label: "May", values: [1, 0.52] }, ]" cell="16" unit="month" highlight="Mar" title="Worst vintage ringed" /> <CohortTriangle data="[ { label: "Jan", values: [1, 0.62, 0.48, 0.41, 0.38, 0.37] }, { label: "Feb", values: [1, 0.58, 0.44, 0.38, 0.35] }, { label: "Mar", values: [1, 0.47, 0.36, 0.31] }, { label: "Apr", values: [1, 0.55, 0.42] }, { label: "May", values: [1, 0.52] }, ]" labels="false" cell="9" title="Dense block" /> </span> ``` 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 `"> <CohortTriangle data="[{ label: "Jan", values: [1, 0.6, 0.45, 0.4] }]" unit="month" cell="16" title="One cohort" /> ``` ```tsx `" > <CohortTriangle data="[{ label: "Jan", values: [1, 0.6, null, 0.4] }]" cell="16" title="Gap slot" /> ``` ```tsx `" > <CohortTriangle data="[ { label: "Jan", values: [100, 62, 48] }, { label: "Feb", values: [100, 47] }, ]" cell="16" unit="month" title="Percent-scale input" /> ``` ```tsx `" > <CohortTriangle data="[ { label: "Jan", values: [1, 0.62, 0.48] }, { label: "Feb", values: [1, 0.47] }, ]" cell="16" unit="month" locale="de-DE" title="Localized retention" /> ``` A single cohort skips the comparison and 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> January retention{" "} <span className="mc-inline"> <CohortTriangle data={jan} labels={false} cell={10} unit="month" summary={false} /> </span>{" "} — the January vintage retains 37% at month 5. </p> ``` **In a table cell** ```tsx <td> <CohortTriangle data={jan} labels={false} cell={12} unit="month" summary={false} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">37%</span> <span className="unit">M5 retention</span> <CohortTriangle data={jan} labels={false} cell={12} unit="month" summary={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Jan <CohortTriangle data={jan} labels={false} cell={10} unit="month" summary={false} /> </button> ``` Best at KPI/card scale — cohort cells need room to resolve. ## Accessibility [#accessibility] Intensity is quantized to five discrete levels rather than a continuous ramp, since a smooth gradient would imply a precision a handful of pixels cannot deliver. It is still 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. | | `gap` | `number` | Gap between cells in viewBox units (default 2). | | `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 plots the current value as a bright head dot, with recent points trailing behind it and fading with age. In the interactive entry the head eases to each new value and the old head steps down into the trail, so a live stream draws a comet and a stall goes still. Opacity encodes **age only**; the y position carries the value. The trail is recency context, not data you have to decode. ```tsx <CometTrail data="[40, 45, 50, 55, 60, 65, 70, 72, 75, 78, 80, 84, 87]" title="Latency" width="90" /> ``` ## Install [#install] ```tsx import { CometTrail } from "@microcharts/react/comet-trail"; <CometTrail data={rollingWindow} title="Latency" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CometTrail } from "@microcharts/react/comet-trail/interactive"; <CometTrail data={rollingWindow} /> ``` ## 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 goes still, which is itself the signal. The head never simulates phantom positions between updates: it eases to each real value. Under `prefers-reduced-motion` the head repositions instantly, since the static encoding is already complete. The same holds off-screen — one shared viewport check means the ease only runs while the chart is in view. ## When to use it [#when-to-use-it] Use it 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. For the full history or an exact multi-point comparison reach for `Sparkline` (or `DotPlot`); for 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. It is capped at 20: past that the trail stops being a trail and becomes a sparkline, which is the better tool for the full history. ## Variants [#variants] `label="last"` is on by default, so the head always prints its number. Changing `trail` gives you more or less recency context without changing the head read, because opacity encodes age and nothing else. ```tsx <CometTrail data={window} label="none" />`" > <span className="inline-flex gap-6 items-center"> <CometTrail data="[40, 45, 50, 55, 60, 65, 70, 72, 75, 78, 80, 84, 87]" trail="6" width="100" summary="false" /> <CometTrail data="[40, 45, 50, 55, 60, 65, 70, 72, 75, 78, 80, 84, 87]" label="none" width="100" summary="false" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <CometTrail data="[]" title="No stream yet" width="90" /> ``` ```tsx `"> <CometTrail data="[52]" title="First reading" width="90" /> ``` ```tsx `" > <CometTrail data="[40, 55, 48, 62]" trail="20" title="Short stream" width="90" /> ``` Empty data draws just the frame, with "No data." as the summary. A single point has no trail to fade: the head alone, and "Now 52." A `trail` larger than the data never backfills or pads. It shows every point there is. ## Four homes [#four-homes] **In a sentence** ```tsx <p> p95 latency is climbing{" "} <span className="mc-inline"> <CometTrail data={rollingWindow} label="none" width={90} height={16} summary={false} /> </span>{" "} — now at 87. </p> ``` **In a table cell** ```tsx <td><CometTrail data={rollingWindow} trail={6} label="none" width={72} height={16} /></td> ``` **In a KPI card** ```tsx <div className="kpi"><span className="figure">87</span><CometTrail data={rollingWindow} width={200} height={36} /></div> ``` **In a tab header** ```tsx <button className="tab">checkout <CometTrail data={rollingWindow} trail={5} label="none" width={44} height={14} /></button> ``` ## Accessibility [#accessibility] The accessible name is the now-value and the recent trend: **"Now 62, rising over the last 3 updates."** With a single point it is the value alone, 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 no reading depends on decoding the fade. 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). | | `fontSize` | `number` | Type size of the numeral after the head, in viewBox units. Defaults from `height`. | 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) A classifier is 73% accurate; ConfusionGrid shows where the other 27% went, which is what an accuracy 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 an inset stroke rather than by color, and the ink ramp is the same for agreement and error cells, so good and bad are positions on the grid. That is what keeps it readable under forced colors. Put the one-line key — rows actual, columns predicted — next to every example. ```tsx `" > <ConfusionGrid data="{ labels: ["A", "B", "C"], counts: [[70, 8, 2], [6, 62, 12], [3, 9, 58]] }" label="accuracy" title="Classifier" size="120" /> ``` ## Install [#install] ```tsx import { ConfusionGrid } from "@microcharts/react/confusion-grid"; <ConfusionGrid data={counts} title="Classifier" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ConfusionGrid } from "@microcharts/react/confusion-grid/interactive"; <ConfusionGrid data={{ labels, counts }} /> ``` ## When to use it [#when-to-use-it] Use it for classifier evaluation and any paired-classification agreement: triage vs outcome, plan vs actual, rater A vs rater B. For a single accuracy number reach for Delta; for more than four classes, a full-size heatmap. ## Sizing [#sizing] **KPI card** ```tsx <ConfusionGrid data={counts} label="accuracy" size={64} /> ``` **worst-confusion accent** ```tsx <ConfusionGrid data={counts} accent="errors" /> ``` ## Variants [#variants] `accent="errors"` moves the accent from the diagonal to the worst confusion cell. `label="accuracy"` prints the accuracy number; it is off by default and never rendered without the grid, so the number does not leave its context. ```tsx `"> <ConfusionGrid data="{ labels: ["A", "B", "C"], counts: [ [70, 8, 2], [6, 40, 34], [3, 9, 58], ], }" accent="errors" title="Worst confusion" size="110" /> ``` ## Edge cases [#edge-cases] ```tsx 4 clamps to the first four classes (dev-warns)" code="`<ConfusionGrid data={{ labels: ["A", "B", "C", "D", "E"], counts: [ [40, 3, 1, 0, 2], [4, 38, 2, 1, 0], [1, 3, 44, 2, 1], [0, 2, 3, 36, 4], [2, 1, 0, 3, 41], ], }} title="5 classes" />`" > <ConfusionGrid data="{ labels: ["A", "B", "C", "D", "E"], counts: [ [40, 3, 1, 0, 2], [4, 38, 2, 1, 0], [1, 3, 44, 2, 1], [0, 2, 3, 36, 4], [2, 1, 0, 3, 41], ], }" title="5 classes" size="110" /> ``` 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 warning is dev-only, but the render truncates in production too; pass pre-filtered `labels`/`counts` if you need a specific four. ```tsx `" > <ConfusionGrid data="{ labels: ["cat", "dog"], counts: [ [40, 10], [0, 0], ], }" title="No dog samples" size="90" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Classifier accuracy{" "} <span className="mc-inline"> <ConfusionGrid data={{ labels, counts }} summary={false} /> </span>{" "} — 87% cats correct, 12% dogs misclassified. </p> ``` **In a table cell** ```tsx <td> <ConfusionGrid data={{ labels, counts }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">87%</span> <ConfusionGrid data={{ labels, counts }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> cats <ConfusionGrid data={{ labels, counts }} /> </button> ``` Best at KPI/card scale — the matrix is hard to read below ~36px. ## Accessibility [#accessibility] The row view answers the question practitioners ask — "of the real cats, how many did we call dogs?" — so the summary phrasing states the row normalization ("% of cats") and the denominator travels with every number. 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, with the tally it was computed from: **"Actual cat, predicted dog: 12% of cats (12)."** A row percentage cannot be inverted back to a count without the row 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) | `{ 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. | | `size` | `number` | Grid square edge in viewBox units. Defaults to a size that grows with the class count. | | `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) Four incidents, the largest of them in September. Constellation places each event as a dot by time (x) and value (y), with an optional magnitude driving its size by area (`r ∝ √m`), because area is how the eye reads magnitude. A hairline connects the events in time order, so a handful of incidents shows its sequence. 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]; <Constellation data={[ { x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }, { x: 5, y: 30, m: 3 }, { x: 8, y: 65, m: 5 }, ]} xFormat={month} title="Incidents" />`" > <Constellation data="[{ x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }, { x: 5, y: 30, m: 3 }, { x: 8, y: 65, m: 5 }]" xFormat="(x) => ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep"][x]" title="Incidents" width="120" height="34" /> ``` ## Install [#install] ```tsx import { Constellation } from "@microcharts/react/constellation"; <Constellation data={[ { x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }, { x: 5, y: 30, m: 3 }, { x: 8, y: 65, m: 5 }, ]} title="Incidents" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Constellation } from "@microcharts/react/constellation/interactive"; <Constellation data={events} /> ``` ## When to use it [#when-to-use-it] Use it for sparse incidents or outages on a timeline, milestones carrying a magnitude, or any rare events where the sequence is the story. For dense event streams reach for `Seismogram` or `EventTimeline`; for a continuous trend, `Sparkline`. Skip it when you need precise value comparison, since dot area is a low-precision channel. ## Sizing [#sizing] **month labels in the summary** ```tsx const monthFmt = (x: number) => ["Jan","Feb","Mar","Apr","May","Jun"][x]; <Constellation data={events} xFormat={monthFmt} /> ``` **pure scatter (no chronology line)** ```tsx <Constellation data={events} connect={false} /> ``` ## Variants [#variants] `connect` is on by default, since with rare events the order and rhythm are usually the story. Turn it off for a plain scatter, or set `label="max"` to print the largest event's value. ```tsx <Constellation data={events} connect={false} />`" > <span className="inline-flex gap-6 items-center"> <Constellation data="[ { x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }, { x: 5, y: 30, m: 3 }, { x: 8, y: 65, m: 5 }, ]" label="max" width="110" height="32" summary="false" /> <Constellation data="[ { x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }, { x: 5, y: 30, m: 3 }, { x: 8, y: 65, m: 5 }, ]" connect="false" width="110" height="32" summary="false" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <Constellation data="[]" title="No incidents" width="110" height="28" /> ``` ```tsx `"> <Constellation data="[{ x: 3, y: 40 }]" title="One incident" width="110" height="28" /> ``` ```tsx `"> <Constellation data="[{ x: 0 }, { x: 2 }, { x: 5 }, { x: 8 }]" title="No values" width="110" height="28" /> ``` 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: one dot, and the summary says "1 event at …" rather than forcing a range. When every point omits `y`, the vertical position is **deterministic jitter that encodes nothing** — it spreads the dots so they do not stack. In that mode the connector's slope carries no meaning (it still runs in time order), and neither the summary nor the interactive readout mentions vertical position. The x position is never jittered, so simultaneous events sit at the same x. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Incidents this quarter{" "} <span className="mc-inline"> <Constellation data={[{ x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }]} summary={false} /> </span>{" "} — four events, largest severity in February. </p> ``` **In a table cell** ```tsx <td> <Constellation data={[{ x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">4</span> <span className="unit">this quarter</span> <Constellation data={[{ x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Prod <Constellation data={[{ x: 0, y: 40, m: 2 }, { x: 2, y: 90, m: 7 }]} /> </button> ``` Best at KPI/card scale — event clouds need height to separate. ## 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. Dot area is a low-precision channel, so magnitude is always carried by a number as well, never by size 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). | | `fontSize` | `number` | Type size of the peak label, in viewBox units. Defaults from `height`. | | `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) Thirty fill-weight readings off a production line, two of them outside the band. ControlStrip separates ordinary process variation from a genuine excursion: it is a Shewhart individuals chart, and 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, so the eye lands on the exception instead of scanning every dot. ```tsx `" > <ControlStrip data="[74, 73, 75, 74, 76, 73, 74, 75, 74, 73, 82, 74, 75, 73, 74, 76, 74, 73, 75, 74, 66, 74, 75, 74, 73, 76, 74, 75, 74, 73]" title="Line 3 fill weight" width="260" height="26" /> ``` ## Install [#install] ```tsx import { ControlStrip } from "@microcharts/react/control-strip"; <ControlStrip data={weights} title="Line 3 fill weight" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ControlStrip } from "@microcharts/react/control-strip/interactive"; <ControlStrip data={weights} /> ``` ## When to use it [#when-to-use-it] Use it for a production line or metric in a table cell, an SPC control chart in a KPI card, or flagging out-of-control excursions. For a plain trend reach for Sparkline; for a strongly trending series, ChangePoint, since control limits assume a stationary process. ## Sizing [#sizing] **Western Electric run rules** ```tsx <ControlStrip data={weights} rules="we" /> ``` **known baseline (golden period)** ```tsx <ControlStrip data={weights} baseline={74} /> ``` ## Variants [#variants] `rules="we"` adds the implemented Western Electric subset, enumerated so none of them fires silently: WE-1 (a point beyond 3σ), WE-2 (two of three beyond 2σ), and WE-4 (eight on one side). `dots="all"` draws every point rather than only the excursions, and `limits="percentile"` swaps the ±3σ̂ band for stated quantiles, for skewed processes. ```tsx <ControlStrip data={weights} dots="all" />`" > <span className="inline-flex flex-col gap-1.5"> <ControlStrip data="[ 74, 73, 75, 74, 76, 73, 74, 75, 74, 73, 82, 74, 75, 73, 74, 76, 74, 73, 75, 74, 66, 74, 75, 74, 73, 76, 74, 75, 74, 73, ]" rules="we" title="WE rules" width="220" height="24" /> <ControlStrip data="[74, 73, 75, 74, 76, 73, 74, 75, 74, 73, 82, 74, 75, 73, 74, 76]" dots="all" title="All dots" width="220" height="24" /> </span> ``` ```tsx `" > <ControlStrip data="[2.4, 2.5, 2.45, 2.55, 2.5, 2.6, 2.4, 2.5, 2.45, 2.9, 2.5, 2.55]" format="{ maximumFractionDigits: 1 }" locale="de-DE" title="Localized limits" width="220" height="24" /> ``` 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 `" > <ControlStrip data="[10, 11, 9, 10, 12, 8]" title="Provisional" width="160" height="22" /> ``` ```tsx `" > <ControlStrip data="[5, 5, 5, 5]" title="Constant" width="160" height="22" /> ``` Below 10 points there are too few moving ranges to trust σ̂, so the band gets a dashed outline and the summary appends "Limits provisional (n=6)". A constant series has MR̄ = 0 and therefore σ̂ = 0: the band collapses into the center hairline rather than painting a fake zero-height zone. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Line 3 fill weight{" "} <span className="mc-inline"> <ControlStrip data={weights} summary={false} /> </span>{" "} — in control except one excursion at t=18. </p> ``` **In a table cell** ```tsx <td> <ControlStrip data={weights} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">74g</span> <span className="unit">in control</span> <ControlStrip data={weights} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Line 1 <ControlStrip data={weights} /> </button> ``` ## 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) A sensor was meant to report 18 times and reported 10. CoverageStrip fills the measured slots and outlines the missing ones with a hairline, so you can see where nothing was measured before you trust the numbers. The distinction between `null` (no measurement) and `0` (a measured zero) is the whole chart, and **shape** carries it, so it survives forced-colors and print. Nothing is interpolated across a gap. ```tsx <CoverageStrip data="[3, 4, null, 5, 0, null, null, 6, 8, 7, null, 9, 11, 10]" expected="18" label="percent" title="Sensor uptime" width="240" height="16" /> ``` ## Install [#install] ```tsx import { CoverageStrip } from "@microcharts/react/coverage-strip"; <CoverageStrip data={readings} expected={18} label="percent" title="Sensor uptime" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CoverageStrip } from "@microcharts/react/coverage-strip/interactive"; <CoverageStrip data={readings} expected={18} label="percent" /> ``` ## When to use it [#when-to-use-it] Use it for data-quality cells beside a metric, sensor-uptime rows, and trailing-gap detection. For magnitude over time reach for HeatStrip; for exact values, Sparkline. ## Sizing [#sizing] **trailing gap counts** ```tsx // an array that simply stops is the worst gap of all <CoverageStrip data={[1, 1, 1]} expected={8} label="percent" /> ``` **zero ≠ missing** ```tsx // a measured zero is filled; a gap is hollow <CoverageStrip data={[3, 0, null, 5]} /> ``` ## Variants [#variants] ```tsx <CoverageStrip data={readings} shape="round" />`" > <span className="inline-flex flex-col gap-1.5"> <CoverageStrip data="[3, 4, null, 5, 0, null, 6, 8, 7, null, 9, 11]" mode="intensity" domain="[0, 12]" title="Intensity" width="200" height="12" /> <CoverageStrip data="[3, 4, null, 5, 0, null, 6, 8, 7, null, 9, 11]" shape="round" title="Round" width="200" height="12" /> </span> ``` ```tsx `" > <CoverageStrip data="[3, 4, null, 5, 0, null, null, 6, 8, 7, null, 9, 11, 10]" expected="18" label="percent" locale="de-DE" title="Localized coverage" width="200" height="14" /> ``` `mode="intensity"` shades each filled slot by its value; `shape="round"` swaps the slot shape. `locale` formats the coverage percentage in the gutter and the accessible summary: the strip above reads "56 %" with German percent spacing instead of "56%". `format` shapes the measured values the interactive entry announces. ## Edge cases [#edge-cases] ```tsx `" > <CoverageStrip data="[1, 1, 1]" expected="8" label="percent" title="Trailing gap" width="180" height="16" /> ``` ```tsx `"> <CoverageStrip data="[3, 0, null, 5]" title="Zero vs missing" width="100" height="16" /> ``` ```tsx `"> <CoverageStrip data="[]" title="Empty" width="80" height="16" /> ``` An `expected` beyond `data.length` pads the tail with hollow slots, so a feed that stops reporting counts as coverage loss rather than dropping out of the denominator. A measured `0` is a filled cell like any other value; only `null` (no record) is hollow. A missing slot stays visibly empty instead of being smoothed into its neighbors, and an empty series renders the frame alone. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Sensor uptime this week{" "} <span className="mc-inline"> <CoverageStrip data={readings} summary={false} /> </span>{" "} — 86% coverage, two gaps on Tuesday. </p> ``` **In a table cell** ```tsx <td> <CoverageStrip data={readings} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">86%</span> <span className="unit">sensor uptime</span> <CoverageStrip data={readings} /> </div> ``` **In a tab header** ```tsx <button className="tab"> North <CoverageStrip data={readings} /> </button> ``` ## 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) Friday peaks at 61, Sunday dips to 38, and slot 2 has been climbing across three cycles. CyclePlot reshapes the series 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 is the spine and drift is the within-slot lines: two questions of the same data, on two marks. ```tsx `" > <CyclePlot data="[38, 40, 45, 48, 52, 61, 44, 38, 42, 45, 48, 52, 61, 44, 38, 44, 45, 48, 52, 61, 44, 38, 46, 45, 48, 52, 61, 44, 38, 48, 45, 48, 52, 61, 44, 38, 50, 45, 48, 52, 61, 44]" period="7" slots="["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]" cycleUnit="weeks" title="Weekly shape" width="200" height="40" /> ``` ## Install [#install] ```tsx import { CyclePlot } from "@microcharts/react/cycle-plot"; <CyclePlot data={daily} period={7} slots={weekdays} cycleUnit="weeks" title="Weekly shape" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { CyclePlot } from "@microcharts/react/cycle-plot/interactive"; <CyclePlot data={daily} period={7} /> ``` ## When to use it [#when-to-use-it] Use it for a KPI card that says "the week has a shape", for weekday traffic / hourly load / monthly sales seasonality, or for spotting one slot that is itself drifting. For a plain time series reach for Sparkline; for one composition, SegmentedBar. A `period` outside 4–12 warns in development: below that there is no cycle to see, above it the slots stop being separable. ## Sizing [#sizing] **median center (skewed slots)** ```tsx <CyclePlot data={daily} period={7} center="median" /> ``` **spine only (quiet form)** ```tsx <CyclePlot data={daily} period={7} trend={false} /> ``` ## Variants [#variants] `center` picks the slot statistic: mean by default, `"median"` when a slot's distribution is skewed. The interactive per-slot announcement names which one it used. `trend={false}` drops the spine and leaves the within-slot lines. Those lines are never smoothed and never joined across a slot boundary — each polyline begins and ends inside its own column, so a Monday trend cannot bleed into Tuesday. ```tsx <CyclePlot data={[38, 40, 45, 48, 52, 61, 44, 38, 42, 45, 48, 52, 61, 44, 38, 44, 45, 48, 52, 61, 44]} period={7} trend={false} />`" > <span className="inline-flex gap-4 items-center"> <CyclePlot data="[38, 40, 45, 48, 52, 61, 44, 38, 42, 45, 48, 52, 61, 44, 38, 44, 45, 48, 52, 61, 44]" period="7" center="median" width="150" height="32" /> <CyclePlot data="[38, 40, 45, 48, 52, 61, 44, 38, 42, 45, 48, 52, 61, 44, 38, 44, 45, 48, 52, 61, 44]" period="7" trend="false" width="150" height="32" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <CyclePlot data="[null, 40, 45, 48, 52, 61, null, 42, 45, 48, 52, 61, null, 44, 45, 48, 52, 61]" period="6" title="One slot always missing" width="160" height="32" /> ``` ```tsx `" > <CyclePlot data="[38, 45, 52, 44, 40, 48, 61]" period="7" title="One cycle" width="160" height="32" /> ``` ```tsx `" > <CyclePlot data="[3800, 4000, 4500, 4800, 5200, 6100, 4400, 3800, 4200, 4500, 4800, 5200, 6100, 4400]" period="7" locale="de-DE" title="Localized visits" width="200" height="36" /> ``` A slot with no finite values 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, since a line needs at least two observations in a slot. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Weekly traffic shape{" "} <span className="mc-inline"> <CyclePlot data={daily} period={7} summary={false} /> </span>{" "} — weekday peak Wednesday, weekend dip. </p> ``` **In a table cell** ```tsx <td> <CyclePlot data={daily} period={7} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">Wed</span> <span className="unit">peak day</span> <CyclePlot data={daily} period={7} /> </div> ``` **In a tab header** ```tsx <button className="tab"> US <CyclePlot data={daily} period={7} /> </button> ``` Best at KPI/card scale — weekly cycles need width for slot groups. ## 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 compares two versions of a dataset and gives each key 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 table that added 500 rows and dropped 480 nets to +20, the same as one that added 20 and dropped nothing, and those are not the same event. ```tsx <DataDiff data="[ { key: "users", added: 340, removed: 120 }, { key: "orders", added: 88, removed: 30 }, { key: "items", added: 40, removed: 20 }, { key: "tags", added: 24, removed: 8 }, { key: "notes", added: 12, removed: 6 }, { key: "flags", added: 8, removed: 3 }, ]" title="Schema diff" width="200" height="72" /> ``` ## Install [#install] ```tsx import { DataDiff } from "@microcharts/react/data-diff"; <DataDiff data={diff} title="Schema diff" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DataDiff } from "@microcharts/react/data-diff/interactive"; <DataDiff data={diff} /> ``` ## When to use it [#when-to-use-it] Use it for a table cell per dataset version, a KPI card for a sync or import job, or any per-key added/removed audit where churn matters. For a plain ranking reach for MiniBar; for parts of a single whole, SegmentedBar. ## Sizing [#sizing] **key tags for standalone use** ```tsx <DataDiff data={diff} labels /> ``` **net tick + totals footer** ```tsx <DataDiff data={diff} net label="totals" /> ``` ## Variants [#variants] `labels` prints in-chart key tags, `net` adds a per-row tick at added − removed, and `label="totals"` prints a `+added / −removed` footer. One symmetric scale spans every row, so a big change never shrinks to fit a small one; to compare across charts, pass them a shared `domain`. ```tsx <DataDiff data={[ { key: "users", added: 340, removed: 120 }, { key: "orders", added: 88, removed: 30 }, { key: "items", added: 40, removed: 20 }, ]} net label="totals" />`" > <span className="inline-flex gap-4"> <DataDiff data="[ { key: "users", added: 340, removed: 120 }, { key: "orders", added: 88, removed: 30 }, { key: "items", added: 40, removed: 20 }, ]" width="150" height="56" /> <DataDiff data="[ { key: "users", added: 340, removed: 120 }, { key: "orders", added: 88, removed: 30 }, { key: "items", added: 40, removed: 20 }, ]" label="totals" width="150" height="56" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <DataDiff data="[]" title="No keys" width="100" height="24" /> ``` ```tsx `"> <DataDiff data="[ { key: "a", added: 0, removed: 0 }, { key: "b", added: 0, removed: 0 }, ]" title="No changes" width="100" height="24" /> ``` An empty `data` array renders an empty chart and reports **"No data."** A non-empty diff where every key is 0/0 still draws a hairline placeholder tick per row and reports **"No changes across 2 keys."** A key with no change keeps its row, so the absence of a change never reads as the absence of the key. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Schema migration diff{" "} <span className="mc-inline"> <DataDiff data={diff} summary={false} /> </span>{" "} — 3 tables added, 1 column removed. </p> ``` **In a table cell** ```tsx <td> <DataDiff data={diff} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+3</span> <span className="unit">tables added</span> <DataDiff data={diff} /> </div> ``` **In a tab header** ```tsx <button className="tab"> users <DataDiff data={diff} /> </button> ``` ## 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 prints a signed number with a direction glyph: `+12.4%` with an up triangle, `−8%` with a down one. It is text-first, so 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: a red/green-only read fails for color-blind viewers and under `forced-colors`. ```tsx <span className="text-lg"> <Delta value="0.124" title="Revenue vs last week" /> </span> ``` ## Install [#install] ```tsx import { Delta } from "@microcharts/react/delta"; <Delta value={0.124} title="Revenue vs last week" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Delta } from "@microcharts/react/delta/interactive"; <Delta value={0.12} /> ``` ## When to use it [#when-to-use-it] Use it for a KPI's change, a period-over-period percent, or inline metric movement. Skip it 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. **inherits text size** ```tsx // Delta is text — it takes the font-size of whatever wraps it <span style={{ fontSize: "1rem" }}> Revenue <Delta value={0.124} /> </span> ``` **larger** ```tsx // scale it up beside a KPI figure by lifting the font-size <span style={{ fontSize: "1.75rem" }}> <Delta value={0.124} /> </span> ``` ## Variants [#variants] `from` derives a percent change. `positive="down"` flips which color means "good" for metrics where down is good (latency, churn, cost); it never changes which way the glyph points, so such a metric still shows a down-pointing triangle in the "good" color. Percent is the default format, since most deltas are relative change rather than raw magnitude. Pass `format` for currency, counts, or any other unit. ```tsx `"> <span className="text-lg"> <Delta value="0.124" title="Percent" /> </span> ``` ```tsx `"> <span className="text-lg"> <Delta value="128" from="100" title="From prior" /> </span> ``` ```tsx `"> <span className="text-lg"> <Delta value="-0.08" title="Down is bad" /> </span> ``` ```tsx `"> <span className="text-lg"> <Delta value="-0.05" positive="down" title="Down is good" /> </span> ``` `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 `"> <span className="text-lg"> <Delta value="0.124" format="{ style: "percent", maximumFractionDigits: 1 }" locale="de-DE" title="Localized change" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <span className="text-lg"> <Delta value="0" title="Flat" /> </span> ``` ```tsx <Delta value={Infinity} />`" > <span className="inline-flex gap-4 text-lg"> <Delta value="Number.NaN" title="Not a number" /> <Delta value="Number.POSITIVE_INFINITY" title="Infinite" /> </span> ``` 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 in place of a number, and **"No change."** rather than a misleading `NaN%`. ## Four homes [#four-homes] **In a sentence** ```tsx <p> API p95 latency held at 212ms this hour,{" "} <Delta value={-0.08} positive="down" title="vs the previous hour" />{" "} from the hour before. </p> ``` **In a table cell** ```tsx <td> {region.revenue} <Delta value={region.value} title={`${region.region} vs last quarter`} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$84.2k</span> <Delta value={0.124} title="vs last month" /> </div> ``` **In a tab header** ```tsx <button className="tab"> Growth <Delta value={0.124} /> </button> ``` ## 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) Demand outweighs supply 1.42× and the spread is 1. DepthWedge draws the shape behind those numbers: demand accumulates leftward from the spread and supply rightward, so two filled wedges carry the book's posture in a glyph, with the spread — the gap between the best bid and the best ask — printed above the mid line. The y-scale is linear, full stop. A silent log axis would exaggerate near-spread depth and flatter thin books, and log-depth reading belongs to full-size tools. ```tsx `" > <DepthWedge data="{ demand: [ { level: 99.75, amount: 420 }, { level: 99.5, amount: 360 }, { level: 99.25, amount: 280 }, { level: 99, amount: 200 }, { level: 98.5, amount: 120 }, ], supply: [ { level: 100.25, amount: 300 }, { level: 100.5, amount: 240 }, { level: 100.75, amount: 160 }, { level: 101, amount: 90 }, ], }" title="Order book" width="300" height="30" /> ``` ## Install [#install] ```tsx import { DepthWedge } from "@microcharts/react/depth-wedge"; <DepthWedge data={{ demand, supply }} title="Order book" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DepthWedge } from "@microcharts/react/depth-wedge/interactive"; <DepthWedge data={{ demand, supply }} /> ``` ## When to use it [#when-to-use-it] Use it for order-book depth or liquidity and for supply vs demand posture. For a time series reach for Sparkline; for a single ratio, Delta. ## Sizing [#sizing] **pair cell** ```tsx <DepthWedge data={{ demand, supply }} label="none" width={60} height={16} /> ``` **normalized** ```tsx <DepthWedge data={{ demand, supply }} normalize /> ``` ## Variants [#variants] `normalize` plots cumulative shares per side instead of absolute amounts. `levels` sets how far from the mid the wedges reach, and that visible range is part of the claim: whenever the summary names a leading side it scopes the ratio with "within the shown range". ```tsx `" > <DepthWedge data="{ demand: [ { level: 99.5, amount: 420 }, { level: 99, amount: 260 }, ], supply: [ { level: 100.5, amount: 300 }, { level: 101, amount: 180 }, ], }" title="Normalized" width="300" height="30" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <DepthWedge data="{ demand: [ { level: 99.5, amount: 200 }, { level: 99, amount: 100 }, ], supply: [ { level: 100.5, amount: 200 }, { level: 101, amount: 100 }, ], }" title="Balanced" width="300" height="30" /> ``` ```tsx `" > <DepthWedge data="{ demand: [{ level: 99.5, amount: 200 }], supply: [] }" title="One-sided" width="300" height="30" /> ``` Equal totals per side make the summary state the balance rather than name a lead. An empty side renders a single wedge and the spread reads 0, since there is no gap to measure without both a best bid and a best ask. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Order book depth{" "} <span className="mc-inline"> <DepthWedge data={{ demand, supply }} summary={false} /> </span>{" "} — bid-side liquidity thicker below mid. </p> ``` **In a table cell** ```tsx <td> <DepthWedge data={{ demand, supply }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.04</span> <span className="unit">bid/ask imbalance</span> <DepthWedge data={{ demand, supply }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> BTC <DepthWedge data={{ demand, supply }} /> </button> ``` Best at KPI/card scale — book depth needs width to show both sides. ## Accessibility [#accessibility] The accessible name states the posture, scoped to the shown range: **"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 prints a small count as a die face: five pips for `5`, an empty face for `0`. The canonical 1–6 patterns are subitizable, so you read them without counting. Zero means zero, not missing data. Above six there is no pattern to subitize, so the face shows the exact numeral instead of inventing a seven-pip layout. ```tsx <span className="inline-flex gap-3 items-center"> <DicePips value="1" size="20" title="one" /> <DicePips value="2" size="20" title="two" /> <DicePips value="3" size="20" title="three" /> <DicePips value="4" size="20" title="four" /> <DicePips value="5" size="20" title="five" /> <DicePips value="6" size="20" title="six" /> </span> ``` ## Install [#install] ```tsx import { DicePips } from "@microcharts/react/dice-pips"; <DicePips value={4} title="Severity" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DicePips } from "@microcharts/react/dice-pips/interactive"; <DicePips value={4} /> ``` ## When to use it [#when-to-use-it] Use it for a severity or rating 0–6 in a cell, a small count in a sentence, or an incident-severity badge. For counts above six use TallyMarks, for magnitudes MiniBar, and for proportions Progress. ## Sizing [#sizing] **pips-only for dense columns** ```tsx <DicePips value={5} face={false} /> ``` **above 6 shows the exact numeral — no invented pattern** ```tsx <DicePips value={9} /> ``` ## Variants [#variants] ```tsx <DicePips value={9} />`" > <span className="inline-flex gap-4 items-center"> <DicePips value="5" face="false" size="22" /> <DicePips value="9" size="22" /> </span> ``` The face outline is on by default because it keeps a lone die legible on any surface. `face={false}` drops it for a repeated table column, where the header already frames the column. The second demo is the numeral fallback at `value={9}`. ## Edge cases [#edge-cases] ```tsx `"> <DicePips value="0" size="22" /> ``` ```tsx `"> <DicePips value="-2" size="22" /> ``` ```tsx `"> <DicePips value="3.6" size="22" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Incident severity{" "} <span className="mc-inline"> <DicePips value={4} summary={false} /> </span>{" "} — rated 4 of 6, elevated but not critical. </p> ``` **In a table cell** ```tsx <td> <DicePips value={4} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">4</span> <span className="unit">of 6</span> <DicePips value={4} /> </div> ``` **In a tab header** ```tsx <button className="tab"> INC-142 <DicePips value={4} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the value against its range: **"4 out of 6."** Past six it drops the range and gives the number alone (**"9."**). Zero reads "0 out of 6." 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). | | `size` | `number` | Die square edge in viewBox units (default 16). | 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 places each named value as a dot on one shared scale, which is the minimum ink per comparison. Reach for it over bars when the scale doesn't start at zero: position stays readable where a truncated bar length would overstate the difference. Without `stem` the domain fits the data and position is the read; with `stem` the domain is forced through zero and the read becomes magnitude. ```tsx `" > <DotPlot data="[ { label: "Ada", value: 96 }, { label: "Kim", value: 41 }, { label: "Sam", value: 88 }, ]" title="Review scores" width="180" height="66" /> ``` ## Install [#install] ```tsx import { DotPlot } from "@microcharts/react/dot-plot"; <DotPlot data={team} title="Review scores" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DotPlot } from "@microcharts/react/dot-plot/interactive"; <DotPlot data={team} /> ``` ## When to use it [#when-to-use-it] Use it for KPI leaderboards, named comparisons in cards, and rows where a truncated bar would overstate the difference. Avoid it above 7 rows; for a time series use Sparkline. ## Sizing [#sizing] **KPI leaderboard** ```tsx <DotPlot data={team} label="value" highlight="Ada" width={140} height={56} /> ``` **magnitude read (stems)** ```tsx // stems force a zero-anchored domain — position becomes magnitude <DotPlot data={team} stem /> ``` ## Variants [#variants] ```tsx `" > <DotPlot data="[ { label: "Ada", value: 96 }, { label: "Kim", value: 41 }, { label: "Sam", value: 88 }, ]" title="Magnitude read" width="180" height="66" /> ``` ```tsx `"> <DotPlot data="[ { label: "Ada", value: 96 }, { label: "Kim", value: 41 }, { label: "Sam", value: 88 }, ]" label="value" highlight="Ada" title="Leaderboard" width="180" height="66" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <DotPlot data="[]" title="No data" width="100" height="16" /> ``` ```tsx `"> <DotPlot data="[{ label: "Only", value: 42 }]" title="Single" width="100" height="16" /> ``` ```tsx `" > <DotPlot data="[ { label: "Ada", value: 96 }, { label: "Kim", value: null }, { label: "Sam", value: 88 }, ]" title="Gap preserved" width="180" height="66" /> ``` 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 and 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, so the real values around it keep the spacing they'd have in a fully populated set. It is also excluded from the category count in the summary. Two dots that would land on the same spot on adjacent rows de-overlap by half a unit. Labels truncate by character count and drop entirely once rows get dense. Both are arithmetic on the layout; nothing is measured from the rendered text. ## Four homes [#four-homes] **In a sentence** ```tsx <p> This cycle's review scores{" "} <span className="mc-inline"> <DotPlot data={team} width={76} height={34} summary={false} /> </span>{" "} spread from Kim's 41 to Ada's 96 — a 55-point range. </p> ``` **In a table cell** ```tsx <td> <DotPlot data={pod.scores} width={70} height={24} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">96</span> <span className="unit">Ada, of 5 reviewed</span> <DotPlot data={team} label="value" highlight="Ada" width={110} height={46} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Engineering <DotPlot data={pod.scores.slice(0, 3)} width={44} height={20} summary={false} /> </button> ``` ## Accessibility [#accessibility] The accessible name carries the count and the extremes: **"3 categories. Highest Ada 96, lowest Kim 41."** The interactive entry roves rows and announces each one 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) Conversion is up 75% and the market it trades against is up 33%. DualSparkline puts both lines in one word-sized frame so the gap between them is the read. Exactly two series, ever: three overlapped lines at 16 px are unreadable, and SparkGroup is the chart for that. The benchmark renders dashed, thinner, and neutral, so it is never distinguished by color alone. Both series share one domain, so there are no dual axes and no per-series normalization. ```tsx <DualSparkline data="[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21]" compare="[12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16]" title="Conversion vs market" width="220" height="28" /> ``` ## Install [#install] ```tsx import { DualSparkline } from "@microcharts/react/dual-sparkline"; <DualSparkline data={ours} compare={market} title="Conversion vs market" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DualSparkline } from "@microcharts/react/dual-sparkline/interactive"; <DualSparkline data={ours} compare={market} /> ``` ## When to use it [#when-to-use-it] Use it for metric-vs-benchmark in table cells and actual-vs-plan in KPI cards. For 3 or more series use SparkGroup, and for series in different units use two charts, since this one never draws dual axes. ## Sizing [#sizing] **channel rows vs market** ```tsx {channels.map((c) => ( <DualSparkline key={c.id} data={c.series} compare={market} title={c.name} /> ))} ``` **flip the primary — benchmark as the judged line** ```tsx <DualSparkline data={market} compare={ours} label="last" /> ``` ## Variants [#variants] ```tsx <DualSparkline data={ours} compare={market} band={[13, 16]} />`" > <span className="inline-flex flex-col gap-3"> <DualSparkline data="[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21]" compare="[12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16]" label="last" title="With last value" width="220" height="28" /> <DualSparkline data="[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21]" compare="[12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16]" band="[13, 16]" title="With band" width="220" height="28" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <DualSparkline data="[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5]" compare="[12, 12.4, 12.8, 13.1]" title="Benchmark ends early" width="200" height="28" /> ``` ```tsx `"> <DualSparkline data="[12, null, 12.4, 14, null, 14.8, 16]" compare="[12, 12.4, null, 13.1, 13.6, null, 14.2]" title="Independent gaps" width="200" height="28" /> ``` ```tsx `" > <DualSparkline data="[1200, 1300, 1240, 1400, 1520, 1480, 1600]" compare="[1200, 1240, 1280, 1310, 1360, 1400, 1420]" label="last" locale="de-DE" title="Localized values" width="220" height="28" /> ``` Each series carries its own gaps. A `null` in `data` breaks only the primary line at that index, a `null` in `compare` breaks only the benchmark, and the nearest-x hover still reports both values (or "no data" for whichever side is missing). A benchmark shorter than the primary series ends where its data ends; stretching it to span the frame would fake a correlation. Endpoints that land on the same point dedupe to one dot. With a `locale`, the endpoint label and both hover values follow that locale's grouping. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout conversion is outrunning the market{" "} <span className="mc-inline"> <DualSparkline data={ours} compare={market} width={70} height={18} summary={false} /> </span>{" "} — up 75% vs the market's 33%. </p> ``` **In a table cell** ```tsx <td> <DualSparkline data={ours} compare={market} width={70} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">21%</span> <span className="unit">plan 16% · +75% vs +33%</span> <DualSparkline data={ours} compare={market} label="last" width={130} height={40} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Checkout <DualSparkline data={ours} compare={market} width={50} height={14} /> </button> ``` ## 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 computes two rolling means from one raw series: a thin fast window that reacts instantly, and a thick slow window that carries the sustained read. Both run against a target line, so a momentary spike stays separable from sustained drift. The slow trace is the thicker of the two because drift away from target matters more than a spike, and the window sizes are part of the reading, so `windows` is an explicit prop. It is the general form of any noisy metric with a compliance target. ```tsx <DualWindowMeter data="Array.from({ length: 60 }, (_, i) => -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2 - (i > 40 ? 2 : 0))" target="-23" title="Loudness" format="{ maximumFractionDigits: 1 }" width="300" height="28" /> ``` ## Install [#install] ```tsx import { DualWindowMeter } from "@microcharts/react/dual-window-meter"; <DualWindowMeter data={samples} target={-23} format={{ maximumFractionDigits: 1 }} title="Loudness" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { DualWindowMeter } from "@microcharts/react/dual-window-meter/interactive"; <DualWindowMeter data={samples} target={-23} windows={[3, 30]} /> ``` ## When to use it [#when-to-use-it] Use it for loudness and LUFS metering, latency SLO compliance, and CPU-headroom compliance. For a single series use Sparkline, and reach for something else when there is no target to compare against. ## Sizing [#sizing] **latency SLO cell** ```tsx <DualWindowMeter data={samples} target={200} width={80} height={16} /> ``` **with corridor** ```tsx <DualWindowMeter data={samples} target={70} band={[60, 80]} /> ``` ## Variants [#variants] ```tsx -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2, ); <DualWindowMeter data={samples} target={-23} band={[-25, -21]} />`" > <DualWindowMeter data="Array.from({ length: 60 }, (_, i) => -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2)" target="-23" band="[-25, -21]" title="Corridor" width="300" height="28" /> ``` ```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 <DualWindowMeter data={samples} target={-23} windows={[5, 20]} />`" > <DualWindowMeter data="Array.from({ length: 60 }, (_, i) => -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2)" target="-23" windows="[5, 20]" title="Custom windows" width="300" height="28" /> ``` ```tsx -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2 - (i > 40 ? 2 : 0), ); <DualWindowMeter data={samples} target={-23} format={{ maximumFractionDigits: 1 }} locale="de-DE" />`" > <DualWindowMeter data="Array.from({ length: 60 }, (_, i) => -22 + Math.sin(i / 3) * 4 + Math.sin(i / 11) * 2 - (i > 40 ? 2 : 0))" target="-23" format="{ maximumFractionDigits: 1 }" locale="de-DE" title="Localized readings" width="300" height="28" /> ``` With a `locale`, the right-edge readings and the accessible summary follow that locale's decimal mark: the fast reading above renders "-23,9" rather than "-23.9", and the accessible name reads "Slow window -25 vs target -23; fast -23,9." ## Edge cases [#edge-cases] ```tsx `" > <DualWindowMeter data="[48, 48.3, 48.6, 48.9, 49.2, 49.5, 49.8, 50.1, 50.4, 50.7]" target="50" title="Filling" width="200" height="28" /> ``` Both traces share one domain, and each begins only where its window has filled. With the default `windows={[3, 30]}` and the 10 samples above, the fast trace starts at sample 3 and the slow trace never appears at all; its readout shows "—" instead of a partial-window value. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Integrated loudness{" "} <span className="mc-inline"> <DualWindowMeter data={samples} target={-23} summary={false} /> </span>{" "} — −22 LUFS, within broadcast target. </p> ``` **In a table cell** ```tsx <td> <DualWindowMeter data={samples} target={-23} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">−22</span> <span className="unit">LUFS integrated</span> <DualWindowMeter data={samples} target={-23} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Track 1 <DualWindowMeter data={samples} target={-23} /> </button> ``` ## 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 draws each row as a hollow dot at `from`, a filled dot at `to`, and a connector between them: a band that moved 62,000 → 84,000 reads without a legend. Direction is shape-coded, so it never relies on color alone, and the pair renders the same whichever way the values run. With `positive` the connector takes the valence token by direction. Drop `positive` for ranges such as min→max or a confidence span, which have no valence to color. ```tsx <Dumbbell data="[{ from: 62000, to: 84000 }]" title="Band move" style="{ width: 200, height: 28 }" /> ``` ## Install [#install] ```tsx import { Dumbbell } from "@microcharts/react/dumbbell"; <Dumbbell data={[{ from: 62000, to: 84000 }]} title="Band move" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Dumbbell } from "@microcharts/react/dumbbell/interactive"; <Dumbbell data={bands} /> ``` ## When to use it [#when-to-use-it] Use it for salary bands, before/after per table row, and ranges. When you have many categories and the crossings matter, use Slope; when the path between the two ends matters, use Sparkline. ## Sizing [#sizing] **table cell** ```tsx <Dumbbell data={[row.band]} width={60} height={12} /> ``` **a range, not a change** ```tsx // no positive prop — a min→max range has no valence to color <Dumbbell data={[{ from: p5, to: p95 }]} /> ``` ## Variants [#variants] ```tsx `" > <Dumbbell data="[ { label: "Paris", from: 52, to: 61 }, { label: "Oslo", from: 66, to: 60 }, ]" positive="up" title="Moves" style="{ width: 200, height: 44 }" /> ``` ```tsx `"> <Dumbbell data="[{ from: 40, to: 60 }]" label="value" domain="[0, 100]" title="With values" style="{ width: 240, height: 26 }" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <Dumbbell data="[{ from: 55, to: 55 }]" title="Flat" style="{ width: 200, height: 24 }" /> ``` When a row's `from` equals its `to`, the connector is dropped and one filled dot is drawn, rather than a hollow and a filled dot stacked on the same coordinate. The summary says **"No change at 55."** for a single row. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Berlin's band moved{" "} <span className="mc-inline"> <Dumbbell data={[{ from: 48, to: 68 }]} width={70} height={14} summary={false} /> </span>{" "} from €48k to €68k — up 42%. </p> ``` **In a table cell** ```tsx <td> <Dumbbell data={[{ label: "Berlin", from: 48, to: 68 }]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">€48k → €68k</span> <span className="unit">Berlin, up 42%</span> <Dumbbell data={bands} positive="up" width={130} height={52} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Offices <Dumbbell data={[{ from: 48, to: 68 }]} width={60} height={12} summary={false} /> </button> ``` ## Accessibility [#accessibility] A single row reads **"From 40 to 60, up 50%."**, a degenerate pair reads **"No change at 55."**, and a multi-row chart leads with the largest change (**"2 rows. Largest change Paris, up 17%."**). The interactive entry roves rows with ↑/↓ (←/→ do the same) and announces 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 draws a faint bundle of member paths with one emphasized representative, so you can see where the simulated futures disagree in **shape** and not only at the endpoint. Two ensembles can share an average and a final range while running completely different routes to get there, which is what a mean line hides. The emphasized path defaults to the real member closest to the pointwise median, so the representative is a path the simulation produced; `emphasis="median"` draws the synthetic pointwise median instead. Ghost selection is evenly spaced endpoint-rank quantiles, so the same input renders identically every time. ```tsx `" > <EnsembleGhosts data="[[40, 41, 43, 45, 47, 50], [40, 39, 38, 36, 35, 33], [40, 42, 44, 47, 49, 52], [40, 40, 41, 41, 42, 43], [40, 43, 47, 52, 56, 61], [40, 38, 35, 32, 30, 28], [40, 41, 42, 44, 45, 46], [40, 39, 40, 41, 42, 44]]" title="Simulated futures" width="200" height="44" /> ``` ## Install [#install] ```tsx import { EnsembleGhosts } from "@microcharts/react/ensemble-ghosts"; <EnsembleGhosts data={futures} title="Simulated futures" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { EnsembleGhosts } from "@microcharts/react/ensemble-ghosts/interactive"; <EnsembleGhosts data={futures} /> ``` ## When to use it [#when-to-use-it] Use it for a KPI card that shows the futures rather than the average, for Monte-Carlo and simulation output where paths disagree in shape, and for showing that outcomes fan out. When you need interval precision, use ForecastCone; for a single path, use Sparkline. ## Sizing [#sizing] **endpoint dots (countable spread)** ```tsx <EnsembleGhosts data={futures} endpoints /> ``` **synthetic median path** ```tsx <EnsembleGhosts data={futures} emphasis="median" /> ``` ## Variants [#variants] ```tsx <EnsembleGhosts data={[ [40, 41, 43, 45, 47, 50], [40, 39, 38, 36, 35, 33], [40, 42, 44, 47, 49, 52], [40, 43, 47, 52, 56, 61], [40, 38, 35, 32, 30, 28], ]} emphasis="median" />`" > <span className="inline-flex gap-4 items-center"> <EnsembleGhosts data="[ [40, 41, 43, 45, 47, 50], [40, 39, 38, 36, 35, 33], [40, 42, 44, 47, 49, 52], [40, 43, 47, 52, 56, 61], [40, 38, 35, 32, 30, 28], ]" width="150" height="40" /> <EnsembleGhosts data="[ [40, 41, 43, 45, 47, 50], [40, 39, 38, 36, 35, 33], [40, 42, 44, 47, 49, 52], [40, 43, 47, 52, 56, 61], [40, 38, 35, 32, 30, 28], ]" emphasis="median" width="150" height="40" /> </span> ``` `endpoints` marks where each ghost lands. `label="end"` is the default and states the emphasized path's landing value in a right gutter; `label="none"` drops it. ## Edge cases [#edge-cases] ```tsx `"> <EnsembleGhosts data="[[40, 43, 47, 52, 56, 61]]" title="One simulated path" width="160" height="40" /> ``` ```tsx `" > <EnsembleGhosts data="[ [4000, 4100, 4300, 4500, 4700, 5000], [4000, 3900, 3800, 3600, 3500, 3300], [4000, 4200, 4400, 4700, 4900, 5200], ]" locale="de-DE" title="Localized endpoints" width="160" height="40" /> ``` With one member there is no bundle to draw, so the chart renders that single path as the representative and the summary states its ending value rather than a spread. A member containing a non-finite value (`NaN`, `Infinity`) is excluded from ghost selection and from the median entirely, rather than gapped mid-path, and a dev-only console warning flags it: a partially invalid future usually means bad simulation output. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Simulated futures{" "} <span className="mc-inline"> <EnsembleGhosts data={futures} summary={false} /> </span>{" "} — 70% of paths finish above target. </p> ``` **In a table cell** ```tsx <td> <EnsembleGhosts data={futures} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">70%</span> <span className="unit">paths above target</span> <EnsembleGhosts data={futures} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Base <EnsembleGhosts data={futures} /> </button> ``` ## 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 static frame is not a hypothetical-outcome plot. The HOP loop, which flips through the members one at a time on hover, lives only in the interactive entry; with reduced motion, ←/→ step the members discretely instead, and each one 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) A checkout SLO has 62% of its error budget left on day 12 of 30. ErrorBudget plots that remaining line against the **steady-burn diagonal**, the pace that exactly spends the window, so "too fast" becomes a position: below the diagonal means you are outrunning the budget. Faster burn-rate reference lines sit below it as faint policy context. The 1×/6×/14.4× multipliers are the Google SRE Workbook's multiwindow burn-rate alert **convention** rather than physics, so `rates` is configurable and the lines never render data-colored. ```tsx <ErrorBudget data="[1, 0.96, 0.93, 0.9, 0.86, 0.83, 0.79, 0.75, 0.71, 0.67, 0.64, 0.62]" window="30" unit="day" label="remaining" title="Checkout SLO" width="260" height="30" /> ``` ## Install [#install] ```tsx import { ErrorBudget } from "@microcharts/react/error-budget"; <ErrorBudget data={remaining} window={30} title="Checkout SLO" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ErrorBudget } from "@microcharts/react/error-budget/interactive"; <ErrorBudget data={remaining} window={30} /> ``` ## When to use it [#when-to-use-it] Use it for an SLO error budget in a KPI card, a service list where each row is a budget, and spotting a fast burn before it exhausts the window. For a plain uptime series use Sparkline; for a one-number budget use Progress. ## Sizing [#sizing] **fast-burn exhausts the window** ```tsx <ErrorBudget data={[1, 0.82, 0.6, 0.38, 0.18, 0.04, 0]} window={20} /> ``` **diagonal only (quietest form)** ```tsx <ErrorBudget data={[1, 0.96, 0.93, 0.9, 0.86, 0.83, 0.79, 0.75, 0.71, 0.67, 0.64, 0.62]} rates={[1]} /> ``` ## Variants [#variants] ```tsx <ErrorBudget data={remaining} window={30} rates={[1]} />`" > <span className="inline-flex flex-col gap-1.5"> <ErrorBudget data="[1, 0.82, 0.6, 0.38, 0.18, 0.04, 0]" window="20" unit="day" title="Fast-burn" width="220" height="28" /> <ErrorBudget data="[1, 0.96, 0.93, 0.9, 0.86, 0.83, 0.79, 0.75, 0.71, 0.67, 0.64, 0.62]" window="30" rates="[1]" unit="day" title="Diagonal only" width="220" height="28" /> </span> ``` ```tsx `"> <ErrorBudget data="[1, 0.96, 0.93, 0.9, 0.86, 0.83, 0.79, 0.75, 0.71, 0.67, 0.64, 0.62]" window="30" unit="day" locale="de-DE" title="Localized" width="220" height="28" /> ``` `format` defaults to a percent formatter, and with `locale` it follows that locale's own conventions for the remaining-budget label: "62 %" with a space before the sign under `de-DE`, rather than the English "62%". ## Edge cases [#edge-cases] ```tsx `"> <ErrorBudget data="[]" title="No data yet" width="220" height="28" /> ``` ```tsx `"> <ErrorBudget data="[0.9]" window="30" unit="day" title="Day 1" width="220" height="28" /> ``` ```tsx `"> <ErrorBudget data="[1, 0.82, 0.6, 0.38, 0.18, 0.04, 0]" window="20" unit="day" title="Exhausted" width="220" height="28" /> ``` With no data the chart renders an empty root and the accessible name says **"No data."** A single point still draws the diagonal, the wedges, and the endpoint, though `currentRate` reads `0`, since it needs a prior step: it is the observed burn slope over the last `max(2, ⌈n/6⌉)` steps divided by the steady slope. 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 usual remaining-and-rate sentence. Values are clamped to `0–1`, so nothing draws below the floor or above a full budget. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout SLO budget{" "} <span className="mc-inline"> <ErrorBudget data={remaining} window={30} summary={false} /> </span>{" "} — 34% remaining, burn rate elevated. </p> ``` **In a table cell** ```tsx <td> <ErrorBudget data={remaining} window={30} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">34%</span> <span className="unit">remaining</span> <ErrorBudget data={remaining} window={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <ErrorBudget data={remaining} window={30} /> </button> ``` ## 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 budget 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 takes a job's `progress`, `elapsed` time, and observed `rate` and shows how long is left at that pace. Its axis is time rather than fraction: the solid part is the elapsed share of the predicted total, and the muted remainder is how long the rest will take at the current rate. When the rate drops, the remainder grows instead of marching to a fixed finish. The ETA label is on by default; `label="percent"` shows the fraction instead. ```tsx <EtaBar progress="0.64" elapsed="3.6" rate="0.18" etaFormat="(t) => `${Math.round(t)} min`" title="Export" width="280" height="16" /> ``` ## Install [#install] ```tsx import { EtaBar } from "@microcharts/react/eta-bar"; <EtaBar progress={0.64} elapsed={3.6} rate={0.18} etaFormat={(t) => `${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. ## Try it [#try-it] ```tsx import { EtaBar } from "@microcharts/react/eta-bar/interactive"; <EtaBar progress={0.64} elapsed={3.6} rate={0.18} etaFormat={(t) => `${Math.round(t)} min`} /> ``` ## When to use it [#when-to-use-it] Use it for download and export progress, job-queue ETAs, and deploy timers. For fraction-only progress use Progress; for unbounded counters use Delta. ## Sizing [#sizing] **table cell** ```tsx <EtaBar progress={0.64} elapsed={128} rate={0.5} width={60} height={8} /> ``` **stalled** ```tsx <EtaBar progress={0.3} elapsed={40} rate={0} /> ``` ## 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 toward a frame it hasn't seen. The transition lives in shared CSS (`.mc-eta-live rect`), so it inherits the shared `:where(.mc-root *) { transition: none }` reduced-motion block: under `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 `"> <EtaBar progress="0.42" elapsed="10" rate="0.05" label="percent" title="Percent" width="280" height="16" /> ``` ```tsx `" > <EtaBar progress="0.42" elapsed="10" rate="0.05" locale="de-DE" title="Localized ETA" width="280" height="16" /> ``` 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 you need. ## Edge cases [#edge-cases] ```tsx `"> <EtaBar progress="0.3" elapsed="40" rate="0" title="Stalled transfer" width="280" height="16" /> ``` ```tsx `" > <EtaBar progress="0.08" elapsed="30" rate="0.001" title="Barely started" width="280" height="16" /> ``` ```tsx `"> <EtaBar progress="1" elapsed="12" rate="0.1" title="Complete" width="280" height="16" /> ``` 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. That texture means unknown, 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`. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Export progress{" "} <span className="mc-inline"> <EtaBar progress={0.64} elapsed={3.6} rate={0.18} summary={false} /> </span>{" "} — 64% done, ~2 min remaining. </p> ``` **In a table cell** ```tsx <td> <EtaBar progress={0.64} elapsed={3.6} rate={0.18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">64%</span> <span className="unit">~2 min left</span> <EtaBar progress={0.64} elapsed={3.6} rate={0.18} /> </div> ``` **In a tab header** ```tsx <button className="tab"> report <EtaBar progress={0.64} elapsed={3.6} rate={0.18} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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 draws one tick per event and one lane per source, all on a shared time domain. A vertical band means those sources fired together, a diagonal means one triggered the next, and a sparse row means silence. Lanes stay aligned to that shared domain because the banding is the phenomenon the chart exists to show. One tick is always one event; the single exception is `overflow="bin"`, and the summary discloses it. ```tsx <EventRaster data="[ { label: "api", events: [2, 5, 6, 14, 20, 21, 33, 40, 41, 48, 55] }, { label: "db", events: [3, 6, 15, 21, 34, 41, 55] }, { label: "cache", events: [6, 21, 41, 55] }, { label: "queue", events: [10, 30, 50] }, ]" title="Service events" width="300" height="56" /> ``` ## Install [#install] ```tsx import { EventRaster } from "@microcharts/react/event-raster"; <EventRaster data={services} title="Service events" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { EventRaster } from "@microcharts/react/event-raster/interactive"; <EventRaster data={services} /> ``` ## When to use it [#when-to-use-it] Use it for service events across sources, agent steps, and cron or sensor triggers. For a single lane use RugStrip; for continuous rates use Sparkline. ## Sizing [#sizing] **table cell** ```tsx <EventRaster data={row.lanes} labels={false} width={80} height={24} /> ``` **emphasis** ```tsx <EventRaster data={services} emphasis="api" /> ``` ## Variants [#variants] ```tsx `"> <EventRaster data="[ { label: "api", events: [2, 5, 6, 14, 20, 21, 33, 40, 41, 48, 55] }, { label: "db", events: [3, 6, 15, 21, 34, 41, 55] }, { label: "cache", events: [6, 21, 41, 55] }, ]" emphasis="api" title="api emphasised" width="280" height="56" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <EventRaster data="[ { label: "api", events: [2, 5, 6, 14, 20, 33, 40, 48] }, { label: "db", events: [] }, ]" title="One quiet lane" width="220" height="32" /> ``` ```tsx i * 0.3) }]} overflow="bin" />`" > <EventRaster data="[{ label: "requests", events: Array.from({ length: 200 }, (_, i) => i * 0.3) }]" overflow="bin" title="High-rate lane" width="220" height="20" /> ``` ```tsx `"> <EventRaster data="[{ label: "boot", events: [0] }]" title="Single event" width="160" height="16" /> ``` An empty lane keeps its row, because a source that stopped firing is part of the reading. When a lane is dense enough that its ticks would alias into a solid smear, `overflow="bin"` (the default) switches that lane to per-bucket counts drawn as opacity, and the summary names the lanes that were binned. `overflow="clip"` opts out and keeps one tick per event at any density. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Service events this hour{" "} <span className="mc-inline"> <EventRaster data={services} summary={false} /> </span>{" "} — checkout-api busiest, 9 events. </p> ``` **In a table cell** ```tsx <td> <EventRaster data={services} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">9</span> <span className="unit">checkout-api/hr</span> <EventRaster data={services} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <EventRaster data={services} /> </button> ``` Best at KPI/card scale — multi-lane rasters need height per series. ## Accessibility [#accessibility] The accessible name summarizes 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) An API had a freeze window overnight, nine healthy hours, an incident at 11:00, and a release at 20:00. EventTimeline puts all of it 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. Duration is length on a linear time axis, never log or compressed time. ```tsx `" > <EventTimeline data="[ { start: Date.UTC(2026, 5, 3, 1), end: Date.UTC(2026, 5, 3, 5), label: "Freeze", kind: "accent" }, { start: Date.UTC(2026, 5, 3, 6), end: Date.UTC(2026, 5, 3, 15), label: "Healthy", kind: "positive" }, { start: Date.UTC(2026, 5, 3, 11), label: "Incident", kind: "negative" }, { start: Date.UTC(2026, 5, 3, 16), end: Date.UTC(2026, 5, 3, 18), kind: "negative" }, { start: Date.UTC(2026, 5, 3, 20), label: "Release" }, ]" domain="[Date.UTC(2026, 5, 3), Date.UTC(2026, 5, 4)]" title="API uptime" width="240" height="30" /> ``` ## Install [#install] ```tsx import { EventTimeline } from "@microcharts/react/event-timeline"; <EventTimeline data={windows} domain={today} title="API uptime" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { EventTimeline } from "@microcharts/react/event-timeline/interactive"; <EventTimeline data={windows} domain={today} /> ``` ## When to use it [#when-to-use-it] Use it for per-service uptime rows, and for on-call shifts and release windows in cards. Avoid it past roughly 12 items; for aggregated durations use a MiniBar of the totals. ## Sizing [#sizing] **uptime rows (shared window)** ```tsx {services.map((svc) => ( <EventTimeline key={svc.id} data={svc.windows} domain={today} title={svc.name} /> ))} ``` **with the current moment** ```tsx <EventTimeline data={windows} domain={today} now={Date.now()} /> ``` ## Variants [#variants] ```tsx `" > <EventTimeline data="[ { start: Date.UTC(2026, 5, 3, 6), end: Date.UTC(2026, 5, 3, 15), label: "Healthy", kind: "positive", }, { start: Date.UTC(2026, 5, 3, 11), label: "Incident", kind: "negative" }, ]" domain="[Date.UTC(2026, 5, 3), Date.UTC(2026, 5, 4)]" now="Date.UTC(2026, 5, 3, 21)" title="With now" width="240" height="30" /> ``` ```tsx `" > <EventTimeline data="[ { start: Date.UTC(2026, 5, 3, 6), end: Date.UTC(2026, 5, 3, 15), label: "Healthy", kind: "positive", }, { start: Date.UTC(2026, 5, 3, 16), end: Date.UTC(2026, 5, 3, 18), label: "Deploy", kind: "accent", }, ]" domain="[Date.UTC(2026, 5, 3), Date.UTC(2026, 5, 4)]" label="spans" width="280" height="26" title="Labeled spans" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <EventTimeline data="[ { start: Date.UTC(2026, 5, 3, 9), label: "Deploy" }, { start: Date.UTC(2026, 5, 3, 14), label: "Alert", kind: "negative" }, { start: Date.UTC(2026, 5, 3, 18), label: "Resolved", kind: "positive" }, ]" domain="[Date.UTC(2026, 5, 3), Date.UTC(2026, 5, 4)]" title="Today's events" width="220" height="16" /> ``` ```tsx `" > <EventTimeline data="[{ start: Date.UTC(2026, 5, 2, 20), end: Date.UTC(2026, 5, 3, 10), label: "Maintenance" }]" domain="[Date.UTC(2026, 5, 3), Date.UTC(2026, 5, 3, 6)]" title="Clipped window" width="220" height="16" /> ``` A span that crosses the domain boundary is cut flat at the edge rather than rescaled to fit, so you see the part inside the window and nothing more. An item that falls entirely outside the domain is excluded, with a dev-only console warning. Spans that overlap render translucent in data order; that translucency is a legibility device rather than an encoding, and the exact intervals are in the announcements. ## Four homes [#four-homes] **In a sentence** ```tsx <p> API status today{" "} <span className="mc-inline"> <EventTimeline data={windows} domain={today} width={90} height={14} summary={false} /> </span> {" "} — 3 spans covering 63% of the window. </p> ``` **In a table cell** ```tsx <td> <EventTimeline data={windows} domain={today} width={90} height={14} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">63%</span> <span className="unit">3 spans, 1 incident</span> <EventTimeline data={windows} domain={today} width={200} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> API <EventTimeline data={windows} domain={today} width={54} height={14} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the coverage read: **"2 spans covering 46% of the window; 0 events."** 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 renders the exact number and maps its magnitude to font *weight*: five ordinal tiers across your `domain`, or three with `tiers={3}`. The weight is a redundant second channel, so the large values in a dense column pop preattentively before you read any of them. Five is the most weight steps that stay discriminable at text size. Weight is never the primary read, since it is ordinal and coarse and the numeral is right there. Always pass a `domain`: without one a lone number has no tier to sit in and renders at the middle weight. ```tsx <span style="{ display: "inline-flex", flexDirection: "column", alignItems: "flex-end", gap: 2 }" className="tabular-nums"> <FatDigits value="1204" domain="[0, 2100]" summary="false" fontSize="16" /> <FatDigits value="318" domain="[0, 2100]" summary="false" fontSize="16" /> <FatDigits value="2100" domain="[0, 2100]" summary="false" fontSize="16" /> <FatDigits value="76" domain="[0, 2100]" summary="false" fontSize="16" /> </span> ``` ## Install [#install] ```tsx import { FatDigits } from "@microcharts/react/fat-digits"; <FatDigits value={1204} domain={[0, 2100]} title="Revenue" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { FatDigits } from "@microcharts/react/fat-digits/interactive"; <FatDigits value={1204} domain={[0, 2100]} /> ``` ## When to use it [#when-to-use-it] Use it 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. For trends use Sparkline, for proportions Progress, and for comparisons MiniBar. ## Sizing [#sizing] **a scannable table column (the hero)** ```tsx {rows.map((v) => <FatDigits value={v} domain={[0, 2100]} />)} ``` **digit mode weights each digit by its own magnitude** ```tsx <FatDigits value={1902} encode="digit" /> ``` ## Variants [#variants] ```tsx <FatDigits value={1204} domain={[0, 2100]} tiers={3} />`" > <span className="inline-flex gap-5 items-center"> <FatDigits value="1902" encode="digit" fontSize="18" /> <FatDigits value="1204" domain="[0, 2100]" tiers="3" fontSize="18" /> </span> ``` `encode="digit"` weights each digit by its own magnitude instead, a redundancy that helps you scan long ids and amounts. The idea is adapted from FatFonts, which encodes magnitude as glyph ink area using a custom font; shipping a font would add a dependency, so weight tiers on the inherited font carry the ordinal here. `format` also takes `Intl.NumberFormatOptions`, and the numeral follows the locale's own grouping and decimal marks. ```tsx `" > <FatDigits value="12040" domain="[0, 20000]" format="{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }" locale="de-DE" fontSize="18" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <FatDigits value="1204" fontSize="18" /> ``` ```tsx `"> <FatDigits value="Number.NaN" domain="[0, 2100]" fontSize="18" title="Missing" /> ``` A non-finite value renders nothing, neither numeral nor weight, and reports **"No data."** rather than guessing a tier. On a font without many weights the browser snaps to the nearest available face, so fewer of the tiers stay visually distinct. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Revenue column scan{" "} <span className="mc-inline"> <FatDigits value={2100} domain={[0, 2100]} summary={false} /> </span>{" "} — Acme at 2.1M stands out. </p> ``` **In a table cell** ```tsx <td> <FatDigits value={row.value} domain={[0, 2100]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">2.1M</span> <FatDigits value={2100} domain={[0, 2100]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Acme <FatDigits value={row.value} domain={[0, 2100]} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the exact value and its tier: **"1,204 — tier 3 of 5."** In digit mode it is the number alone, 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). | | `fontSize` | `number` | Numeral type size in viewBox units (default 14) — here the numeral is the mark, so this sizes the chart. | | `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 makes the label the bar, so a named task shows its own progress: the word `uploading` renders with 62% of its glyph ink in accent and the rest muted. 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% bisects the word. The fill is a percentage of the glyphs you see rather than of a hidden wider track. Glyph ink is uneven, so fine reads are ±5%: the fill edge lands between letters, not on an exact tick. Add `label="value"` for the percent alongside, or use Progress when precision is the point. ```tsx <FillWord word="uploading" value="0.62" title="Upload" fontSize="18" /> ``` ## Install [#install] ```tsx import { FillWord } from "@microcharts/react/fill-word"; <FillWord word="uploading" value={0.62} /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { FillWord } from "@microcharts/react/fill-word/interactive"; <FillWord word="uploading" value={0.62} /> ``` ## When to use it [#when-to-use-it] Use it for a labeled progress read in a sentence or cell, a sync or upload status where the label names the task, or a quota or TTL where the word is the metric. For precise percentages use Progress, for trends Sparkline, and for many parallel bars MiniBar. ## Sizing [#sizing] **drain mode for a remaining-time story** ```tsx <FillWord word="expiring" value={0.7} mode="drain" /> ``` **show the exact percent alongside** ```tsx <FillWord word="storage" value={0.4} label="value" /> ``` ## Variants [#variants] ```tsx <FillWord word="storage" value={0.4} label="value" />`" > <span className="inline-flex gap-5 items-center"> <FillWord word="expiring" value="0.7" mode="drain" fontSize="16" /> <FillWord word="storage" value="0.4" label="value" fontSize="16" /> </span> ``` Fill is the default mode, since a word filling as a step completes covers most uses. `mode="drain"` empties the ink from the left as the value rises, which is the remaining-time read fill can't give you: TTLs, expiring sessions. ## Edge cases [#edge-cases] ```tsx <FillWord word="uploading" value={1} />`" > <span className="inline-flex gap-5 items-center"> <FillWord word="uploading" value="0" fontSize="16" title="0%" /> <FillWord word="uploading" value="1" fontSize="16" title="100%" /> </span> ``` At 0% the word is entirely the muted track; at 100% it is entirely accent ink. Both ends are limits of the same clip rather than special cases in the code. ```tsx `"> <FillWord value="0.5" fontSize="16" title="Empty" /> ``` An empty `word` renders nothing and reports **"No data."**, since there is no label to be the bar. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Upload progress{" "} <span className="mc-inline"> <FillWord word="loading" value={0.62} summary={false} /> </span>{" "} — 62% of the file transferred. </p> ``` **In a table cell** ```tsx <td> <FillWord word="loading" value={0.62} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">62%</span> <span className="unit">complete</span> <FillWord word="loading" value={0.62} /> </div> ``` **In a tab header** ```tsx <button className="tab"> avatar <FillWord word="loading" value={0.62} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives the task and its state: **"storage: 40% complete."** In drain mode, where `value` is the fraction already drained, `value={0.7}` reads **"expiring: 30% remaining."** 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 doesn't spam a 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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. | | `fontSize` | `number` | Word type size in viewBox units (default 12) — here the word is the mark, so this sizes the chart. | | `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 folds many days (or weeks, or any cycle) onto a single period axis and draws the median with 25–75 and 5–95 percentile envelopes — the typical-period grammar clinical glucose profiles standardized on. The envelopes are per-bin quantiles, never smoothed across bins, and the outer boundary fades so 5–95 doesn't read as a hard limit. Pass a `today` overlay to see whether the current period sits inside the usual band. ```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), })); <FoldedDayBand data={observations} today={today} title="Typical day" />`" > <FoldedDayBand data="Array.from({ length: 14 }, (_d, d) => Array.from({ length: 24 }, (_h, h) => ({ t: d * 24 + h, value: Math.round(40 + 42 * Math.max(0, 1 - Math.abs(h - 14) / 10) + Math.sin(d + h) * 8) }))).flat()" today="Array.from({ length: 24 }, (_h, h) => ({ t: h, value: Math.round(40 + 42 * Math.max(0, 1 - Math.abs(h - 14) / 10) + 14) }))" title="Typical day" width="300" height="40" /> ``` ## Install [#install] ```tsx import { FoldedDayBand } from "@microcharts/react/folded-day-band"; <FoldedDayBand data={observations} today={today} title="Typical day" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { FoldedDayBand } from "@microcharts/react/folded-day-band/interactive"; <FoldedDayBand data={observations} today={today} /> ``` ## When to use it [#when-to-use-it] Use it for typical-day traffic or load profiles, and for on-call or energy capacity planning. For a raw time series use Sparkline; a single period has nothing to fold. ## Sizing [#sizing] **on-call cell** ```tsx <FoldedDayBand data={observations} width={80} height={20} /> ``` **now vs typical** ```tsx <FoldedDayBand data={observations} today={today} /> ``` ## Variants [#variants] ```tsx <FoldedDayBand data={observations} format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }} locale="de-DE" />`" > <span className="inline-flex flex-col gap-3"> <FoldedDayBand data="Array.from({ length: 14 }, (_d, d) => Array.from({ length: 24 }, (_h, h) => ({ t: d * 24 + h, value: Math.round(40 + 42 * Math.max(0, 1 - Math.abs(h - 14) / 10) + Math.sin(d + h) * 8), })), ).flat()" percentiles="[[25, 75]]" title="Middle half only" width="300" height="40" /> <FoldedDayBand data="Array.from({ length: 14 }, (_d, d) => Array.from({ length: 24 }, (_h, h) => ({ t: d * 24 + h, value: Math.round(40 + 42 * Math.max(0, 1 - Math.abs(h - 14) / 10) + Math.sin(d + h) * 8), })), ).flat()" format="{ minimumFractionDigits: 1, maximumFractionDigits: 1 }" locale="de-DE" title="Localized peak" width="220" height="32" /> </span> ``` `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 `"> <FoldedDayBand data="[{ t: 3, value: 10 }]" title="Single observation" width="160" height="32" /> ``` ```tsx ({ t: i, value: 7 }))} />`" > <FoldedDayBand data="Array.from({ length: 48 }, (_v, i) => ({ t: i, value: 7 }))" title="Flat" width="200" height="32" /> ``` 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. Identical values across every bin still render as a flat line, distinct from an empty `data` array, which renders nothing and announces "No data." When a bin holds too few observations the band collapses to the median there rather than inventing a width. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Traffic vs typical day{" "} <span className="mc-inline"> <FoldedDayBand data={observations} summary={false} /> </span>{" "} — afternoon peak normal; tonight runs hot. </p> ``` **In a table cell** ```tsx <td> <FoldedDayBand data={observations} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+14</span> <span className="unit">above median at 2pm</span> <FoldedDayBand data={observations} /> </div> ``` **In a tab header** ```tsx <button className="tab"> US-East <FoldedDayBand data={observations} /> </button> ``` ## 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) Q4 revenue is at 38 and the target is 45: ForecastCone shows the spread of where it lands. History is a solid line, and the forecast is a fan of prediction bands that **widen over the horizon** under a **dashed** median. Three of those choices are fixed rather than style options. At most two bands, 50 and 80, because a 95% band reads as false tail confidence at micro scale. The median is always dashed, so an estimate never renders as fact. And the cone must visibly widen: bands that narrow or stay flat are flagged, never quietly inflated. ```tsx <ForecastCone data="[30, 32, 31, 34, 36, 35, 38]" forecast="{ mid: [39, 40, 41, 42], p80: [ [36, 42], [35, 45], [34, 50], [33, 55], ], p50: [ [37, 41], [37, 43], [36, 46], [35, 49], ], }" target="45" label="landing" title="Q4 revenue" width="260" height="28" /> ``` ## Install [#install] ```tsx import { ForecastCone } from "@microcharts/react/forecast-cone"; <ForecastCone data={history} forecast={forecast} target={45} title="Q4 revenue" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ForecastCone } from "@microcharts/react/forecast-cone/interactive"; <ForecastCone data={history} forecast={forecast} target={45} /> ``` ## When to use it [#when-to-use-it] Use it for a "will we hit Q4?" forecast in a KPI card, a projection with its uncertainty inside a sentence, and band-vs-target landing reads. For a forecast with no uncertainty use Sparkline; for one estimate's spread, GradedBand. ## Sizing [#sizing] **band vs target** ```tsx <ForecastCone data={history} forecast={forecast} target={45} /> ``` **single band (tightest form)** ```tsx <ForecastCone data={history} forecast={{ mid, p80 }} /> ``` ## Variants [#variants] ```tsx `" > <span className="inline-flex flex-col gap-1.5"> <ForecastCone data="[30, 32, 31, 34, 36, 35, 38]" forecast="{ mid: [39, 40, 41, 42], p80: [ [36, 42], [35, 45], [34, 50], [33, 55], ], }" title="Single band" width="220" height="26" /> <ForecastCone data="[30, 32, 31, 34, 36, 35, 38]" forecast="{ mid: [39, 40, 41, 42], p80: [ [36, 42], [35, 45], [34, 50], [33, 55], ], p50: [ [37, 41], [37, 43], [36, 46], [35, 49], ], }" target="50" title="Vs a higher target" width="220" height="26" /> </span> ``` ```tsx `" > <ForecastCone data="[3000, 3200, 3100, 3400, 3600, 3500, 3800]" forecast="{ mid: [3900, 4000, 4100, 4200], p80: [ [3600, 4200], [3500, 4500], [3400, 5000], [3300, 5500], ], }" format="{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }" locale="de-DE" title="Localized forecast" width="220" height="26" /> ``` `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 `" > <ForecastCone data="[]" forecast="{ mid: [10, 12, 14], p80: [ [8, 12], [8, 15], [7, 19], ], }" title="No history" width="160" height="26" /> ``` ```tsx `" > <ForecastCone data="[10, 11]" forecast="{ mid: [12, 13, 14], p80: [ [9, 15], [10, 14], [11, 13], ], }" title="Narrowing input" width="160" height="26" /> ``` With no history the cone still draws from the first forecast point, and the accessible summary 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 error: uncertainty that doesn't grow with the horizon misrepresents confidence decay. It renders exactly as supplied, never auto-inflated, and logs a one-time dev warning so the mistake surfaces in development. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Q4 revenue forecast{" "} <span className="mc-inline"> <ForecastCone data={history} forecast={forecast} summary={false} /> </span>{" "} — median path clears target by week 3. </p> ``` **In a table cell** ```tsx <td> <ForecastCone data={history} forecast={forecast} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">112%</span> <span className="unit">of target (median)</span> <ForecastCone data={history} forecast={forecast} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Q4 <ForecastCone data={history} forecast={forecast} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the median, the horizon interval, and today's actual. With a `target` it adds 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 prints one stepped rectangle per stage — 12,400 visitors, 5,704 signups, 2,730 activated, 1,116 paid — each anchored at zero, so the drop *between* neighbors is the read: the biggest one is where the pipeline leaks. It never draws the classic tapered silhouette, which interpolates data that doesn't exist. ```tsx <Funnel data="[ { label: "Visitors", value: 12400 }, { label: "Signups", value: 5704 }, { label: "Activated", value: 2730 }, { label: "Paid", value: 1116 }, ]" title="Signup funnel" width="220" height="66" /> ``` ## Install [#install] ```tsx import { Funnel } from "@microcharts/react/funnel"; <Funnel data={stages} title="Signup funnel" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Funnel } from "@microcharts/react/funnel/interactive"; <Funnel data={stages} /> ``` ## When to use it [#when-to-use-it] Use it for per-campaign funnels in table cells and conversion bridges in cards. For unordered categories use MiniBar, and keep a funnel to 6 stages or fewer. ## Sizing [#sizing] **table cell** ```tsx <Funnel data={campaign.stages} width={60} height={18} /> ``` **the leak** ```tsx <Funnel data={stages} highlight="Activated" /> ``` ## Variants [#variants] ```tsx `" > <Funnel data="[ { label: "Visitors", value: 12400 }, { label: "Signups", value: 5704 }, { label: "Paid", value: 1116 }, ]" mode="rate" title="Rate" width="220" height="66" /> ``` `mode="rate"` always normalizes to stage 1, never to the previous stage: per-stage rates hide compounding loss. ```tsx `" > <Funnel data="[ { label: "Visitors", value: 12400 }, { label: "Signups", value: 5704 }, { label: "Activated", value: 2730 }, { label: "Paid", value: 1116 }, ]" highlight="Activated" title="The leak" width="220" height="66" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <Funnel data="[{ label: "Visitors", value: 8000 }]" title="Single stage" width="120" height="40" /> ``` ```tsx `" > <Funnel data="[ { label: "Trial", value: 900 }, { label: "Churned", value: 420 }, { label: "Win-back", value: 610 }, ]" title="Re-engagement funnel" width="220" height="56" /> ``` A single stage renders as one full-height column with no connector slats, at an overall 100%. A stage larger than its predecessor renders at its true size, as re-engagement funnels do, and the summary appends the inversion. ## Four homes [#four-homes] **In a sentence** ```tsx <p> This week's signup funnel{" "} <span className="mc-inline"> <Funnel data={stages} width={64} height={20} summary={false} /> </span>{" "} converted 12,400 visitors to 1,116 paid — a 9% overall rate. </p> ``` **In a table cell** ```tsx <td> <Funnel data={campaign.stages} width={60} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">9%</span> <span className="unit">12,400 → 1,116</span> <Funnel data={stages} highlight="Activated" width={90} height={28} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Organic <Funnel data={stages} width={64} height={16} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the span and the overall rate: **"4 stages, 12,400 to 1,116 — overall 9%."** It appends **"Stage 3 exceeds stage 2."** when a pipeline inverts. The interactive entry roves stages, announcing each as **"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 shows the rhythm of activity over time the way ActivityGrid does, in a single ink. 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 linear radius map would exaggerate the highs quadratically. A zero cell is a hairline ring, present but quiet, and a `null` cell is nothing at all, since print and grayscale otherwise lose the zero-versus-missing distinction color grids get for free. The steps are ordinal rather than values: `ActivityGrid` is the color twin when exact values matter. ```tsx <GardenGrid data="[12, 20, 8, 0, 15, 28, 34, 5, 0, 22, 18, 9, 3, 0, 24, 30, 11, 6, 19, 0, 26]" unit="weeks" title="Activity" cell="11" /> ``` ## Install [#install] ```tsx import { GardenGrid } from "@microcharts/react/garden-grid"; <GardenGrid data={weeks} title="Activity" unit="weeks" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { GardenGrid } from "@microcharts/react/garden-grid/interactive"; <GardenGrid data={weeks} /> ``` ## When to use it [#when-to-use-it] Use it 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. For exact per-cell values use ActivityGrid with hover or HeatStrip; for trends, Sparkline. ## Sizing [#sizing] **strip mode for a table cell** ```tsx const weeks = [12, 20, 8, 0, 15, 28, 34, 5, 0, 22, 18, 9]; <GardenGrid data={weeks} rows={1} /> ``` **empty='blank' for sparse data** ```tsx const weeks = [12,20,8,0,15,28,34,5,0,22,18,9,3,0,24,30,11,6,19,0,26]; <GardenGrid data={weeks} empty="blank" /> ``` ## Variants [#variants] ```tsx <GardenGrid data={weeks} empty="blank" />`" > <span className="inline-flex flex-col gap-3"> <GardenGrid data="[12, 20, 8, 0, 15, 28, 34, 5, 0, 22, 18, 9]" rows="1" cell="11" /> <GardenGrid data="[12, 20, 8, 0, 15, 28, 34, 5, 0, 22, 18, 9, 3, 0]" empty="blank" cell="11" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <GardenGrid data="[]" title="No history" cell="11" /> ``` ```tsx `"> <GardenGrid data="[0, 0, 0, 0, 0, 0, 0]" title="Quiet week" cell="11" /> ``` ```tsx `"> <GardenGrid data="[5, null, 8, null, null, 3]" title="Gaps" cell="11" /> ``` ```tsx `"> <GardenGrid data="[12]" title="One" cell="11" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Contributions this quarter{" "} <span className="mc-inline"> <GardenGrid data={weeks} rows={1} summary={false} /> </span>{" "} — busy mid-month, quiet weeks 4 and 10. </p> ``` **In a table cell** ```tsx <td> <GardenGrid data={weeks} rows={1} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">34</span> <span className="unit">peak week</span> <GardenGrid data={weeks} /> </div> ``` **In a tab header** ```tsx <button className="tab"> microcharts <GardenGrid data={weeks} rows={1} /> </button> ``` Best at KPI/card scale — activity grids need room for cell area. ## 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."** — because dot area reads to a step rather than 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"). | | `cell` | `number` | Cell edge length in viewBox units (default 10). | | `gap` | `number` | Gap between cells in viewBox units (default 2). | | `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) A stage climbs 67 m over 900, and the wall sits at 800. GradeProfile shows where. Each segment between two points is filled from the baseline and colored by its grade, from gentle to brutal, with the elevation ridge riding on top, and the steepest pitch is labeled so the hardest moment isn't buried. **How to read it** — color is a *quantized* grade bin, never a smooth ramp: flats and every descent take the faint band, then a mid tone, the negative color, and the darkest fill for the brutal pitches. The default `bins` of `[3, 6, 10]` percent match how climbs are described, so a route reads as gentle, rolling, steep, or wall. Height is the elevation ridge for shape; the encoded channel is the color, and the interactive readout gives the exact grade. Below 72 pixels wide the four bins collapse to a flat-versus-climb read, because the finer classes stop being separable there. ```tsx <GradeProfile data="[ { d: 0, elev: 800 }, { d: 100, elev: 809 }, { d: 250, elev: 812 }, { d: 350, elev: 817 }, { d: 500, elev: 835 }, { d: 700, elev: 833 }, { d: 900, elev: 865 }, ]" format="{ style: "unit", unit: "meter", unitDisplay: "short" }" title="Queen stage" width="240" height="48" /> ``` `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"; <GradeProfile data={trail} format={(n) => `${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. ## Try it [#try-it] ```tsx import { GradeProfile } from "@microcharts/react/grade-profile/interactive"; <GradeProfile data={trail} /> ``` ## When to use it [#when-to-use-it] Use it for route and climb profiles in cycling, running, and hiking, and to show *where* the hard pitches fall. For a single elevation series use Sparkline; distance must be monotonic, so an unordered track won't work. ## Sizing [#sizing] **stage cell** ```tsx <GradeProfile data={stage.profile} label="none" width={80} height={24} /> ``` **call out the wall** ```tsx <GradeProfile data={trail} bins={[4, 8, 12]} /> ``` ## Variants [#variants] ```tsx `" > <GradeProfile data="[ { d: 0, elev: 800 }, { d: 250, elev: 812 }, { d: 500, elev: 835 }, { d: 700, elev: 833 }, { d: 900, elev: 865 }, ]" label="none" title="No label" width="240" height="44" /> ``` ```tsx `" > <GradeProfile data="[ { d: 0, elev: 800 }, { d: 100, elev: 809 }, { d: 350, elev: 817 }, { d: 500, elev: 835 }, { d: 900, elev: 865 }, ]" bins="[5, 8, 12]" format="{ style: "unit", unit: "meter", unitDisplay: "short" }" title="Custom bins" width="240" height="44" /> ``` ```tsx `" > <GradeProfile data="[ { d: 0, elev: 600 }, { d: 2000, elev: 780 }, { d: 5000, elev: 1150 }, { d: 8000, elev: 1400 }, { d: 12000, elev: 1850 }, ]" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Alpine stage" width="240" height="48" /> ``` `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" />`" > <GradeProfile data="[ { d: 0, elev: 900 }, { d: 300, elev: 840 }, { d: 600, elev: 780 }, ]" format="{ style: "unit", unit: "meter", unitDisplay: "short" }" title="Descent" width="200" height="44" /> ``` ```tsx `" > <GradeProfile data="[ { d: 0, elev: 100 }, { d: 100, elev: 120 }, { d: 200, elev: NaN }, { d: 300, elev: 160 }, ]" title="Partial track" width="200" height="44" /> ``` A descent-only route has no climb to grade, so no pitch is emphasized and the summary reports **"600 m, no real climb."** Descents always take the gentlest color rather than borrowing a climb's, because climbing difficulty is what the chart grades. 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."** ## Four homes [#four-homes] **In a sentence** ```tsx <p> Queen stage elevation{" "} <span className="mc-inline"> <GradeProfile data={trail} summary={false} /> </span>{" "} — 865 m gain over 800 km. </p> ``` **In a table cell** ```tsx <td> <GradeProfile data={trail} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">865 m</span> <span className="unit">total gain</span> <GradeProfile data={trail} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Stage 12 <GradeProfile data={trail} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives the distance, the total climb, and where the hardest pitch falls: **"900, 67 gain; steepest 16% at 800."** None of it depends on color. The interactive entry announces each pitch as you rove it (`←`/`→`), with the true grade and the cumulative climb. 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 prints one number's uncertainty as nested central intervals, graded by opacity, with a median tick. Opacity maps to probability level and nothing else. It is **never** a bar from zero: a bar with an error whisker induces edge-literalism bias, where the bar's end reads as the answer and the whisker as noise, and a graded band has no endpoint to over-read. Inner intervals are clipped inside their outer, so quantile rounding can never invert the nesting. ```tsx <GradedBand data="Array.from({ length: 160 }, (_, i) => 21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3)))" label="median" title="Forecast estimate" width="240" height="16" /> ``` ## Install [#install] ```tsx import { GradedBand } from "@microcharts/react/graded-band"; <GradedBand data={posterior} label="median" title="Forecast estimate" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { GradedBand } from "@microcharts/react/graded-band/interactive"; <GradedBand data={posterior} label="median" /> ``` ## When to use it [#when-to-use-it] Use it for a forecast with its uncertainty, estimate-vs-actual in a KPI card, and posterior summaries. For countable odds use QuantileDots; for a forecast over time, ForecastCone. ## Sizing [#sizing] **estimate vs actual** ```tsx // the dot is a distinct shape from the median tick <GradedBand data={posterior} value={28} /> ``` **soft edge = approximate** ```tsx <GradedBand data={posterior} softEdge /> ``` ## Variants [#variants] ```tsx 21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3)), ); <GradedBand data={posterior} value={28} /> <GradedBand data={posterior} softEdge />`" > <span className="inline-flex flex-col gap-1.5"> <GradedBand data="Array.from( { length: 160 }, (_, i) => 21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3)), )" value="28" title="Estimate vs actual" width="200" height="14" /> <GradedBand data="Array.from( { length: 160 }, (_, i) => 21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3)), )" title="Soft edge" width="200" height="14" /> </span> ``` ```tsx (21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3))) / 10, ); <GradedBand data={drift} label="median" locale="de-DE" />`" > <GradedBand data="Array.from( { length: 160 }, (_, i) => (21 + Math.round(9 * Math.sin(i) + 6 * Math.sin(i * 2.3))) / 10, )" label="median" locale="de-DE" title="Localized median" width="200" height="14" /> ``` 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 `" > <GradedBand data="[42, 42, 42, 42, 42, 42]" title="No spread" width="200" height="14" /> ``` The accessible name for that strip is **"Point value 42, no interval."** A degenerate sample is announced as a point estimate rather than as certainty about a range. ```tsx `" > <GradedBand data="[19, 20, 21, 21, 22, 22, 23, 23, 24, 26]" value="48" title="Forecast miss" width="200" height="14" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Forecast estimate{" "} <span className="mc-inline"> <GradedBand data={posterior} summary={false} /> </span>{" "} — 50% band spans 17–21, point at 19. </p> ``` **In a table cell** ```tsx <td> <GradedBand data={posterior} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">19</span> <span className="unit">median estimate</span> <GradedBand data={posterior} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Revenue <GradedBand data={posterior} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the median with the innermost and outermost intervals. For the forecast above that is **"Median 22; 50% within 15–27, 95% within 8–36."** The interactive entry roves the band edges, announcing 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 draws one ECG-style spike per event in a rolling window, 60 seconds by default, because liveness questions are about the last minute. 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 window leaves a flat baseline: the down signal is carried by shape, not color. Pass `now` from your data layer. The static entry never calls `Date.now()`, because a server render and the client hydrate would disagree and mismatch. ```tsx <HeartbeatBlip events="[97000, 92000, 85000, 70000, 55000, 48000]" now="100000" title="Requests" width="90" /> ``` ## Install [#install] ```tsx import { HeartbeatBlip } from "@microcharts/react/heartbeat-blip"; // pass 'now' from your data layer — never Date.now() in a server render <HeartbeatBlip events={eventTimestamps} now={serverNow} title="Requests" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { HeartbeatBlip } from "@microcharts/react/heartbeat-blip/interactive"; <HeartbeatBlip events={eventTimestamps} now={serverNow} /> ``` ## 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. This is the deliberate idle-loop exception, allowed because the loop parameter (elapsed time) is the datum. It pauses off-screen through a shared observer, and under `prefers-reduced-motion` it does not sweep at all: the static strip re-renders on each data change instead, carrying the same information discretely. **A flat service never gets a fake pulse.** When the window empties, the baseline goes flat and stays flat. ## When to use it [#when-to-use-it] Use it for the liveness of a service or stream, a request rate in a header, or per-service liveness in a status table. For exact event counts use `Seismogram` or `EventTimeline`, for a continuous level `BreathingDot`, and for 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 <HeartbeatBlip events={[]} now={serverNow} />`" > <span className="inline-flex gap-6 items-center"> <HeartbeatBlip events="[97000, 92000, 85000, 70000, 55000, 48000]" now="100000" label="count" width="90" summary="false" /> <HeartbeatBlip events="[]" now="100000" width="90" summary="false" /> </span> ``` ```tsx `" > <HeartbeatBlip events="[97000, 92000, 85000]" now="100000" label="count" format="{ useGrouping: false }" locale="de-DE" title="Requests" width="90" /> ``` `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 `"> <HeartbeatBlip events="[]" now="100000" title="Requests" width="90" /> ``` ```tsx `"> <HeartbeatBlip events="[97000, Number.NaN, 92000]" now="100000" title="Requests" width="90" /> ``` ```tsx `" > <HeartbeatBlip events="[95000, 10000]" now="100000" title="Requests" width="90" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Request liveness{" "} <span className="mc-inline"> <HeartbeatBlip events={eventTimestamps} now={serverNow} width={90} height={16} summary={false} /> </span>{" "} — six events in the last minute. </p> ``` **In a table cell** ```tsx <td><HeartbeatBlip events={svc.events} now={serverNow} width={72} height={16} /></td> ``` **In a KPI card** ```tsx <div className="kpi"><span className="figure">6</span><HeartbeatBlip events={eventTimestamps} now={serverNow} width={200} height={30} /></div> ``` **In a tab header** ```tsx <button className="tab">api <HeartbeatBlip events={eventTimestamps} now={serverNow} width={44} height={14} /></button> ``` ## 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. Down and no-data are different states, and the summary distinguishes them. The trace sweeps only when motion is allowed and the chart is on-screen. The live region announces on data change, and there is no per-spike navigation, because the spikes are transient and 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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. | | `fontSize` | `number` | Type size of the count numeral, in viewBox units. Defaults from `height`. | 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) You lay out the grid — a matrix of tenants × hours, a custom calendar, a density chip in a sentence — and HeatCell is the single cell inside it, showing how intense one value is against a known scale. Color is quantized into five discrete steps, the most a reader reliably distinguishes at cell size and the same ramp ActivityGrid uses, so intensity means one thing across the library. Continuous opacity would fake precision a 12-px cell can't deliver. ```tsx <span className="inline-flex items-center gap-1.5"> {[12, 35, 58, 79, 96].map((v) => ( <HeatCell key={v} value={v} domain={[0, 100]} title={`Load ${v}`} style={{ width: 18, height: 18 }} /> ))} </span> ``` ## Install [#install] ```tsx import { HeatCell } from "@microcharts/react/heat-cell"; <HeatCell value={42} domain={[0, 100]} title="Load" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { HeatCell } from "@microcharts/react/heat-cell/interactive"; <HeatCell value={42} domain={[0, 100]} /> ``` ## When to use it [#when-to-use-it] Use it for table-cell matrices, custom grids, and intensity chips beside labels. For precise comparison use MiniBar or DotPlot; for a time series, HeatStrip or 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) => <HeatCell value={v} domain={domain} />)); ``` ## Sizing [#sizing] **a shared-domain row** ```tsx // every cell calibrates against ONE domain — never per-cell auto-scale {[12, 40, 62, 88].map((v) => ( <HeatCell key={v} value={v} domain={[0, 100]} /> ))} ``` **value chip** ```tsx // wider cells can carry their number <HeatCell value={8} domain={[0, 9]} label="value" style={{ width: 28, height: 28 }} /> ``` ## Variants [#variants] ```tsx <HeatCell value={70} domain={[0, 100]} shape="round" /> <HeatCell value={70} domain={[0, 100]} shape="dot" />`" > <span className="inline-flex items-center gap-1.5"> <HeatCell value="70" domain="[0, 100]" title="Square" style="{ width: 18, height: 18 }" /> <HeatCell value="70" domain="[0, 100]" shape="round" title="Round" style="{ width: 18, height: 18 }" /> <HeatCell value="70" domain="[0, 100]" shape="dot" title="Dot" style="{ width: 18, height: 18 }" /> </span> ``` ```tsx `"> <HeatCell value="8" domain="[0, 9]" label="value" title="Intensity" style="{ width: 28, height: 28 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <HeatCell value="Number.NaN" domain="[0, 100]" title="No data" style="{ width: 18, height: 18 }" /> ``` ```tsx <HeatCell value={140} domain={[0, 100]} />`" > <span className="inline-flex items-center gap-1.5"> <HeatCell value="-20" domain="[0, 100]" title="Below floor" style="{ width: 18, height: 18 }" /> <HeatCell value="140" domain="[0, 100]" title="Above ceiling" style="{ width: 18, height: 18 }" /> </span> ``` Values outside the `domain` clamp to the end steps and are never silently rescaled, so one out-of-range cell can't restretch a grid. A zero-width domain renders the single mid step and logs a dev warning. ## Four homes [#four-homes] **In a sentence** ```tsx <p> shard-a load just crossed{" "} <span className="mc-inline"> <HeatCell value={76} domain={[0, 100]} summary={false} /> </span>{" "} 76% — level 4 of 5. </p> ``` **In a table cell** ```tsx // every cell in the grid shares ONE domain <tr> <td>shard-a</td> {[18, 42, 76, 55].map((v) => ( <td key={v}><HeatCell value={v} domain={[0, 100]} /></td> ))} </tr> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">76%</span> <span className="unit">peak this cycle</span> {[18, 42, 76, 55].map((v) => ( <HeatCell key={v} value={v} domain={[0, 100]} /> ))} </div> ``` **In a tab header** ```tsx <button className="tab"> shard-a <HeatCell value={76} domain={[0, 100]} /> </button> ``` ## Accessibility [#accessibility] The accessible name carries the value *and* its calibration: the shape cells above read **"70 — level 4 of 5."** 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 and 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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 prints one stepped intensity cell per point along a single timeline: it is the 1×N sibling of ActivityGrid, with the same discrete step scale and the same cell vocabulary. A slot with no record renders a hairline outline, visibly different from a low value, because empty is not zero. 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), and the faint track look is reserved for empty slots. The steps stay discrete, the `domain` is shared rather than autoscaled per row, and downsampling takes the max per bucket rather than the mean. ```tsx <HeatStrip data="[12, 25, 38, 52, 66, 79, 88, 90, 84, 71, 55, 40, 28, 45, 62, 78, 85, 74, 58, 35]" domain="[0, 100]" title="Load per hour" width="240" height="20" /> ``` ## Install [#install] ```tsx import { HeatStrip } from "@microcharts/react/heat-strip"; <HeatStrip data={hourlyLoad} domain={[0, 100]} title="Load per hour" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { HeatStrip } from "@microcharts/react/heat-strip/interactive"; <HeatStrip data={hourlyLoad} domain={[0, 100]} /> ``` ## When to use it [#when-to-use-it] Use it for per-tenant load rows and intensity ribbons in dense tables. For exact shape use Sparkline; for weekday rhythm, ActivityGrid. ## Sizing [#sizing] **shared-domain rows** ```tsx // one domain per table — rows stay comparable {tenants.map((t) => ( <HeatStrip key={t.id} data={t.load} domain={[0, 100]} /> ))} ``` **nulls hold their slot** ```tsx // a missing record is visibly different from zero <HeatStrip data={[3, null, 8, null, 5]} /> ``` ## Variants [#variants] ```tsx <HeatStrip data={load} domain={[0, 100]} shape="dot" />`" > <span className="inline-flex flex-col gap-1.5"> <HeatStrip data="[12, 38, 66, 88, 84, 55, 28, 62, 85, 58]" domain="[0, 100]" shape="round" title="Round" width="160" height="14" /> <HeatStrip data="[12, 38, 66, 88, 84, 55, 28, 62, 85, 58]" domain="[0, 100]" shape="dot" title="Dot" width="160" height="14" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <HeatStrip data="[3, null, 8, null, 5]" title="Gaps" width="100" height="16" /> ``` A `null` holds its slot as a hairline outline instead of collapsing the strip, so positions stay aligned across stacked rows. ## Four homes [#four-homes] **In a sentence** ```tsx <p> API load over the last 20 minutes{" "} <span className="mc-inline"> <HeatStrip data={hourlyLoad} domain={[0, 100]} height={14} summary={false} /> </span>{" "} — peaked at 90%, closed at 35%. </p> ``` **In a table cell** ```tsx // one shared domain — rows stay comparable {tenants.map((t) => ( <tr key={t.name}> <td>{t.name}</td> <td><HeatStrip data={t.load} domain={[0, 100]} /></td> </tr> ))} ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">90%</span> <span className="unit">peak, minute 8</span> <HeatStrip data={hourlyLoad} domain={[0, 100]} width={200} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Acme <HeatStrip data={hourlyLoad} domain={[0, 100]} height={12} /> </button> ``` ## Accessibility [#accessibility] The summary reuses `describeSeries` verbatim: for the load strip above that is **"Trending up 383%. Range 12 to 88. Last value 58."** 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 sorts raw observations into uniform bins and draws the counts zero-anchored, never density-smoothed, so a distribution's mode, spread, and skew read off one strip. The bin count is √n capped at 12: enough shape to see skew, few enough bars to survive 60 px. Pre-aggregated counts are not supported; that's SparkBar's contract. ```tsx <HistogramStrip data="Array.from({ length: 120 }, (_, i) => (i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60)))" title="Response times" style="{ width: 220, height: 56 }" /> ``` ## Install [#install] ```tsx import { HistogramStrip } from "@microcharts/react/histogram-strip"; <HistogramStrip data={times} title="Response times" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { HistogramStrip } from "@microcharts/react/histogram-strip/interactive"; <HistogramStrip data={times} bins={8} /> ``` ## When to use it [#when-to-use-it] Use it for latency clusters in a sentence and distributions per table row. For raw marks use RugStrip; for a series over time, Sparkline. ## Sizing [#sizing] **where you fall** ```tsx const times = Array.from({ length: 120 }, (_, i) => i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60), ); <HistogramStrip data={times} markValue={45} /> ``` **fixed edges across rows** ```tsx const times = Array.from({ length: 120 }, (_, i) => i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60), ); <HistogramStrip data={times} domain={[0, 100]} /> ``` ## Variants [#variants] ```tsx i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60), ); <HistogramStrip data={responseTimes} markValue={45} title="You are here" />`" > <HistogramStrip data="Array.from({ length: 120 }, (_, i) => i % 3 === 0 ? 40 + (i % 10) : 20 + ((i * 7) % 60), )" markValue="45" title="You are here" style="{ width: 220, height: 56 }" /> ``` `markValue` marks the bin its value falls in and never re-bins around it. ```tsx `" > <HistogramStrip data="[12000, 18000, 24000, 30000, 36000, 42000, 18000, 24000, 30000]" locale="de-DE" title="Localized bin edges" style="{ width: 220, height: 56 }" /> ``` 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 `"> <HistogramStrip data="[]" title="No observations yet" style="{ width: 160, height: 40 }" /> ``` ```tsx `"> <HistogramStrip data="[4, 4, 4, 4, 4]" title="Constant" style="{ width: 160, height: 40 }" /> ``` 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 an equal-width binner would draw for a zero-span domain, and an explicit `bins` count collapses to the observation count when you ask for more bins than you have values. ## Four homes [#four-homes] **In a sentence** ```tsx <p> API latency this hour{" "} <span className="mc-inline"> <HistogramStrip data={times} width={90} height={18} summary={false} /> </span>{" "} — most calls land 40–50 ms, a few tail past 70. </p> ``` **In a table cell** ```tsx <td> <HistogramStrip data={Array.from({ length: 20 }, (_, i) => 15 + ((i * 4) % 15))} width={64} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">46 ms</span> <span className="unit">median</span> <HistogramStrip data={times} width={90} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> API <HistogramStrip data={times} width={64} height={16} /> </button> ``` ## 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) 34 of 40 seats are taken: Honeycomb fills 34 cells of an area-filling hex grid. The unit is the cell, so the count is countable, and this stays occupancy of a capacity rather than a magnitude. Cell size never changes with value, only the count does. 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 <Honeycomb value="34" total="40" unit="seats" title="Occupancy" cell="6" /> ``` ## Install [#install] ```tsx import { Honeycomb } from "@microcharts/react/honeycomb"; <Honeycomb value={34} total={40} unit="seats" title="Occupancy" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Honeycomb } from "@microcharts/react/honeycomb/interactive"; <Honeycomb value={28} total={40} /> ``` ## When to use it [#when-to-use-it] Use it for seats or licenses taken of a capacity, an occupancy read in a KPI card, or a countable of-total in a cell. For a capacity over about sixty use `Progress`, and for a magnitude with no total, MiniBar. It isn't a trend chart, and `PictogramRow` is the one that counts unlike things. ## Sizing [#sizing] **strip mode for a table cell** ```tsx <Honeycomb value={7} total={10} rows={1} /> ``` **blank empties for a quieter, uncluttered surface** ```tsx <Honeycomb value={28} total={40} empty="blank" /> ``` ## Variants [#variants] ```tsx <Honeycomb value={28} total={40} empty="blank" />`" > <span className="inline-flex gap-6 items-center"> <Honeycomb value="7" total="10" rows="1" cell="6" /> <Honeycomb value="28" total="40" empty="blank" cell="5" /> </span> ``` Empty cells are outlined by default, so takenness survives grayscale: a filled cell against an outlined one reads without color. `empty="blank"` drops the empty cells entirely instead of dimming them, for surfaces where the outline reads as noise. Packing is automatic, because a near-square comb is the recognizable form; `rows` overrides it. ## Edge cases [#edge-cases] ```tsx `"> <Honeycomb value="0" total="12" unit="seats" title="Empty" cell="6" /> ``` ```tsx `"> <Honeycomb value="45" total="40" unit="seats" title="Overbooked" cell="6" /> ``` ```tsx `"> <Honeycomb value="0" total="0" title="No capacity" cell="6" /> ``` A value past the total fills every cell while the accessible name keeps the true number, so occupancy is never silently clipped. Above sixty cells the counting stops working, so the chart emits a dev warning; reach for `Progress` there. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Seat occupancy{" "} <span className="mc-inline"> <Honeycomb value={34} total={40} rows={2} summary={false} /> </span>{" "} — 34 of 40 taken, one row left. </p> ``` **In a table cell** ```tsx <td> <Honeycomb value={34} total={40} rows={2} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">34/40</span> <span className="unit">seats taken</span> <Honeycomb value={34} total={40} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Room A <Honeycomb value={10} total={12} rows={2} /> </button> ``` ## 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 and 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"). | | `cell` | `number` | Hex cell radius in viewBox units (default 4). | | `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 prints a whole wide-range series in a 14-pixel row. It cuts the series into bands and folds them, so layer opacity carries magnitude and extremes stay visible at heights where a sparkline would flatten into noise. Two folds is the default, because two read approximately without training; three trade learnability for density, so reserve them for ranges that genuinely span them. **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 <Horizon data="[2, 5, 9, 14, 22, 31, 26, 18, 12, 24, 38, 45, 41, 30, 19, 11, 6, 3, 8, 16, 27, 35, 29, 20]" title="Cluster load" width="220" height="22" /> ``` ## Install [#install] ```tsx import { Horizon } from "@microcharts/react/horizon"; <Horizon data={cpuLoad} title="Cluster load" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Horizon } from "@microcharts/react/horizon/interactive"; <Horizon data={cpuLoad} /> ``` ## When to use it [#when-to-use-it] Use it for dense monitoring rows (dozens stacked) and wide-range series in tight cells. Folding needs a key, so skip it for first-glance audiences; with a few rows and room to spare, use Sparkline. ## Sizing [#sizing] **monitoring rows** ```tsx {hosts.map((h) => ( <Horizon key={h.id} data={h.load} title={h.name} /> ))} ``` **fold around a target** ```tsx <Horizon data={cpuLoad} baseline={20} /> ``` ## Variants [#variants] ```tsx `" > <Horizon data="[2, 5, 9, 14, 22, 31, 26, 18, 12, 24, 38, 45, 41, 30, 19, 11, 6, 3, 8, 16, 27, 35, 29, 20]" folds="3" title="Three folds" width="220" height="22" /> ``` ```tsx `" > <Horizon data="[18, 22, 19, 25, 31, 20, 17, 24, 29, 21, 19, 26]" baseline="20" title="Latency vs 20 ms SLA" width="220" height="22" /> ``` `baseline` is authored, never inferred: a fold origin is a claim about what "normal" means. ```tsx `" > <Horizon data="[ 200, 500, 900, 1400, 2200, 3100, 2600, 1800, 1200, 2400, 3800, 4500, ]" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Localized cluster load" width="220" height="22" /> ``` `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 `" > <Horizon data="[5, -3, 8, -12, 2, -6, 10]" title="Net flow" width="160" height="22" /> ``` ```tsx `"> <Horizon data="[5, 5, 5, 5, 5]" title="Flat" width="160" height="22" /> ``` 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 density means distance from the baseline rather than sign. Identical values across the whole row still render as a solid block rather than vanishing, keeping "flat" visibly distinct from "no data." ## Four homes [#four-homes] **In a sentence** ```tsx <p> Cluster load this hour{" "} <span className="mc-inline"> <Horizon data={cpuLoad} width={64} height={14} summary={false} /> </span>{" "} — peaked near 45 before dropping under 10, now steady at 20. </p> ``` **In a table cell** ```tsx <td> <Horizon data={hosts[0].load} width={70} height={14} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">45</span> <span className="unit">now 20</span> <Horizon data={cpuLoad} width={140} height={20} /> </div> ``` **In a tab header** ```tsx <button className="tab"> web-01 <Horizon data={hosts[0].load} width={40} height={10} /> </button> ``` ## 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) A session expires in nine minutes, and you need both halves of that: how much is gone and how much is left. Hourglass shows both. Sand fills the top chamber for remaining and the bottom for elapsed, and both areas are **area-true**: a linear-height fill in a triangular bulb would overstate early progress by up to 2×, so the geometry solves for true proportional area instead. `value` is the elapsed fraction, the same polarity as `Progress`, so the two compose in one product without re-learning which way is which. A thin stream at the neck renders while `0 < value < 1` and marks the running state; it disappears at both ends, so finished and not-started are shape-distinct. ```tsx <span className="inline-flex gap-4 items-center"> <Hourglass value="0.15" height="40" title="15% elapsed" /> <Hourglass value="0.5" height="40" title="half" /> <Hourglass value="0.85" height="40" title="85% elapsed" /> </span> ``` ## Install [#install] ```tsx import { Hourglass } from "@microcharts/react/hourglass"; <Hourglass value={0.75} title="Session" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Hourglass } from "@microcharts/react/hourglass/interactive"; <Hourglass value={0.75} /> ``` ## When to use it [#when-to-use-it] Use it 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. For exact percentages use Progress, for trends use Sparkline, and for fractions that aren't time use a chart that doesn't imply a clock. ## Sizing [#sizing] **TTL cell — remaining is the story** ```tsx <Hourglass value={0.7} label="remaining" /> ``` **finished is shape-distinct (no stream)** ```tsx <Hourglass value={1} /> ``` ## Variants [#variants] ```tsx <Hourglass value={1} />`" > <span className="inline-flex gap-6 items-center"> <Hourglass value="0.7" label="remaining" height="44" /> <Hourglass value="1" height="44" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <Hourglass value="0" title="Not started" height="36" /> ``` ```tsx `"> <Hourglass value="1" title="Finished" height="36" /> ``` ```tsx `" > <Hourglass value="1.4" title="Overrun" height="36" /> ``` ```tsx `"> <Hourglass value="Number.NaN" title="Invalid" height="36" /> ``` Out-of-range values clamp to 0–1 in the drawing and in the accessible name, and `NaN` renders as 0 elapsed. Both ends drop the neck stream, so a not-started or finished glyph never reads as a running timer. In the static entry the stream is a state mark and is never animated. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Session time remaining{" "} <span className="mc-inline"> <Hourglass value={0.7} summary={false} /> </span>{" "} — 75% elapsed, renew soon. </p> ``` **In a table cell** ```tsx <td> <Hourglass value={0.7} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">75%</span> <span className="unit">elapsed</span> <Hourglass value={0.7} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Admin <Hourglass value={0.7} /> </button> ``` ## 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, with opacity plus a scale-from-the-floor settle rather than a path interpolation. It announces only when the value crosses 50, 90, or 100%, so a streaming value never floods a 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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. | | `fontSize` | `number` | Type size of the percent numeral, in viewBox units (default 8). | 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 draws which state a system was in over time: each state is a flat run at its own height, left to right, joined by right-angle steps. There are no diagonals: a state holds until the next one, so a sloped line would invent readings that never happened. Run lengths and the number of steps carry how choppy the period was. The `states` order sets the vertical axis, and order is meaningful. Pass it explicitly for ordinal states like sleep depth or incident severity; for nominal states with no rank, `mode="lanes"` keeps the axis from implying one. ```tsx `" > <Hypnogram data="[ { t: 0, state: "Awake" }, { t: 8, state: "Light" }, { t: 22, state: "Deep" }, { t: 38, state: "Light" }, { t: 50, state: "REM" }, { t: 62, state: "Light" }, { t: 74, state: "Deep" }, { t: 86, state: "Light" }, { t: 98, state: "REM" }, { t: 110, state: "Awake" }, ]" states="["Awake", "REM", "Light", "Deep"]" domain="[0, 120]" title="Sleep stages" width="300" height="64" /> ``` ## Install [#install] ```tsx import { Hypnogram } from "@microcharts/react/hypnogram"; <Hypnogram data={sleep} states={["Awake","REM","Light","Deep"]} title="Sleep stages" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Hypnogram } from "@microcharts/react/hypnogram/interactive"; <Hypnogram data={sleep} states={["Awake", "REM", "Light", "Deep"]} /> ``` ## When to use it [#when-to-use-it] Use it for sleep stages and for deploy, machine, or incident state over time. For continuous signals use Sparkline; for a single current state use StatusDot. ## Sizing [#sizing] **table cell** ```tsx <Hypnogram data={row.states} width={80} height={16} /> ``` **lanes (nominal)** ```tsx <Hypnogram data={sleep} mode="lanes" /> ``` ## Variants [#variants] ```tsx `" > <Hypnogram data="[ { t: 0, state: "Awake" }, { t: 30, state: "REM" }, { t: 60, state: "Deep" }, { t: 90, state: "Light" }, ]" states="["Awake", "REM", "Light", "Deep"]" domain="[0, 120]" mode="lanes" title="Lanes" width="260" height="64" /> ``` ```tsx `" > <Hypnogram data="[ { t: 0, state: "Awake" }, { t: 8, state: "Light" }, { t: 22, state: "Deep" }, { t: 38, state: "Light" }, { t: 74, state: "Deep" }, { t: 110, state: "Awake" }, ]" states="["Awake", "REM", "Light", "Deep"]" domain="[0, 120]" emphasis="Deep" title="Deep emphasised" width="260" height="64" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <Hypnogram data="[{ t: 0, state: "Deep" }]" domain="[0, 120]" title="Deep sleep throughout" width="220" height="40" /> ``` ```tsx `" > <Hypnogram data="[ { t: 0, state: "Awake" }, { t: 20, state: "Groggy" }, { t: 40, state: "Light" }, ]" states="["Awake", "Light", "Deep"]" title="Unlisted state appended" width="220" height="52" /> ``` A single entry holds its state across the whole domain. That is one flat run, and the summary names it directly: **"1 state, no transitions; Deep throughout."** A state present in the data but missing from an explicit `states` order is never dropped. It is appended as its own row, with a dev-only console warning, so the strip accounts for every state it is given. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Last night's sleep{" "} <span className="mc-inline"> <Hypnogram data={sleep} states={states} width={90} height={20} summary={false} /> </span>{" "} — two deep blocks. </p> ``` **In a table cell** ```tsx <td><Hypnogram data={night.states} states={states} width={72} height={16} /></td> ``` **In a KPI card** ```tsx <div className="kpi"><span className="figure">2 deep</span><Hypnogram data={sleep} states={states} width={200} height={30} /></div> ``` **In a tab header** ```tsx <button className="tab">Mon <Hypnogram data={sleep} states={states} width={64} height={18} /></button> ``` Best at KPI/card scale — stage steps need height to separate. ## Accessibility [#accessibility] The accessible name states the shape: **"3 transitions across 4 states; longest run Awake."** The interactive entry roves the runs, and each announces its state and span (**"Light, from 8 to 22."**). Its entrance is a left-to-right clip reveal of the finished trace; smoothing or easing the step corners would change what the data says, so the steps are never interpolated. 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 makes a stated rate countable: it prints **3 in 20** beside a grid of twenty units with three of them filled. `total={20}` and `label="ratio"` are the default pairing because "3 in 20" reads faster for a lay audience than "15%", and the ratio label plus the fixed grid keep the denominator on the page instead of in the reader's head. Units fill in contiguous reading order from the top-left: medical-risk-communication research finds scattered fills measurably harder to count, and counting is the whole point of this chart. There are **no partial-unit fills ever**. A 37% rate is never a unit drawn 37% full, and a rate too small to fill one unit is flagged rather than faked. ```tsx <IconArray value="0.15" total="20" title="Adverse events" width="140" height="30" /> ``` ## Install [#install] ```tsx import { IconArray } from "@microcharts/react/icon-array"; <IconArray value={0.15} total={20} title="Adverse events" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { IconArray } from "@microcharts/react/icon-array/interactive"; <IconArray value={0.15} /> ``` ## When to use it [#when-to-use-it] Use it for risk in a sentence, uptake and adoption rates, and lay-audience probabilities. For a trend use Sparkline; for a full distribution use QuantileDots. ## Sizing [#sizing] **1 in 10 framing** ```tsx <IconArray value={0.1} total={10} /> ``` **risk polarity** ```tsx // fewer is better → filled units read as the risk <IconArray value={0.15} total={20} positive="down" /> ``` ## Variants [#variants] ```tsx <IconArray value={0.15} total={20} positive="down" />`" > <span className="inline-flex flex-col gap-2"> <IconArray value="0.1" total="10" title="1 in 10" width="130" height="28" /> <IconArray value="0.15" total="20" positive="down" title="Risk polarity" width="150" height="28" /> </span> ``` ```tsx <IconArray value={0.6} total={10} shape="round" /> <IconArray value={0.37} total={100} label="none" />`" > <span className="inline-flex flex-col gap-2"> <IconArray value="0.15" total="20" label="percent" title="Percent label" width="110" height="26" /> <IconArray value="0.6" total="10" shape="round" title="Round units" width="130" height="28" /> <IconArray value="0.37" total="100" label="none" title="100-unit grid" width="90" height="90" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <IconArray value="0" total="20" title="No adverse events" width="140" height="30" /> ``` ```tsx `" > <IconArray value="0.01" total="20" title="Rare event" width="140" height="30" /> ``` ```tsx `"> <IconArray value="1" total="20" title="All affected" width="140" height="30" /> ``` A rate of exactly 0 draws every unit hollow. A rate that is real but rounds to 0 whole units (`note: "sub"`) also shows a hollow grid, and the summary says so rather than standing a fractional unit 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Adverse event rate{" "} <span className="mc-inline"> <IconArray value={0.15} total={20} summary={false} /> </span>{" "} — 3 of 20 patients, 15%. </p> ``` **In a table cell** ```tsx <td> <IconArray value={0.15} total={20} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">15%</span> <span className="unit">3 of 20</span> <IconArray value={0.15} total={20} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Cohort A <IconArray value={0.15} total={20} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives the count and the rate: **"3 in 20. About 15%."** Degenerate rates are spelled out too ("0 in 20", "20 in 20 — all."). The interactive entry roves the grid in reading order with the arrow keys (2-D, row-major), announcing each unit's state and 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 stacks ordinal responses on either side of a center line: disagree to the left, agree to the right. Graded opacity encodes ordinal distance from neutral, never magnitude. Where SegmentedBar shows composition without valence, this reads the lean and how hard it leans. Both neutral conventions ship. `neutral="split"` straddles the center line, the canonical placement; `neutral="omit"` takes neutral out of the bar for a cleaner pole comparison, and its share is still counted in the total and spoken in the accessible summary. ```tsx <LikertStrip data="[ { label: "Strongly disagree", value: 10 }, { label: "Disagree", value: 14 }, { label: "Neutral", value: 14 }, { label: "Agree", value: 34 }, { label: "Strongly agree", value: 28 }, ]" title="Q1 satisfaction" width="220" height="24" /> ``` ## Install [#install] ```tsx import { LikertStrip } from "@microcharts/react/likert-strip"; <LikertStrip data={[ { label: "Strongly disagree", value: 10 }, { label: "Disagree", value: 14 }, { label: "Neutral", value: 14 }, { label: "Agree", value: 34 }, { label: "Strongly agree", value: 28 }, ]} title="Q1 satisfaction" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { LikertStrip } from "@microcharts/react/likert-strip/interactive"; <LikertStrip data={responses} /> ``` ## When to use it [#when-to-use-it] Use it for survey question rows (share one scale via SparkGroup) and sentiment in cards. Avoid it past 7 levels; for exact per-level values use MiniBar. ## Sizing [#sizing] **survey rows** ```tsx {questions.map((q) => ( <LikertStrip key={q.id} data={q.responses} title={q.text} /> ))} ``` **net score for dense tables** ```tsx <LikertStrip data={responses} label="net" /> ``` ## Variants [#variants] ```tsx <LikertStrip data={responses} label="net" />`" > <span className="inline-flex flex-col gap-3"> <LikertStrip data="[ { label: "Strongly disagree", value: 10 }, { label: "Disagree", value: 14 }, { label: "Neutral", value: 14 }, { label: "Agree", value: 34 }, { label: "Strongly agree", value: 28 }, ]" neutral="omit" title="Omit neutral" width="220" height="24" /> <LikertStrip data="[ { label: "Strongly disagree", value: 10 }, { label: "Disagree", value: 14 }, { label: "Neutral", value: 14 }, { label: "Agree", value: 34 }, { label: "Strongly agree", value: 28 }, ]" label="net" title="Net score" width="220" height="24" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <LikertStrip data="[ { label: "Disagree", value: 0 }, { label: "Agree", value: 0 }, ]" title="No responses" width="160" height="20" /> ``` ```tsx `" > <LikertStrip data="[ { label: "Disagree", value: 0 }, { label: "Neutral", value: 10 }, { label: "Agree", value: 0 }, ]" title="All neutral" width="160" height="20" /> ``` 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 across the center line onto the wrong side, and the static entry logs a dev warning when it happens. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout satisfaction, Q1{" "} <span className="mc-inline"> <LikertStrip data={responses} label="none" height={16} summary={false} /> </span>{" "} — 62% agree, 24% disagree. Leans positive. </p> ``` **In a table cell** ```tsx <td> <LikertStrip data={q.responses} label="none" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">62%</span> <span className="unit">agree, 24% disagree</span> <LikertStrip data={responses} label="none" width={160} /> </div> ``` **In a tab header** ```tsx <button className="tab"> New users <LikertStrip data={responses} label="none" width={64} /> </button> ``` ## Accessibility [#accessibility] The accessible name carries the full valence read: **"62% agree, 24% disagree, 14% neutral. Leans positive."** A |net| under 5 points reads "Balanced." The interactive entry steps the levels in data order and announces the response count beside the share it was computed from: **"Disagree: 14% (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 prints a five-number summary in a table row: min, Q1, median, Q3, max. Whiskers run to the min and max by default, because Tukey fences imply a normality assumption most product data doesn't meet. `whiskers="tukey"` switches to those fences and exposes fence-breakers as dots, capped at 3 per side (the furthest). Precomputed `stats` whose values aren't monotonic are refused with a dev warning rather than drawn as a plausible-looking box. ```tsx <MicroBox stats="{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }" title="p95 latency" width="220" height="32" /> ``` ## Install [#install] ```tsx import { MicroBox } from "@microcharts/react/micro-box"; <MicroBox stats={{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }} title="p95 latency" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MicroBox } from "@microcharts/react/micro-box/interactive"; <MicroBox data={latencies} /> ``` ## When to use it [#when-to-use-it] Use it for latency percentile rows (p50/p95/p99) and spread beside a stat; precomputed `stats` is the production path. For distribution shape use HistogramStrip. Fewer than 5 observations render as dots rather than a box. ## Sizing [#sizing] **precomputed stats (production path)** ```tsx <MicroBox stats={{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }} /> ``` **shared domain rows** ```tsx <MicroBox stats={p50} domain={[0, 300]} /> <MicroBox stats={p95} domain={[0, 300]} /> ``` ## Variants [#variants] ```tsx 40 + i), 400, 500]; <MicroBox data={values} whiskers="tukey" />`" > <MicroBox data="[...Array.from({ length: 20 }, (_, i) => 40 + i), 400, 500]" whiskers="tukey" title="With outliers" style="{ width: 220, height: 32 }" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <MicroBox data="[3, 7, 9]" title="Only three" width="220" height="32" /> ``` ```tsx `" > <MicroBox data="[7, 7, 7, 7, 7, 7]" title="All equal" width="220" height="32" /> ``` Under 5 observations MicroBox draws the raw values as dots, never a box built from too few of them. An all-equal series has a degenerate IQR: the box collapses to a 1-unit tick, and the domain pads ±1 around the value so the mark stays centered and visible. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout latency this week{" "} <span className="mc-inline"> <MicroBox stats={{ min: 40, q1: 88, median: 110, q3: 145, max: 320 }} width={90} height={16} summary={false} /> </span>{" "} — median 110ms, tail past 300ms. </p> ``` **In a table cell** ```tsx <td> <MicroBox stats={{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }} width={70} height={16} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">42 ms</span> <span className="unit">median, IQR 35–51</span> <MicroBox stats={{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }} width={200} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Prod <MicroBox stats={{ min: 12, q1: 35, median: 42, q3: 51, max: 96 }} width={44} height={14} /> </button> ``` ## 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) and announces 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) A KPI card prints "62% Chrome" and wants a mix icon beside it. MicroDonut is that icon: a ring at icon size showing roughly what something is made of. For any comparative read, use [SegmentedBar](/docs/charts/segmented-bar) instead — a flat bar beats a donut of the same data at every size we ship. The wedge cap and the labeled rollup are non-optional, and so is the hole: a ring encodes angle and arc length together, which is the read a pie's area can't match, and the reason pie stays unshipped. Nothing here is exploded, tilted, or shadowed. ```tsx <MicroDonut data="[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 90 }, { label: "Edge", value: 30 }, { label: "Arc", value: 20 }, ]" size="48" title="Traffic mix" style="{ width: 72, height: 72 }" /> ``` ## Install [#install] ```tsx import { MicroDonut } from "@microcharts/react/micro-donut"; <MicroDonut data={mix} title="Traffic mix" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MicroDonut } from "@microcharts/react/micro-donut/interactive"; <MicroDonut data={mix} /> ``` ## When to use it [#when-to-use-it] Use it for a mix icon beside the printed headline number in a KPI card. For any comparative read use SegmentedBar; these docs steer there first. ## Sizing [#sizing] **beside the printed number** ```tsx // the number is the datum; the donut repeats it — declare it decorative <span>62% Chrome <MicroDonut data={mix} decorative /></span> ``` **accessible mix icon** ```tsx <MicroDonut data={mix} title="Traffic mix" /> ``` ## Variants [#variants] ```tsx 62% Chrome <MicroDonut data={mix} decorative /></span>`" > <span className="text-lg"> {"62% Chrome "} <MicroDonut data="[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 140 }, ]" style="{ width: "1em", height: "1em" }" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <MicroDonut data="[{ label: "Chrome", value: 620 }]" title="All Chrome" size="40" style="{ width: 40, height: 40 }" /> ``` ```tsx `"> <MicroDonut data="[]" title="Empty" size="40" style="{ width: 40, height: 40 }" /> ``` A single category fills the ring, and that full annulus looks like a 100% ProgressRing; the summary names the category and its share, which is what tells the two apart. Empty data draws no wedges. ## Four homes [#four-homes] **In a sentence** ```tsx // the number is the datum; the donut repeats it — declare it decorative <p> Chrome carried 62% of this week's sessions{" "} <span className="mc-inline"> <MicroDonut data={mix} decorative summary={false} /> </span>{" "} — the rest split across four browsers. </p> ``` **In a table cell** ```tsx <td> <MicroDonut data={mix} style={{ width: 18, height: 18 }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1,000</span> <span className="unit">62% Chrome</span> <MicroDonut data={mix} title="Traffic mix" /> </div> ``` **In a tab header** ```tsx <button className="tab"> Desktop <MicroDonut data={mix} style={{ width: 14, height: 14 }} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the full composition, worded the same 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. | | `size` | `number` | Donut square edge in viewBox units (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). # MicroScatter (/docs/charts/micro-scatter) MicroScatter plots one dot per x/y pair to show whether two metrics move together. Dots render at 75% opacity, so overplot reads as density instead of hiding points behind each other. Duplicates are never jittered, because position is the encoding. Axes go unlabeled at this scale, so `title` has to name both variables, the way the demo below does. The summary's relationship words come from a documented heuristic on |r| (≥ 0.7 strong, ≥ 0.4 moderate, ≥ 0.2 weak), and wherever a relationship word appears, r appears beside it. ```tsx <span className="text-lg"> {"Latency and error rate "} <MicroScatter data="Array.from({ length: 24 }, (_, i) => ({ x: i, y: i * 3 + ((i * 7) % 5) * 6 }))" title="Latency vs error rate" style="{ width: "3em", height: "1.8em" }" /> {" correlate strongly."} </span> ``` ## Install [#install] ```tsx import { MicroScatter } from "@microcharts/react/micro-scatter"; <MicroScatter data={pairs} title="Latency vs error rate" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MicroScatter } from "@microcharts/react/micro-scatter/interactive"; <MicroScatter data={pairs} /> ``` ## When to use it [#when-to-use-it] Use it for correlation in a sentence and two-metric relationships in KPI cards. Past 60 points, bin the data instead; for ordered time series use Sparkline. ## Sizing [#sizing] **in a sentence** ```tsx latency and errors <MicroScatter data={pairs} style={{ width: "2.5em", height: "1.5em" }} /> correlate strongly ``` **with the trend** ```tsx <MicroScatter data={pairs} trend /> ``` ## Variants [#variants] ```tsx `" > <MicroScatter data="Array.from({ length: 24 }, (_, i) => ({ x: i, y: i * 3 + ((i * 7) % 5) * 6 }))" title="With trend" style="{ width: 140, height: 84 }" /> ``` ```tsx `"> <MicroScatter data="Array.from({ length: 24 }, (_, i) => ({ x: i, y: i * 3 + ((i * 7) % 5) * 6 }))" focal="12" title="You are here" style="{ width: 140, height: 84 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <MicroScatter data="[]" title="No pairs" style="{ width: 100, height: 60 }" /> ``` ```tsx `"> <MicroScatter data="[ { x: 1, y: 2 }, { x: 2, y: 3 }, ]" title="Two points" style="{ width: 100, height: 60 }" /> ``` ```tsx `"> <MicroScatter data="[ { x: 5, y: 5 }, { x: 5, y: 5 }, { x: 1, y: 8 }, ]" title="Overlapping pair" style="{ width: 100, height: 60 }" /> ``` Empty data draws just the frame, with "No data." as the summary. Under 3 points, or with a zero-variance cloud, the summary states the count and stops: no correlation claim without enough evidence behind it. Coincident points are never jittered apart. They overlap exactly, and at 75% dot opacity the overlap reads as extra density rather than hiding a point. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Bundle size and paint time{" "} <span className="mc-inline"> <MicroScatter data={pairs} height={20} summary={false} /> </span>{" "} — heavier bundles paint slower, r 0.93. </p> ``` **In a table cell** ```tsx // concurrent requests vs p95 latency, per service {services.map((s) => ( <tr key={s.name}> <td>{s.name}</td> <td><MicroScatter data={s.pairs} /></td> </tr> ))} ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.93</span> <span className="unit">r, strong positive</span> <MicroScatter data={pairs} trend width={200} height={90} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Paid <MicroScatter data={pairs} width={64} height={20} /> r 0.93 </button> ``` ## Accessibility [#accessibility] The accessible name is the count plus the evidence-backed relationship: **"24 points. Strong positive relationship (r 0.93)."** With fewer than 3 points, or a zero-variance cloud, it makes no claim at all and reads just **"2 points."** 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 draws one zero-anchored bar per category, in the order the data arrives, so a row of categories reads at a glance: which is biggest, and by roughly how much. That order is often information itself: weekday order and funnel order carry meaning that ranking would destroy, so sorting is an explicit `order` prop and never silent. An explicit `domain` is widened to include zero, because a bar whose length doesn't start at zero misstates its own magnitude, and the component refuses to draw one. ```tsx <MiniBar data="[ { label: "East", value: 940 }, { label: "West", value: 410 }, { label: "South", value: 620 }, { label: "North", value: 120 }, ]" title="Sales by region" width="150" height="48" /> ``` ## Install [#install] ```tsx import { MiniBar } from "@microcharts/react/mini-bar"; <MiniBar data={regions} title="Sales by region" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MiniBar } from "@microcharts/react/mini-bar/interactive"; <MiniBar data={regions} /> ``` ## When to use it [#when-to-use-it] Use it for per-row category mix in tables and small comparisons in KPI cards. Past 8 categories reach for a full bar chart; for time series use SparkBar. ## Sizing [#sizing] **table cell** ```tsx <MiniBar data={row.mix} width={50} height={16} /> ``` **signed with polarity** ```tsx <MiniBar data={signed} positive="up" /> ``` ## Variants [#variants] ```tsx <MiniBar data={regions} highlight="South" />`" > <span className="inline-flex gap-6"> <MiniBar data="[ { label: "East", value: 940 }, { label: "West", value: 410 }, { label: "South", value: 620 }, { label: "North", value: 120 }, ]" order="desc" title="Ranked" width="120" height="36" /> <MiniBar data="[ { label: "East", value: 940 }, { label: "West", value: 410 }, { label: "South", value: 620 }, { label: "North", value: 120 }, ]" highlight="South" title="This row's category" width="120" height="36" /> </span> ``` ```tsx `" > <MiniBar data="[ { label: "Mon", value: 4 }, { label: "Tue", value: -2 }, { label: "Wed", value: 6 }, { label: "Thu", value: -1 }, { label: "Fri", value: 3 }, ]" positive="up" title="Daily delta" width="120" height="36" /> ``` ```tsx `" > <MiniBar data="[ { label: "East", value: 12400 }, { label: "West", value: 8600 }, ]" locale="de-DE" title="Localized values" width="80" height="36" /> ``` 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 `" > <MiniBar data="[ { label: "a", value: 5 }, { label: "b", value: null }, { label: "c", value: 7 }, ]" title="Gap preserved" width="120" height="36" /> ``` ```tsx `"> <MiniBar data="[{ label: "Only", value: 4 }]" title="Single" width="80" height="36" /> ``` A `null` value draws no bar but keeps its band, so the two real bars around it stay exactly as far apart as in a fully populated row, and a column of MiniBars stays aligned through 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 every bar still renders, with nothing dropped or truncated, but the component logs a dev warning: MiniBar is a cell chart, and a wider comparison belongs in a full bar chart. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Q3 revenue splits four ways{" "} <span className="mc-inline"> <MiniBar data={regions} width={70} height={16} summary={false} /> </span>{" "} — East alone outsells North more than seven to one. </p> ``` **In a table cell** ```tsx <td> <MiniBar data={rep.mix} width={70} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">East</span> <span className="unit">45% of Q3 revenue</span> <MiniBar data={regions} highlight="East" width={140} height={40} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Region <MiniBar data={regions} width={50} height={14} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives the count and the 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) You are 300 lines into a 1,200-line log, three search hits sit elsewhere in it, and the tail hasn't loaded yet. MinimapStrip shows all three at once: a content thumbnail under a viewport window, a separate lane of annotation ticks, and hatched fog over any region you haven't loaded or crawled. Keeping position and annotations in two lanes keeps them as two reads instead of one muddle. Fog is a first-class state rather than a blank stretch that would read as empty content, and the unknown share is disclosed in the accessible name. The window maps linearly to the domain; there is no fisheye to distort where you are. ```tsx <MinimapStrip data="{ content: Array.from({ length: 1200 }, (_, i) => 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" width="300" height="18" /> ``` ## Install [#install] ```tsx import { MinimapStrip } from "@microcharts/react/minimap-strip"; <MinimapStrip data={{ content: Array.from( { length: 1200 }, (_, i) => 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. ## Try it [#try-it] ```tsx import { MinimapStrip } from "@microcharts/react/minimap-strip/interactive"; import { useState } from "react"; const [viewport, setViewport] = useState<[number, number]>([520, 660]); <MinimapStrip data={{ content, window: viewport, marks, known }} onWindowChange={setViewport} /> ``` ## When to use it [#when-to-use-it] Use it for document or log position and long-timeline navigation. For a single value use Progress; for exact content values use Sparkline. ## Sizing [#sizing] **log viewer cell** ```tsx <MinimapStrip data={{ content, window }} width={80} height={12} /> ``` **heat** ```tsx <MinimapStrip data={data} mode="heat" /> ``` ## 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" />`" > <MinimapStrip data="{ content: Array.from({ length: 1200 }, (_, i) => Math.abs(Math.sin(i / 40)) + Math.abs(Math.sin(i / 150)) * 0.6), window: [300, 440], marks: [600, 1000], }" mode="heat" title="Heat" width="300" height="18" /> ``` ## Edge cases [#edge-cases] ```tsx Math.abs(Math.sin(i / 20))), window: [40, 90], known: [[0, 200]], }} />`" > <MinimapStrip data="{ content: Array.from({ length: 200 }, (_, i) => Math.abs(Math.sin(i / 20))), window: [40, 90], known: [[0, 200]], }" title="Fully known" width="220" height="16" /> ``` When `known` covers the whole domain, no fog renders at all. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Scroll position in log{" "} <span className="mc-inline"> <MinimapStrip data={{ content, window, marks, known }} summary={false} /> </span>{" "} — at line 660 of 1,200, dense middle section. </p> ``` **In a table cell** ```tsx <td> <MinimapStrip data={{ content, window, marks, known }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">55%</span> <span className="unit">through document</span> <MinimapStrip data={{ content, window, marks, known }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Errors <MinimapStrip data={{ content, window, marks, known }} /> </button> ``` ## 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). On hover, focus, or drag, the window's own edges float above it as a chip (`400–500`), the same range `aria-valuetext` reports, so a sighted reader and a screen-reader reader get the same numbers. `readout={false}` drops the chip. ## 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 lights a fraction of a disc, a form most people read without a legend. The lit **area** equals the value exactly, via a closed-form terminator rather than the phase-angle approximation that under-lights mid-cycle, so 50% lights half the disc. Progress mode is the default and fills monotonically, the way a sprint, quota, or billing period does. The real lunar cycle waxes and then wanes, so `mode="cycle"` is a separate, explicit data-semantic switch rather than a preset. Area is a medium-precision channel: for exact reads use `Progress`. ```tsx <span className="inline-flex gap-3 items-center"> <MoonPhase value="0.1" size="24" title="10%" /> <MoonPhase value="0.35" size="24" title="35%" /> <MoonPhase value="0.5" size="24" title="50%" /> <MoonPhase value="0.75" size="24" title="75%" /> <MoonPhase value="1" size="24" title="full" /> </span> ``` ## Install [#install] ```tsx import { MoonPhase } from "@microcharts/react/moon-phase"; <MoonPhase value={0.68} title="Sprint" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MoonPhase } from "@microcharts/react/moon-phase/interactive"; <MoonPhase value={0.68} /> ``` ## When to use it [#when-to-use-it] Use it for sprint or quota progress in a sentence, a billing-period or release-cycle marker, or any 0–1 completion. For exact percentages use Progress, for trends use Sparkline, and for comparisons use MiniBar. ## Sizing [#sizing] **cycle mode maps the real lunar phases** ```tsx <MoonPhase value={0.5} mode="cycle" /> // full moon ``` ## Variants [#variants] ```tsx // first quarter <MoonPhase value={0.5} mode="cycle" /> // full moon`" > <span className="inline-flex gap-3 items-center"> <MoonPhase value="0" mode="cycle" size="24" title="new" /> <MoonPhase value="0.25" mode="cycle" size="24" title="first quarter" /> <MoonPhase value="0.5" mode="cycle" size="24" title="full" /> <MoonPhase value="0.75" mode="cycle" size="24" title="last quarter" /> </span> ``` ## Edge cases [#edge-cases] ```tsx // clamps to 0 — new, dark disc <MoonPhase value={1.6} title="Above one" /> // clamps to 1 — full, lit disc`" > <span className="inline-flex gap-3 items-center"> <MoonPhase value="-0.4" size="28" title="Below zero" /> <MoonPhase value="1.6" size="28" title="Above one" /> </span> ``` Values outside 0–1 clamp instead of overflowing: −0.4 draws a new, dark disc and 1.6 a full, lit one. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Sprint progress{" "} <span className="mc-inline"> <MoonPhase value={0.68} summary={false} /> </span>{" "} — 68% through, waxing past half. </p> ``` **In a table cell** ```tsx <td> <MoonPhase value={0.68} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">68%</span> <MoonPhase value={0.68} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Sprint 12 <MoonPhase value={0.68} /> </button> ``` ## 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`. It 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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). | | `size` | `number` | Disc box edge in viewBox units (default 16). | 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 reads a short series as a melody, so its shape lands in one look: each value is a note on a five-line staff, **pitch** (vertical position) is the value, and left-to-right is time. Pitch is the only channel, so there are no clefs, stems, beams, or bar lines. The read is in steps (nine to thirteen positions), so exact values steer to `Sparkline` with a label. Two adjacent equal values are spaced along the time axis and never dodged vertically, because moving a note vertically would change its pitch. ```tsx `" > <MusicStaff data="[3, 5, 4, 8, 6, 9, 7, 11]" label="last" title="Sprint melody" width="120" height="26" /> ``` ## Install [#install] ```tsx import { MusicStaff } from "@microcharts/react/music-staff"; <MusicStaff data={weeks} title="Sprint melody" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { MusicStaff } from "@microcharts/react/music-staff/interactive"; <MusicStaff data={weeks} /> ``` ## When to use it [#when-to-use-it] Use it 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. For exact values use Sparkline with a label; skip it for long series (over sixteen points) and for trends where the exact slope matters. ## Sizing [#sizing] **staff mode clamps the pitch on-staff** ```tsx <MusicStaff data={weeks} mode="staff" /> ``` **a rest (null) leaves a gap** ```tsx <MusicStaff data={[3, 5, null, 8, 6]} /> ``` ## Variants [#variants] ```tsx <MusicStaff data={[3, 5, null, 8, 6]} />`" > <span className="inline-flex gap-6 items-center"> <MusicStaff data="[3, 5, 4, 8, 6, 9]" mode="staff" width="100" height="24" /> <MusicStaff data="[3, 5, null, 8, 6]" width="90" height="24" /> </span> ``` Ledger mode is the default: a note above or below the staff sits on a hairline ledger tick and keeps its true pitch, rather than being clipped. `mode="staff"` is for dense cells where ledger lines would collide with a neighbour; there, out-of-range pitches clamp to the nearest staff line or space, trading visual precision for a collision-free cell. The accessible summary is the same either way, since it describes the raw values, not the clamped pixel positions. ## Edge cases [#edge-cases] ```tsx `"> <MusicStaff data="[7]" title="One value" width="60" height="24" /> ``` ```tsx `" > <MusicStaff data="[null, null, null]" title="No notes" width="80" height="24" /> ``` ```tsx `"> <MusicStaff data="[3000, 5200, 4100, 8300, 6400]" label="last" locale="de-DE" title="Localized last value" width="110" height="24" /> ``` A single value draws one note and no contour line, since a melody needs at least two notes to have a shape. An all-`null` series draws the bare staff with no notes at all, and 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Sprint velocity{" "} <span className="mc-inline"> <MusicStaff data={weeks} width={90} height={20} summary={false} /> </span>{" "} — peaked week 4. </p> ``` **In a table cell** ```tsx <td><MusicStaff data={team.weeks} width={72} height={18} /></td> ``` **In a KPI card** ```tsx <div className="kpi"><span className="figure">11</span><MusicStaff data={weeks} width={200} height={28} /></div> ``` **In a tab header** ```tsx <button className="tab">Platform <MusicStaff data={weeks} width={72} height={20} /></button> ``` ## Accessibility [#accessibility] MusicStaff reuses `Sparkline`'s natural-language summary verbatim. For `[3, 5, 4, 8, 6, 9]` that is **"Trending up 200%. Range 3 to 9. Last value 9."** The interactive entry steps the notes with ←/→ and announces 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. | | `fontSize` | `number` | Type size of the last-note label, in viewBox units. Defaults from `height`. | | `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) Money came in, money went out, and the number you act on is what's left. NetFlow draws inflow as an area above a zero baseline and outflow mirrored below it on **one shared magnitude scale**, never independently scaled to balance the picture, with the net line (`in − out`) on top. Gross and net answer different questions, and one cell answers both. Both areas anchor at zero, the net line is a plain `in − out` and is never smoothed, and its sign is stated in the label's **text**, so direction is never carried by color alone. ```tsx <NetFlow data="[ { in: 42, out: 31 }, { in: 38, out: 35 }, { in: 45, out: 29 }, { in: 40, out: 44 }, { in: 52, out: 38 }, { in: 48, out: 41 }, { in: 55, out: 36 }, { in: 50, out: 47 }, { in: 58, out: 39 }, { in: 44, out: 52 }, { in: 60, out: 41 }, { in: 57, out: 43 }, ]" format="(n) => `${n}k`" label="last" title="Monthly cash flow" width="260" height="30" /> ``` ## Install [#install] ```tsx import { NetFlow } from "@microcharts/react/net-flow"; <NetFlow data={months} title="Monthly cash flow" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { NetFlow } from "@microcharts/react/net-flow/interactive"; <NetFlow data={months} /> ``` ## When to use it [#when-to-use-it] Use it for cash flow per account row, in vs out (signups vs churn) in a KPI card, and any in-vs-out where the net is the decision. For a single net series use Sparkline; for one period's split use Delta or SegmentedBar. ## Sizing [#sizing] **mirrored bars for few months** ```tsx <NetFlow data={[ { in: 42, out: 31 }, { in: 38, out: 35 }, { in: 45, out: 29 }, { in: 40, out: 44 }, { in: 52, out: 38 }, { in: 48, out: 41 }, { in: 55, out: 36 }, { in: 50, out: 47 }, { in: 58, out: 39 }, { in: 44, out: 52 }, { in: 60, out: 41 }, { in: 57, out: 43 }, ]} mode="bars" /> ``` **debt paydown (outflow is the goal)** ```tsx <NetFlow data={[ { in: 42, out: 31 }, { in: 38, out: 35 }, { in: 45, out: 29 }, { in: 40, out: 44 }, { in: 52, out: 38 }, { in: 48, out: 41 }, { in: 55, out: 36 }, { in: 50, out: 47 }, { in: 58, out: 39 }, { in: 44, out: 52 }, { in: 60, out: 41 }, { in: 57, out: 43 }, ]} positive="down" /> ``` ## Variants [#variants] ```tsx <NetFlow data={months} positive="down" />`" > <span className="inline-flex flex-col gap-1.5"> <NetFlow data="[ { in: 45, out: 29 }, { in: 40, out: 44 }, { in: 52, out: 38 }, { in: 44, out: 52 }, { in: 60, out: 41 }, { in: 57, out: 43 }, ]" mode="bars" title="Mirrored bars" width="220" height="28" /> <NetFlow data="[ { in: 45, out: 29 }, { in: 40, out: 44 }, { in: 52, out: 38 }, { in: 44, out: 52 }, { in: 60, out: 41 }, { in: 57, out: 43 }, ]" positive="down" title="Outflow is the goal" width="220" height="28" /> </span> ``` ```tsx `" > <NetFlow data="[ { in: 42000, out: 31000 }, { in: 38000, out: 35000 }, ]" locale="de-DE" title="Localized flow" width="140" height="26" /> ``` 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 `" > <NetFlow data="[{ in: 40, out: 25 }]" title="This month" width="140" height="28" /> ``` ```tsx `" > <NetFlow data="[{ in: -5, out: -3 }]" title="Bad reading" width="140" height="28" /> ``` A single period always renders as mirrored bars, never an area, since an area drawn through one point would imply continuity the data doesn't have. `in`/`out` are magnitudes: a negative value is invalid input rather than a reversed direction, so it is coerced to `0`. When every period's magnitudes are zero (all periods coerced or genuinely empty), NetFlow draws only the zero baseline, with no areas, no bars, and no net line, and the accessible name says so plainly: **"No flow across 1 period."** ## Four homes [#four-homes] **In a sentence** ```tsx <p> Monthly cash flow{" "} <span className="mc-inline"> <NetFlow data={months} summary={false} /> </span>{" "} — net positive for the third month. </p> ``` **In a table cell** ```tsx <td> <NetFlow data={months} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+$42K</span> <span className="unit">this month</span> <NetFlow data={months} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Operating <NetFlow data={months} /> </button> ``` ## Accessibility [#accessibility] The accessible name pairs the net with its gross and counts the positive 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) Four numbers per period, in one table cell: open, high, low, close. Together they say how far a session ran and which way it closed, one row per instrument. Direction comes from valence color and from the body geometry, so it never rests on color alone. `maxPeriods` renders the most recent N sessions and warns in development past that — extra periods are dropped visibly, never averaged into composite candles. ```tsx `" > <Ohlc data="Array.from({ length: 20 }, (_, i) => { const base = 140 + Math.sin(i / 3) * 8 + i * 0.6; return { open: Math.round(base * 10) / 10, high: Math.round((base + 3 + (i % 3)) * 10) / 10, low: Math.round((base - 3 - (i % 2)) * 10) / 10, close: Math.round((base + (i % 2 === 0 ? 2 : -1.5)) * 10) / 10, }; })" title="ACME 20 sessions" width="240" height="30" /> ``` ## Install [#install] ```tsx import { Ohlc } from "@microcharts/react/ohlc"; <Ohlc data={sessions} title="ACME sessions" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Ohlc } from "@microcharts/react/ohlc/interactive"; <Ohlc data={sessions} /> ``` ## When to use it [#when-to-use-it] Use it for watchlist table rows and ticker KPI cards. For a single close series use Sparkline, and keep the count under about 20 periods. ## Sizing [#sizing] **watchlist rows** ```tsx {tickers.map((t) => ( <Ohlc key={t.symbol} data={t.sessions} title={t.symbol} /> ))} ``` **bars with last close** ```tsx <Ohlc data={sessions} mode="bars" label="last" /> ``` ## Variants [#variants] ```tsx <Ohlc data={sessions} label="last" />`" > <span className="inline-flex flex-col gap-3"> <Ohlc data="Array.from({ length: 20 }, (_, i) => { const base = 140 + Math.sin(i / 3) * 8 + i * 0.6; return { open: Math.round(base * 10) / 10, high: Math.round((base + 3 + (i % 3)) * 10) / 10, low: Math.round((base - 3 - (i % 2)) * 10) / 10, close: Math.round((base + (i % 2 === 0 ? 2 : -1.5)) * 10) / 10, }; })" mode="bars" title="Bar form" width="240" height="30" /> <Ohlc data="Array.from({ length: 20 }, (_, i) => { const base = 140 + Math.sin(i / 3) * 8 + i * 0.6; return { open: Math.round(base * 10) / 10, high: Math.round((base + 3 + (i % 3)) * 10) / 10, low: Math.round((base - 3 - (i % 2)) * 10) / 10, close: Math.round((base + (i % 2 === 0 ? 2 : -1.5)) * 10) / 10, }; })" label="last" title="With last close" width="260" height="30" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <Ohlc data="[ { open: 100, high: 103, low: 98, close: 102 }, { open: 102, high: 104, low: 99, close: 102 }, { open: 102, high: 105, low: 100, close: 101 }, ]" title="Doji in the middle" width="120" height="26" /> ``` ```tsx `"> <Ohlc data="[{ open: 100, high: 104, low: 97, close: 103 }]" title="Single period" width="60" height="26" /> ``` A session that opens and closes at the same price renders neutral and keeps a minimum 1-unit body height, so a flat period stays visible instead of collapsing to a hairline. A single period renders its own bar against its own range. ## Four homes [#four-homes] **In a sentence** ```tsx <p> ACME closed the session at{" "} <span className="mc-inline"> <Ohlc data={sessions} width={90} height={14} summary={false} /> </span>{" "} $150.30, up 7.4% over 20 sessions. </p> ``` **In a table cell** ```tsx <td> <Ohlc data={ticker.sessions} title={ticker.symbol} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$150.30</span> <span className="unit">up 7.4% over 20 sessions</span> <Ohlc data={sessions} /> </div> ``` **In a tab header** ```tsx <button className="tab"> ACME <Ohlc data={ticker.sessions} /> </button> ``` ## 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 encodes two numbers in one glyph. The orbit radius is latency, so a wider orbit is a slower service, and the orbit's dash density is the call rate, so denser dashes mean more calls. In the interactive entry the satellite orbits at a speed that mirrors the rate. Both channels are low-precision ambient reads; for exact numbers use a `Sparkline` for latency and a `Delta` or `MiniBar` for rate. A lone orbit radius means nothing on its own, so pass explicit `latencyDomain` and `rateDomain` — the same steer as `FatDigits`. ```tsx <OrbitStatus latency="240" rate="12" latencyDomain="[0, 500]" rateDomain="[0, 20]" title="Payments API" size="40" /> ``` ## Install [#install] ```tsx import { OrbitStatus } from "@microcharts/react/orbit-status"; <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} title="Payments API" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { OrbitStatus } from "@microcharts/react/orbit-status/interactive"; <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} /> ``` ## 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. Rate lives in the dashes as well as the spin, which is what lets a paused satellite still carry both variables. 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. Read the satellite's speed, not its angular position; the position encodes nothing. ## When to use it [#when-to-use-it] Use it for a live dependency health dot in a service table, for latency and rate together in one small mark, or for an infra status read. For exact latency or a trend over time use `Sparkline`; for exact rate use `Delta` or `MiniBar`. ## 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 <OrbitStatus latency={100} rate={0} latencyDomain={[0, 500]} rateDomain={[0, 20]} />`" > <span className="inline-flex gap-6 items-center"> <OrbitStatus latency="340" rate="8" threshold="300" latencyDomain="[0, 500]" rateDomain="[0, 20]" size="40" summary="false" /> <OrbitStatus latency="100" rate="0" latencyDomain="[0, 500]" rateDomain="[0, 20]" size="40" summary="false" /> </span> ``` ```tsx `" > <OrbitStatus latency="1240" rate="12" latencyDomain="[0, 2000]" rateDomain="[0, 20]" label="latency" locale="de-DE" title="Localized latency" size="40" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <OrbitStatus latency="NaN" rate="5" title="Unreachable" size="40" /> ``` ```tsx `"> <OrbitStatus latency="90" rate="0" latencyDomain="[0, 500]" rateDomain="[0, 20]" size="40" /> ``` An unknown latency or rate renders gray, drops the satellite, and stops the spin, so an unreachable dependency never looks healthy. A rate of `0` draws a solid, dash-free orbit. At `threshold` the satellite doubles in size. The rate is quantized to five ordinal steps in both renderings (five dash counts and five angular periods), and radius and speed read from the same domains in the static and interactive frames. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Payments API health{" "} <span className="mc-inline"> <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} size={16} summary={false} /> </span>{" "} — 240 ms latency, 12 req/s, orbit stable. </p> ``` **In a table cell** ```tsx <td> <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} size={18} summary={false} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">240ms</span> <span className="unit">· 12 rps</span> <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> payments <OrbitStatus latency={240} rate={12} latencyDomain={[0, 500]} rateDomain={[0, 20]} /> </button> ``` ## 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 satellite's size and the summary carry the threshold state, 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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. | | `size` | `number` | Orbit box edge in viewBox units (default 20). | | `fontSize` | `number` | Type size of the gutter label, in viewBox units. Defaults from `size`. | 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) East came in at 940 against a plan of 1,200, West a little over its own: PairedBars draws each value beside its reference, category by category. Both bars in a pair share one zero-anchored domain, so their lengths are always comparable. The reference is muted by two structural cues, opacity *and* width, never color alone. ```tsx `" > <PairedBars data="[ { label: "East", value: 940, ref: 1200 }, { label: "West", value: 410, ref: 400 }, { label: "South", value: 620, ref: 600 }, { label: "North", value: 120, ref: 300 }, ]" title="Actual vs plan" width="200" height="64" /> ``` ## Install [#install] ```tsx import { PairedBars } from "@microcharts/react/paired-bars"; <PairedBars data={regions} title="Actual vs plan" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PairedBars } from "@microcharts/react/paired-bars/interactive"; <PairedBars data={regions} /> ``` ## When to use it [#when-to-use-it] Use it for budget vs actual per region and target vs result rows. For data without a reference series use MiniBar, and keep it to 5 pairs or fewer. ## Sizing [#sizing] **table cell** ```tsx <PairedBars data={row.mix} width={60} height={20} /> ``` **overlay for tight cells** ```tsx // ghost = the reference, never the value <PairedBars data={row.mix} mode="overlay" /> ``` ## Variants [#variants] ```tsx `" > <PairedBars data="[ { label: "East", value: 940, ref: 1200 }, { label: "West", value: 410, ref: 400 }, ]" mode="overlay" title="Overlay" width="140" height="44" /> ``` Grouped is the default because overlapping bars hide small overshoots. Reach for `mode="overlay"` when the cell is too tight for two bars per pair. ```tsx `" > <PairedBars data="[ { label: "East", value: 940, ref: 1200 }, { label: "West", value: 410, ref: 400 }, ]" positive="up" title="Over/under plan" width="140" height="44" /> ``` ```tsx `" > <PairedBars data="[{ label: "East", value: 9400, ref: 12000 }]" locale="de-DE" title="Localized" width="80" height="44" /> ``` 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 `" > <PairedBars data="[{ label: "East", value: 940, ref: null }]" title="No plan yet" width="100" height="44" /> ``` ```tsx `"> <PairedBars data="[{ label: "East", value: 940, ref: 1200 }]" title="Single pair" width="80" height="44" /> ``` A pair whose `ref` is `null` draws its value bar alone, with no ghost and no zero-width placeholder, and the interactive entry announces it as **"East: 940, no reference."** A chart where *every* reference is missing dev-warns and points you to MiniBar. Past 5 pairs the grouped read blurs, so the component still renders every pair and logs a dev warning. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Regional spend vs plan this quarter{" "} <span className="mc-inline"> <PairedBars data={regions} width={100} height={18} summary={false} /> </span>{" "} — East is furthest off target, 940 spent against a 1,200 budget. </p> ``` **In a table cell** ```tsx <td> <PairedBars data={[{ label: "East", value: 940, ref: 1200 }]} positive="down" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">2 / 4</span> <span className="unit">regions under budget, this quarter</span> <PairedBars data={regions} positive="down" width={110} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Regional <PairedBars data={[{ label: "East", value: 940, ref: 1200 }]} positive="down" width={64} height={16} summary={false} /> </button> ``` ## 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) Give it a list of causes with their counts and ParetoStrip sorts them descending, then overlays a **cumulative-share line on a fixed 0–100% scale** that shows how few of them you would have to fix to account for most of the total. That scale spans the full plot height and is never rescaled, so the curve can't be steepened. Bars up to the threshold crossing are accent — the **vital few** — and the rest are muted, which is how the chart says where to stop reading. The 80% default is a reference you can move or turn off, not a law. ```tsx <ParetoStrip data="[ { label: "Timeouts", value: 38 }, { label: "OOM", value: 24 }, { label: "Deploy", value: 15 }, { label: "Config", value: 9 }, { label: "Network", value: 7 }, { label: "Auth", value: 4 }, { label: "Disk", value: 3 }, { label: "DNS", value: 3 }, { label: "Other bug", value: 2 }, ]" unit="causes" metric="incidents" title="Incident causes" width="260" height="28" /> ``` ## Install [#install] ```tsx import { ParetoStrip } from "@microcharts/react/pareto-strip"; <ParetoStrip data={causes} unit="causes" metric="incidents" title="Incident causes" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ParetoStrip } from "@microcharts/react/pareto-strip/interactive"; <ParetoStrip data={causes} /> ``` ## When to use it [#when-to-use-it] Use it for a "fix these three" read in a KPI card, incident causes or support tags in a tab header, and any long-tail composition where a few categories dominate. For a plain ranking use MiniBar; for parts of a single whole use SegmentedBar. ## Sizing [#sizing] **roll the tail into Other** ```tsx <ParetoStrip data={causes} maxItems={4} /> ``` **no threshold line** ```tsx <ParetoStrip data={causes} threshold={false} /> ``` ## Variants [#variants] ```tsx <ParetoStrip data={causes} threshold={false} />`" > <span className="inline-flex flex-col gap-2"> <ParetoStrip data="[ { label: "Timeouts", value: 38 }, { label: "OOM", value: 24 }, { label: "Deploy", value: 15 }, { label: "Config", value: 9 }, { label: "Network", value: 7 }, { label: "Auth", value: 4 }, ]" maxItems="3" title="Rolled up" width="240" height="26" /> <ParetoStrip data="[ { label: "Timeouts", value: 38 }, { label: "OOM", value: 24 }, { label: "Deploy", value: 15 }, { label: "Config", value: 9 }, { label: "Network", value: 7 }, { label: "Auth", value: 4 }, ]" threshold="false" title="No threshold" width="240" height="26" /> </span> ``` `maxItems` rolls the tail into `Other`, drawn at its true size but always last and never counted among the vital few. If `Other` towers over the top cause, `maxItems` is set too aggressively. ## Edge cases [#edge-cases] ```tsx `" > <ParetoStrip data="[ { label: "Timeouts", value: 90 }, { label: "OOM", value: 6 }, { label: "Deploy", value: 4 }, ]" unit="causes" metric="incidents" title="One dominant cause" width="240" height="26" /> ``` ```tsx `" > <ParetoStrip data="[{ label: "a", value: 0 }]" metric="incidents" title="Nothing recorded" width="240" height="26" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Incident causes{" "} <span className="mc-inline"> <ParetoStrip data={causes} summary={false} /> </span>{" "} — top 3 causes account for 80%. </p> ``` **In a table cell** ```tsx <td> <ParetoStrip data={causes} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">deploy</span> <span className="unit">42% of incidents</span> <ParetoStrip data={causes} /> </div> ``` **In a tab header** ```tsx <button className="tab"> deploy <ParetoStrip data={causes} /> </button> ``` ## 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) The top row is the whole: one bar per parent, width proportional to its share. The row beneath it holds each parent's children, tiling the exact x-range of their parent, so parentage reads out of the alignment. Alignment is the comparison channel here, which a treemap discards and a flat SegmentedBar drops along with the hierarchy. Two levels is a hard limit: deeper nesting becomes texture at this size. ```tsx `" > <PartitionStrip data="[ { label: "JS", children: [{ label: "react", value: 28 }, { label: "vendor", value: 12 }, { label: "app", value: 8 }] }, { label: "CSS", children: [{ label: "tailwind", value: 16 }, { label: "custom", value: 8 }] }, { label: "img", value: 18 }, { label: "font", value: 10 }, ]" title="Bundle composition" width="320" height="30" /> ``` ## Install [#install] ```tsx import { PartitionStrip } from "@microcharts/react/partition-strip"; <PartitionStrip data={bundle} title="Bundle composition" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PartitionStrip } from "@microcharts/react/partition-strip/interactive"; <PartitionStrip data={bundle} /> ``` ## When to use it [#when-to-use-it] Use it for bundle, storage, or budget composition, and for any two-level breakdown. Deep hierarchies are unreadable at this size, and flat parts belong in SegmentedBar. ## Sizing [#sizing] **storage cell** ```tsx <PartitionStrip data={bundle} labels={false} width={80} height={16} /> ``` **emphasis** ```tsx <PartitionStrip data={bundle} emphasis="react" /> ``` ## Variants [#variants] ```tsx `" > <PartitionStrip data="[ { label: "JS", children: [ { label: "react", value: 28 }, { label: "vendor", value: 12 }, { label: "app", value: 8 }, ], }, { label: "CSS", children: [ { label: "tailwind", value: 16 }, { label: "custom", value: 8 }, ], }, { label: "img", value: 18 }, ]" emphasis="react" title="react emphasised" width="320" height="30" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <PartitionStrip data="[ { label: "img", value: 18 }, { label: "font", value: 10 }, { label: "css", value: 6 }, ]" title="Flat parts" width="220" height="24" /> ``` ```tsx `" > <PartitionStrip data="[ { label: "JS", children: [ { label: "react", value: 28 }, { label: "vendor", value: 12 }, ], }, { label: "CSS", children: [{ label: "tailwind", value: 16 }] }, ]" title="Squeezed" width="220" height="18" /> ``` Data with no children anywhere renders as the parent row alone; the child row stays empty. Height, not width, is what drops the labels: below the floor font size they drop out rather than overlap. Grandchildren are ignored with a dev warning, since three levels of nesting at 24px is texture rather than information. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Bundle composition{" "} <span className="mc-inline"> <PartitionStrip data={bundle} summary={false} /> </span>{" "} — JS is 44% of the payload. </p> ``` **In a table cell** ```tsx <td> <PartitionStrip data={bundle} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">44%</span> <span className="unit">of total size</span> <PartitionStrip data={bundle} /> </div> ``` **In a tab header** ```tsx <button className="tab"> JS <PartitionStrip data={bundle} /> </button> ``` ## 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 own value, its share of the whole, and its share of its parent. The readout chip stacks the same figures as rows so the value is never traded away for width. 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) A service answers at 124.5 ms for half its requests and 1,661.13 ms for the slowest 1%. PercentileLadder puts both ticks on one zero-anchored track, where the distance between them is the reading. Graduated height and accent make the **tail** read strongest, because the tail is usually the SLO question. The origin is never cropped: cropping it would distort every gap. A log transform for long tails is never silent — `scale="log"` applies only when every sample is strictly positive, and an in-chart `log` tag renders whenever it does. ```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" />`" > <PercentileLadder data="Array.from({ length: 200 }, (_, i) => (i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800)))" title="Request latency" width="240" height="18" /> ``` ## Install [#install] ```tsx import { PercentileLadder } from "@microcharts/react/percentile-ladder"; <PercentileLadder data={Array.from({ length: 200 }, (_, i) => 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. ## Try it [#try-it] ```tsx import { PercentileLadder } from "@microcharts/react/percentile-ladder/interactive"; <PercentileLadder data={latencies} /> ``` ## When to use it [#when-to-use-it] Use it for latency SLOs in a sentence, the tail per endpoint in tables, and payment-size spread. For the odds of an outcome use QuantileDots; for the full distribution shape use MicroBox. ## Sizing [#sizing] **stricter SLO percentiles** ```tsx <PercentileLadder data={latencies} ps={[50, 95, 99.9]} /> ``` **log for long tails** ```tsx // the transform is never silent — a log tag renders <PercentileLadder data={latencies} scale="log" /> ``` ## Variants [#variants] ```tsx i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800), ); <PercentileLadder data={latencies} scale="log" /> <PercentileLadder data={latencies} ps={[50, 95, 99.9]} label="values" /> <PercentileLadder data={latencies} marks="dot" /> // value labels and the summary format through Intl — "1.661 ms", not "1,661 ms" <PercentileLadder data={latencies} label="values" format={{ style: "unit", unit: "millisecond", maximumFractionDigits: 0 }} locale="de-DE" />`" > <span className="inline-flex flex-col gap-2"> <PercentileLadder data="Array.from({ length: 200 }, (_, i) => i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800), )" scale="log" title="Log scale" width="220" height="18" /> <PercentileLadder data="Array.from({ length: 200 }, (_, i) => i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800), )" ps="[50, 95, 99.9]" label="values" title="Strict SLOs" width="220" height="20" /> <PercentileLadder data="Array.from({ length: 200 }, (_, i) => i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800), )" marks="dot" title="Dot marks" width="220" height="18" /> <PercentileLadder data="Array.from({ length: 200 }, (_, i) => i < 130 ? 90 + (i % 50) : i < 180 ? 150 + ((i * 7) % 320) : i < 196 ? 480 + ((i * 11) % 900) : 1500 + ((i * 13) % 800), )" label="values" format="{ style: "unit", unit: "millisecond", maximumFractionDigits: 0 }" locale="de-DE" title="de-DE locale" width="260" height="18" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <PercentileLadder data="[7, 7, 7, 7]" title="All equal" width="120" height="14" /> ``` ```tsx `" > <PercentileLadder data="[0, 10, 100, 1000]" scale="log" title="Falls back" width="120" height="14" /> ``` ```tsx `" > <PercentileLadder data="[10, 20, 30, 90, 900]" title="Too narrow for labels" width="40" height="14" /> ``` When every sample is identical the ladder collapses to a single tick instead of stacking overlapping ones. A single value at or below zero makes `scale="log"` fall back to linear, and no `log` tag renders, because the transform was not applied. Below 56 px wide the tick labels drop out rather than overlap. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Request latency{" "} <span className="mc-inline"> <PercentileLadder data={latencies} summary={false} /> </span>{" "} — p99 at 2.1 s, long tail visible. </p> ``` **In a table cell** ```tsx <td> <PercentileLadder data={latencies} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">2.1s</span> <span className="unit">latency</span> <PercentileLadder data={latencies} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <PercentileLadder data={latencies} /> </button> ``` ## 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 traces one entity's **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, exact by construction rather than estimated from the traced series, and the only line on the chart is the entity itself. The axis is never truncated, which would hide how much headroom or floor is left. ```tsx `" > <PercentileTrace data="[40, 46, 52, 58, 63, 68, 72, 76, 79, 81]" unit="week" label="last" title="Standing" width="260" height="30" /> ``` ## Install [#install] ```tsx import { PercentileTrace } from "@microcharts/react/percentile-trace"; <PercentileTrace data={ranks} title="Standing" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PercentileTrace } from "@microcharts/react/percentile-trace/interactive"; <PercentileTrace data={ranks} /> ``` ## When to use it [#when-to-use-it] Use it for one player's or product's rank drifting over time, for whether a standing has crossed into the top or bottom of the pack, and for a percentile KPI where the population context matters. For a raw value over time use Sparkline; for one absolute number against a target use Bullet or Delta. ## Sizing [#sizing] **without population bands** ```tsx <PercentileTrace data={ranks} showBands={false} /> ``` **a slipping standing (down is good)** ```tsx <PercentileTrace data={ranks} positive="down" /> ``` ## Variants [#variants] ```tsx <PercentileTrace data={[40, 46, 52, 58, 63, 68, 72, 76, 79, 81]} showBands={false} />`" > <span className="inline-flex flex-col gap-1.5"> <PercentileTrace data="[40, 46, 52, 58, 63, 68, 72, 76, 79, 81]" unit="week" title="with bands" width="220" height="28" /> <PercentileTrace data="[40, 46, 52, 58, 63, 68, 72, 76, 79, 81]" showBands="false" unit="week" title="no bands" width="220" height="28" /> </span> ``` 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, as with a support ticket's backlog rank. The line already encodes direction, so color is a redundant cue and never the only signal. ```tsx `" > <PercentileTrace data="[78, 72, 64, 55, 47, 40, 34, 29, 26, 24]" positive="down" unit="week" title="down is good" width="220" height="28" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <PercentileTrace data="[63]" unit="week" title="Single reading" width="140" height="26" /> ``` ```tsx `" > <PercentileTrace data="[102, 88, 96]" unit="week" title="Clamped ranks" width="180" height="26" /> ``` ```tsx `" > <PercentileTrace data="[40, 55, 68, 81.5]" format="{ maximumFractionDigits: 1 }" locale="de-DE" label="last" unit="week" title="Localized percentile" width="180" height="26" /> ``` 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Standing over the season{" "} <span className="mc-inline"> <PercentileTrace data={ranks} summary={false} /> </span>{" "} — climbed from 40th to 81st percentile. </p> ``` **In a table cell** ```tsx <td> <PercentileTrace data={ranks} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">81st</span> <span className="unit">percentile now</span> <PercentileTrace data={ranks} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Team A <PercentileTrace data={ranks} /> </button> ``` ## 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) CPU climbs and latency follows a beat later. PhaseTrace plots the two signals against each other as a single trajectory, so the lag becomes a loop and a regime becomes a cluster — structure two separate sparklines hide. Path order carries time, and the current state is a directed endpoint with an arrowhead, so you read where the pair is now and how it got there. The trail is muted and the recent tail accented, so the eye lands on the recent motion first. ```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" />`" > <PhaseTrace data="Array.from({ length: 40 }, (_, i) => { 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" width="110" height="100" /> ``` ## Install [#install] ```tsx import { PhaseTrace } from "@microcharts/react/phase-trace"; <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" title="Phase portrait" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PhaseTrace } from "@microcharts/react/phase-trace/interactive"; <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" /> ``` ## When to use it [#when-to-use-it] Use it for coupled-signal phase portraits: CPU × latency, inflation × unemployment. For reading exact values use DualSparkline; for a single series use Sparkline. ## Sizing [#sizing] **service cell** ```tsx <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" width={32} height={28} /> ``` **quadrant grid** ```tsx <PhaseTrace data={trajectory} grid startDot /> ``` ## 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 />`" > <PhaseTrace data="Array.from({ length: 40 }, (_, i) => { 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" tail="0.4" title="Longer tail" width="110" height="100" /> ``` `tail` sets how much of the path is accented, and `startDot` marks the first observation. You can restyle the tail, endpoint, and arrowhead, but not remove all three: together they are what makes the time direction recoverable. ```tsx `" > <PhaseTrace data="[ { x: 1200, y: 4500 }, { x: 1450, y: 4100 }, { x: 1800, y: 3600 }, ]" xLabel="Requests" yLabel="Errors" locale="de-DE" title="Localized coordinates" width="90" height="80" /> ``` With a `locale`, the accessible summary's coordinates follow that locale's own grouping: "1.800" in German rather than "1,800". In-chart marks carry no text, so only the announced numbers change. The axes are named in the summary and both domains are always linear, never silently transformed. ## Edge cases [#edge-cases] ```tsx `" > <PhaseTrace data="[{ x: 55, y: 62 }]" xLabel="CPU" yLabel="Latency" title="One observation" width="80" height="72" /> ``` ```tsx ({ x: 40, y: 40 }))} xLabel="CPU" yLabel="Latency" />`" > <PhaseTrace data="Array.from({ length: 8 }, () => ({ x: 40, y: 40 }))" xLabel="CPU" yLabel="Latency" title="No motion" width="80" height="72" /> ``` A single observation draws only the accent endpoint dot: there is 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> CPU × latency phase{" "} <span className="mc-inline"> <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" summary={false} /> </span>{" "} — lag loop in the upper-right regime. </p> ``` **In a table cell** ```tsx <td> <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">loop</span> <span className="unit">CPU × latency</span> <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" /> </div> ``` **In a tab header** ```tsx <button className="tab"> api <PhaseTrace data={trajectory} xLabel="CPU" yLabel="Latency" /> </button> ``` Best at KPI/card scale — phase loops need a square plot. ## 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) ●●●○○ — three of five units filled, a count a reader can verify by counting. Filled and hollow differ in shape as well as opacity, and PictogramRow keeps every unit the same size: the glyph never scales with the value. ```tsx <span className="text-lg"> {"The coalition holds "} <PictogramRow value="5" total="8" title="Committee seats held" style="{ width: "4.5em", height: "0.9em" }" /> {" of the seats."} </span> ``` ## Install [#install] ```tsx import { PictogramRow } from "@microcharts/react/pictogram-row"; <PictogramRow value={5} total={8} title="Committee seats held" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PictogramRow } from "@microcharts/react/pictogram-row/interactive"; <PictogramRow value={5} total={8} /> ``` ## When to use it [#when-to-use-it] Use it for seats, slots, and ratings in a sentence, and for capacity rows in tables. Past 20 units counting fails, and continuous ratios are not a count: use Progress for both. ## Sizing [#sizing] **inline in a sentence** ```tsx holds <PictogramRow value={5} total={8} style={{ width: "4.5em", height: "0.8em" }} /> of the seats ``` **star rating via renderPoint** ```tsx <PictogramRow value={3.5} total={5} renderPoint={(u) => <path key={u.index} d={starPath(u)} … />} /> ``` ## Variants [#variants] ```tsx `" > <PictogramRow value="2.5" total="4" title="True partial" width="90" height="18" /> ``` ```tsx `" > <PictogramRow value="2.5" total="4" fractional="round" title="Whole seats" width="90" height="18" /> ``` The default clips the partial unit rather than rounding it away, and draws it as a true circular segment instead of a `clipPath` — generated ids would break server-render determinism. Use `fractional="round"` where a partial unit is meaningless. ```tsx `"> <PictogramRow value="3" total="5" shape="square" title="Squares" width="90" height="18" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <PictogramRow value="3" total="0" title="No capacity" width="90" height="18" /> ``` ```tsx `"> <PictogramRow value="-2" total="4" title="Deficit" width="90" height="18" /> ``` ```tsx `"> <PictogramRow value="9" total="8" title="Over capacity" width="90" height="18" /> ``` A `total` of 0 (or non-finite) draws nothing and the summary is "No data.", since there is no row to count. A negative value renders every unit hollow, and the summary states the true number ("-2 of 4.") rather than clamping to 0. A value past the total fills every unit, and never draws more than `total` units, but the summary keeps the true count ("9 of 8.") and dev-warns, so the overflow is never hidden. ## Four homes [#four-homes] **In a sentence** ```tsx <p> The oversight committee holds{" "} <span className="mc-inline"> <PictogramRow value={5} total={8} height={16} summary={false} /> </span>{" "} of its 8 seats — a working majority. </p> ``` **In a table cell** ```tsx <td> <PictogramRow value={6} total={6} shape="square" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">14 / 20</span> <span className="unit">slots filled</span> <PictogramRow value={14} total={20} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Aria Desk <PictogramRow value={4} total={5} height={10} /> </button> ``` ## 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 turns each value in a cycle into a radial bar at its fixed angle, growing outward from an inner baseline and zero-anchored there. Longer bars are busier times. Midnight (or your week start) sits at 12 o'clock and the cycle runs clockwise. The channel is length, not sector area: equal-value bars at the rim cover more area than ones near the hub, which is why the inner radius is nonzero. Compare lengths, not wedges. ```tsx <PolarClock data="Array.from({ length: 24 }, (_, h) => (h === 14 ? 312 : h === 4 ? 20 : 80 + h))" now="14" title="Traffic by hour" size="72" /> ``` ## Install [#install] ```tsx import { PolarClock } from "@microcharts/react/polar-clock"; <PolarClock data={byHour} now={14} title="Traffic by hour" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { PolarClock } from "@microcharts/react/polar-clock/interactive"; <PolarClock data={byHour} now={14} /> ``` ## When to use it [#when-to-use-it] Use it for the shape of a 24-hour or 7-day cycle, for when a metric is busy across that cycle, and for a compact seasonal read in a KPI card. For exact value comparison unroll the cycle into a `SparkBar`; for a non-cyclic trend use `Sparkline`. Keep it under a few dozen segments. ## Sizing [#sizing] **a 7-day week, opacity mode for tiny sizes** ```tsx <PolarClock data={[120,200,180,210,260,90,60]} mode="opacity" /> ``` **rotate a weekday to the top with origin** ```tsx <PolarClock data={week} origin={1} /> // Monday at 12 o'clock ``` ## Variants [#variants] ```tsx (h === 14 ? 312 : h === 4 ? 20 : 80 + h)); const week = [120, 200, 180, 210, 260, 90, 60]; <PolarClock data={byHour} label="max" /> <PolarClock data={week} mode="opacity" />`" > <span className="inline-flex gap-6 items-center"> <PolarClock data="Array.from({ length: 24 }, (_, h) => (h === 14 ? 312 : h === 4 ? 20 : 80 + h))" label="max" size="64" summary="false" /> <PolarClock data="[120, 200, 180, 210, 260, 90, 60]" mode="opacity" size="56" summary="false" /> </span> ``` At very small sizes, where length is hard to judge, `mode="opacity"` switches the channel to a five-step fill — a radial `ActivityGrid`. That is a named change of encoding, not a cosmetic one. ```tsx (h === 14 ? 312 : h === 4 ? 20 : 80 + h)); <PolarClock data={byHour} labels={false} />`" > <PolarClock data="Array.from({ length: 24 }, (_, h) => (h === 14 ? 312 : h === 4 ? 20 : 80 + h))" labels="false" size="64" summary="false" /> ``` 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. They are merged into one path, so the orientation cue costs a single node. Set `labels={false}` only when the shape itself is the whole story and the axis genuinely doesn't matter. ```tsx (h === 14 ? 1240 : h === 4 ? 20 : 80 + h)); <PolarClock data={byHour} label="max" locale="de-DE" />`" > <PolarClock data="Array.from({ length: 24 }, (_, h) => (h === 14 ? 1240 : h === 4 ? 20 : 80 + h))" label="max" locale="de-DE" title="Localized peak" size="64" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <PolarClock data="Array(24).fill(null)" title="No data yet" size="56" /> ``` ```tsx `"> <PolarClock data="Array(24).fill(50)" title="Steady" size="56" /> ``` A `null` segment leaves a gap and the baseline ring shows the hole, because missing is not the same as zero. A cycle where every value is equal draws every bar at the same length. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Traffic by hour{" "} <span className="mc-inline"> <PolarClock data={byHour} now={14} summary={false} /> </span>{" "} — peak at 2pm, quiet after midnight. </p> ``` **In a table cell** ```tsx <td> <PolarClock data={byHour} now={14} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">14:00</span> <span className="unit">busiest hour</span> <PolarClock data={byHour} now={14} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Mon <PolarClock data={byHour} now={14} /> </button> ``` Best at KPI/card scale — hour wedges need room to resolve. ## 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). | | `size` | `number` | Dial box edge in viewBox units (default 24). | | `fontSize` | `number` | Type size of the peak label under the dial, in viewBox units. Defaults from `size`. | | `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) A backup is 68% done and the only room for it is a tab header. ProgressRing draws that fraction as an arc from a start angle fixed at 12 o'clock, with butt-cut caps: rounded caps overstate small fractions, and a variable start angle makes identical fractions look different. It is not a gauge — no needle, no red zone. ```tsx `" > <ProgressRing value="0.68" label="percent" size="48" title="Backup" style="{ width: 72, height: 72 }" /> ``` ## Install [#install] ```tsx import { ProgressRing } from "@microcharts/react/progress-ring"; <ProgressRing value={0.68} title="Backup" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ProgressRing } from "@microcharts/react/progress-ring/interactive"; <ProgressRing value={0.68} label="percent" /> ``` ## When to use it [#when-to-use-it] Use it for tab headers, KPI card corners, and cooldowns or retry timers (`sweep`). For a precise read use Progress, where the percent label is the datum. ## Sizing [#sizing] **tab header** ```tsx <span>Sync <ProgressRing value={0.68} style={{ width: "0.9em", height: "0.9em" }} /></span> ``` **cooldown (sweep)** ```tsx // retry timer: the remaining wedge shrinks <ProgressRing value={elapsed / total} sweep /> ``` ## Variants [#variants] ```tsx `" > <ProgressRing value="0.68" size="40" title="Cooldown" style="{ width: 56, height: 56 }" /> ``` ```tsx `"> <ProgressRing value="0.68" weight="6" size="40" title="Thick ring" style="{ width: 56, height: 56 }" /> ``` ```tsx `"> <ProgressRing value="0.68" label="percent" locale="de-DE" title="Localized percent" size="40" style="{ width: 56, height: 56 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <ProgressRing value="1.4" label="percent" title="Over budget" size="40" style="{ width: 56, height: 56 }" /> ``` ```tsx `"> <ProgressRing value="Number.NaN" label="percent" title="Unknown" size="40" style="{ width: 56, height: 56 }" /> ``` Past 100% the ring clamps full while the center label carries the true percent, the same contract as Progress. A non-finite `value` announces no data instead of a fraction. A full circle is drawn as two half-arcs, since SVG cannot draw a single 360° arc. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Nightly backup is{" "} <span className="mc-inline"> <ProgressRing value={0.68} style={{ width: 18, height: 18 }} summary={false} /> </span>{" "} two-thirds through. </p> ``` **In a table cell** ```tsx <td> <ProgressRing value={0.82} style={{ width: 18, height: 18 }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">142</span> <span className="unit">of 200 GB</span> <ProgressRing value={0.71} style={{ width: 32, height: 32 }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Photos <ProgressRing value={0.4} sweep style={{ width: 14, height: 14 }} /> </button> ``` ## 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, so 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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). | | `size` | `number` | Ring box edge in viewBox units (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). # Progress (/docs/charts/progress) 68% complete, or 3 of 5 steps: the label *is* the datum, which is why it is on by default. The bar gives the instant read, and the track never shrinks to make room for the label — the viewBox widens instead — so fractions stay comparable down a table column. ```tsx <Progress value="0.68" title="Onboarding" width="160" height="22" /> ``` ## Install [#install] ```tsx import { Progress } from "@microcharts/react/progress"; <Progress value={0.68} title="Onboarding" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Progress } from "@microcharts/react/progress/interactive"; <Progress value={0.68} /> ``` ## When to use it [#when-to-use-it] Use it for KPI cards, table completion columns, and step counts (`segments`). For icon-size slots use ProgressRing; for composition use SegmentedBar. ## Sizing [#sizing] **table column** ```tsx // fixed width per row — fractions stay comparable down the column <Progress value={row.done} max={row.total} style={{ width: 96 }} /> ``` **stepped onboarding** ```tsx <Progress value={3} max={5} segments={5} label="fraction" /> ``` ## Variants [#variants] ```tsx `" > <Progress value="3" max="5" segments="5" label="fraction" title="Steps" width="160" height="22" /> ``` ```tsx <Progress value={0.68} label="none" />`" > <span className="inline-flex items-center gap-4"> <Progress value="34" max="50" label="value" title="Value label" width="140" height="22" /> <Progress value="0.68" label="none" title="Bare bar" width="110" height="22" /> </span> ``` ```tsx `" > <Progress value="0.68" positive="down" title="Budget burn" width="160" height="22" /> ``` ```tsx `"> <Progress value="34" max="50" label="value" locale="de-DE" title="Localized" width="140" height="22" /> ``` 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 `" > <Progress value="112" max="100" title="Overshoot" width="160" height="22" /> ``` ```tsx `"> <Progress value="0" title="Not started" width="160" height="22" /> ``` ```tsx 0 — an empty track, // not an invented fraction <Progress value={5} max={0} title="Unknown goal" />`" > <Progress value="5" max="0" title="Unknown goal" width="160" height="22" /> ``` Past 100% the bar clamps at full and the label carries the true figure ("112%"). That is the one documented case where the bar and the number disagree, and the number wins. `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 ratio it can't compute. A `value` of `0` renders a real, empty-but-present bar rather than the no-data state, so "not started" and "unmeasurable" stay visually distinct. `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 for a ceiling. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Database migration to the new cluster is{" "} <span className="mc-inline"> <Progress value={0.68} title="Migration status" width={90} height={18} summary={false} /> </span> {" "} — on track for Friday's cutover. </p> ``` **In a table cell** ```tsx <td> <Progress value={release.done} title={`${release.name} readiness`} style={{ width: 96 }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$34.9k</span> <span className="unit">of $50k</span> <Progress value={34900} max={50000} title="Budget burn" width={140} height={22} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Setup <Progress value={3} max={5} segments={5} label="fraction" /> </button> ``` ## 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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 places a focal dot by its 2-D position, marks the split with a hairline cross, and draws the peers as tiny muted ghosts. The split defaults to the domain midpoints and takes an override, but it is never hidden: it renders wherever the split truly is. The first read is which quadrant the item sits in against the field; exact position is the second read, which is what keeps the chart legible at 24×24. At that size the axes carry no labels, so pass `xLabel`, `yLabel`, and a `title` — the accessible name is doing the naming. ```tsx <QuadrantDot data="{ x: 3, y: 9 }" field="[ { x: 2, y: 8 }, { x: 8, y: 9 }, { x: 3, y: 7 }, { x: 9, y: 2 }, { x: 7, y: 3 }, { x: 1, y: 1 }, { x: 5, y: 6 }, { x: 6, y: 8 }, { x: 4, y: 3 }, { x: 8, y: 5 }, { x: 2, y: 4 }, { x: 7, y: 7 }, { x: 3, y: 2 }, { x: 6, y: 1 }, ]" xDomain="[0, 10]" domain="[0, 10]" xLabel="effort" yLabel="impact" title="Effort vs impact" width="96" height="96" /> ``` ## Install [#install] ```tsx import { QuadrantDot } from "@microcharts/react/quadrant-dot"; <QuadrantDot data={item} field={backlog} xLabel="effort" yLabel="impact" title="Effort vs impact" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { QuadrantDot } from "@microcharts/react/quadrant-dot/interactive"; <QuadrantDot data={item} field={backlog} split={[5, 5]} xLabel="effort" yLabel="impact" /> ``` ## When to use it [#when-to-use-it] Use it for the classic prioritization 2×2 (one cell per initiative in a table), an effort-vs-impact read in a KPI card, and any "which quadrant, against the field" decision. For a full scatter plot use MicroScatter; for a single ranked list use MiniBar. ## Sizing [#sizing] **lone glyph (no field)** ```tsx <QuadrantDot data={item} xLabel="effort" yLabel="impact" /> ``` **named quadrants** ```tsx <QuadrantDot data={item} field={backlog} quadrants={["quick win", "big bet", "skip", "time sink"]} /> ``` ## Variants [#variants] ```tsx <QuadrantDot data={item} field={backlog} quadrants={["quick win", "big bet", "skip", "time sink"]} />`" > <span className="inline-flex gap-4 items-center"> <QuadrantDot data="{ x: 3, y: 9 }" xDomain="[0, 10]" domain="[0, 10]" split="[5, 5]" xLabel="effort" yLabel="impact" width="72" height="72" /> <QuadrantDot data="{ x: 3, y: 9 }" field="[ { x: 2, y: 8 }, { x: 8, y: 9 }, { x: 9, y: 2 }, { x: 6, y: 1 }, { x: 4, y: 3 }, ]" xDomain="[0, 10]" domain="[0, 10]" quadrants="["quick win", "big bet", "skip", "time sink"]" width="72" height="72" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `" > <QuadrantDot data="{ x: 5, y: 5 }" field="[ { x: 1, y: 5 }, { x: 8, y: 5 }, ]" xLabel="effort" yLabel="impact" title="Flat impact" width="72" height="72" /> ``` ```tsx `"> <QuadrantDot data="{ x: 5, y: 5 }" field="[ { x: 5, y: 5 }, { x: 1, y: 1 }, ]" xLabel="effort" yLabel="impact" title="Coincident peer" width="72" height="72" /> ``` ```tsx `"> <QuadrantDot data="{ x: Number.NaN, y: 5 }" title="Bad input" width="72" height="72" /> ``` 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. It carries the "No data." summary, the same degenerate path an empty series takes elsewhere in the library. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Effort vs impact{" "} <span className="mc-inline"> <QuadrantDot data={item} field={backlog} summary={false} /> </span>{" "} — one high-impact, low-effort outlier. </p> ``` **In a table cell** ```tsx <td> <QuadrantDot data={item} field={backlog} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1</span> <span className="unit">initiative flagged</span> <QuadrantDot data={item} field={backlog} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Auth <QuadrantDot data={item} field={backlog} /> </button> ``` Best at KPI/card scale — quadrants need a near-square seat. ## 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."** With a `field` it adds how crowded that quadrant is, and `quadrants={["quick win", …]}` replaces the generated quadrant names with your own. The interactive entry starts on the focal, then cycles the peers nearest-first, announcing each one's 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 draws a sample or a posterior as 20 dots, each one an equal-probability quantile. A dot is therefore about a **1-in-N chance**, not a raw observation. Add a `threshold` and the dots past the line are re-inked and ringed, and the summary reports the answer as a frequency ("10 in 20") rather than a percentage. Two cues carry the count, so it survives grayscale, print, and forced-colors. ```tsx <QuantileDots data="Array.from({ length: 200 }, (_, i) => Math.round(4 + (i % 30) * 0.35 + ((i * 7) % 13) * 1.1 + (i % 50 === 0 ? 20 : 0)), )" threshold="15" format="{ style: "unit", unit: "minute", unitDisplay: "short", maximumFractionDigits: 0 }" title="Bus wait" width="260" height="30" /> ``` ## Install [#install] ```tsx import { QuantileDots } from "@microcharts/react/quantile-dots"; <QuantileDots data={waits} threshold={15} format={(n) => `${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. ## Try it [#try-it] ```tsx import { QuantileDots } from "@microcharts/react/quantile-dots/interactive"; <QuantileDots data={waits} threshold={15} /> ``` ## When to use it [#when-to-use-it] Use it for a "will we miss the SLA?" read in a sentence, countable odds in a KPI card, and a posterior communicated as a frequency rather than a percent. For a precise distribution shape use HistogramStrip; for one estimate's interval use GradedBand. ## Sizing [#sizing] **count the odds past a line** ```tsx <QuantileDots data={waits} threshold={15} side="above" /> ``` **fewer dots — faster to count** ```tsx <QuantileDots data={waits} count={15} threshold={15} /> ``` ## Variants [#variants] ```tsx <QuantileDots data={waits} threshold={10} side="below" />`" > <span className="inline-flex flex-col gap-1.5"> <QuantileDots data="Array.from({ length: 200 }, (_, i) => Math.round(4 + (i % 30) * 0.35 + ((i * 7) % 13) * 1.1 + (i % 50 === 0 ? 20 : 0)), )" count="15" threshold="15" format="{ style: "unit", unit: "minute", unitDisplay: "short", maximumFractionDigits: 0 }" title="15 dots" width="220" height="28" /> <QuantileDots data="Array.from({ length: 200 }, (_, i) => Math.round(4 + (i % 30) * 0.35 + ((i * 7) % 13) * 1.1 + (i % 50 === 0 ? 20 : 0)), )" threshold="10" side="below" format="{ style: "unit", unit: "minute", unitDisplay: "short", maximumFractionDigits: 0 }" title="under 10 min" width="220" height="28" /> </span> ``` `format` also takes `Intl.NumberFormatOptions` — with a `locale`, the announced threshold follows that locale's own grouping and decimal marks. ```tsx `" > <QuantileDots data="Array.from({ length: 200 }, (_, i) => Math.round(4000 + (i % 30) * 350 + ((i * 7) % 13) * 1100 + (i % 50 === 0 ? 20000 : 0)), )" threshold="15000" format="{ style: "unit", unit: "minute", unitDisplay: "narrow" }" locale="de-DE" title="Localized threshold" width="220" height="26" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <QuantileDots data="[]" title="No samples" width="140" height="20" /> ``` ```tsx `"> <QuantileDots data="[7, 7, 7, 7, 7]" title="Certain outcome" width="140" height="20" /> ``` ```tsx `"> <QuantileDots data="Array.from({ length: 200 }, (_, i) => Math.round(4 + (i % 30) * 0.35 + ((i * 7) % 13) * 1.1))" threshold="9999" side="above" title="Threshold past the max" width="140" height="20" /> ``` Empty data draws nothing and the summary says so. An all-equal sample collapses every dot into one column: that is certainty in the sample, not a rendering glitch. A threshold outside the observed range reads as 0 in N (or N in N), so the count always reports the fraction of the field it catches. Use `count` to change how many dots the sample is quantized into; 20 is the default because a reader subitizes a handful of highlighted dots faster than they parse "48%". ## Four homes [#four-homes] **In a sentence** ```tsx <p> Bus wait times{" "} <span className="mc-inline"> <QuantileDots data={waits} threshold={15} summary={false} /> </span>{" "} — 16% of waits exceed the 15 min SLA. </p> ``` **In a table cell** ```tsx <td> <QuantileDots data={waits} threshold={15} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">16%</span> <span className="unit">over 15 min</span> <QuantileDots data={waits} threshold={15} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Route 42 <QuantileDots data={waits} threshold={15} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the odds as a frequency: **"8 in 15 chances above 15 min."** Without a `threshold` it reads "Most likely …; range …" instead. In the interactive entry, 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) A support queue sits at 214 against a capacity of 100, and the question is whether it is draining or growing. QueueDepth 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 changes the *shape* of the edge as well as its color and survives greyscale. The endpoint carries the current value and a trend glyph (▴/▾) fit over the last quarter of the window: a low-precision direction cue, which is why the summary names that window in words rather than projecting a finish. ```tsx <QueueDepth data="[42, 55, 70, 88, 96, 120, 150, 182, 214]" capacity="100" title="Support queue" width="260" height="30" /> ``` ## Install [#install] ```tsx import { QueueDepth } from "@microcharts/react/queue-depth"; <QueueDepth data={[42, 55, 70, 88, 96, 120, 150, 182, 214]} capacity={100} title="Support queue" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { QueueDepth } from "@microcharts/react/queue-depth/interactive"; <QueueDepth data={data} capacity={100} /> ``` ## When to use it [#when-to-use-it] Use it for a support-queue backlog in a KPI card, a work-in-progress stock against its WIP limit, and a will-it-drain read in a tab header. For a rate rather than a stock use Sparkline; for a single count use Delta. ## Sizing [#sizing] **draining below capacity** ```tsx <QueueDepth data={[214, 190, 150, 120, 96, 70, 48]} capacity={100} /> ``` **no capacity reference** ```tsx <QueueDepth data={[42, 55, 70, 88, 96, 120, 150, 182, 214]} /> ``` ## Variants [#variants] ```tsx <QueueDepth data={backlog} /> <QueueDepth data={backlog} capacity={100} label="none" />`" > <span className="inline-flex flex-col gap-1.5"> <QueueDepth data="[214, 190, 150, 120, 96, 70, 48]" capacity="100" title="Draining" width="220" height="28" /> <QueueDepth data="[42, 55, 70, 88, 96, 120, 150, 182, 214]" title="No capacity reference" width="220" height="28" /> <QueueDepth data="[42, 55, 70, 88, 96, 120, 150, 182, 214]" capacity="100" label="none" title="No label" width="220" height="28" /> </span> ``` ```tsx `" > <QueueDepth data="[420, 560, 720, 880, 1040, 1280, 1560, 1880, 2140]" capacity="1000" locale="de-DE" title="Localized backlog" width="220" height="28" /> ``` With a `locale`, both the endpoint label and the accessible summary's numbers follow that locale's own grouping. In German the summary reads **"2.140 queued, 2,1× capacity, growing over the last quarter."** rather than "2,140 queued, 2.1× capacity". ## Edge cases [#edge-cases] ```tsx `" > <QueueDepth data="[42, 55, 70, 88, 96, 120, 150, 182, 214]" title="No capacity" width="220" height="28" /> ``` ```tsx `"> <QueueDepth data="[90, 70, 50, 30]" capacity="100" title="Draining" width="220" height="28" /> ``` ```tsx `" > <QueueDepth data="[10, null, 40, null, 80, 120]" capacity="100" title="Gaps" width="220" height="28" /> ``` With no `capacity`, the area still draws and the summary reads **"214 queued, growing over the last quarter."** The capacity clause drops, because there is 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Support queue backlog{" "} <span className="mc-inline"> <QueueDepth data={data} capacity={100} summary={false} /> </span>{" "} — 64 open, approaching capacity. </p> ``` **In a table cell** ```tsx <td> <QueueDepth data={data} capacity={100} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">64</span> <span className="unit">open tickets</span> <QueueDepth data={data} capacity={100} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Tier 1 <QueueDepth data={data} capacity={100} /> </button> ``` ## 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. The interactive entry steps the periods, announcing each depth and whether it is 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 draws a precise rate line over low-precision **ghost volume bars**, which are the denominator. No prop removes the bars: a 4.1% conversion rate reads very differently on 38 events than on 3,800. The bars are muted and unlabeled because the question they answer is "enough?", not "how many?". Set `minVolume` and any period below it renders its rate point hollow, a shape cue that survives forced-colors and print. ```tsx <RateVolume data="[ { rate: 0.023, volume: 220 }, { rate: 0.025, volume: 190 }, { rate: 0.028, volume: 160 }, { rate: 0.029, volume: 130 }, { rate: 0.031, volume: 110 }, { rate: 0.034, volume: 90 }, { rate: 0.036, volume: 66 }, { rate: 0.041, volume: 38 }, ]" format="{ style: "percent", maximumFractionDigits: 1 }" minVolume="50" label="last" title="Conversion rate" width="260" height="28" /> ``` ## Install [#install] ```tsx import { RateVolume } from "@microcharts/react/rate-volume"; <RateVolume data={periods} minVolume={50} title="Conversion rate" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { RateVolume } from "@microcharts/react/rate-volume/interactive"; <RateVolume data={periods} minVolume={50} /> ``` ## When to use it [#when-to-use-it] Use it for a conversion or error rate with its denominator, a KPI card where the rate is the headline, and spotting a big rate move on thin volume. When volume itself needs a precise read, pair a SparkBar; for a plain series use Sparkline. ## Sizing [#sizing] **flag thin denominators** ```tsx // a 4.1% rate on 38 events renders hollow — read it with caution const periods = [ { rate: 0.023, volume: 220 }, { rate: 0.025, volume: 190 }, { rate: 0.028, volume: 160 }, { rate: 0.029, volume: 130 }, { rate: 0.031, volume: 110 }, { rate: 0.034, volume: 90 }, { rate: 0.036, volume: 66 }, { rate: 0.041, volume: 38 }, ]; <RateVolume data={periods} minVolume={50} /> ``` **per-period step rates** ```tsx const periods = [ { rate: 0.023, volume: 220 }, { rate: 0.025, volume: 190 }, { rate: 0.028, volume: 160 }, { rate: 0.029, volume: 130 }, { rate: 0.031, volume: 110 }, { rate: 0.034, volume: 90 }, { rate: 0.036, volume: 66 }, { rate: 0.041, volume: 38 }, ]; <RateVolume data={periods} curve="step" /> ``` ## Variants [#variants] ```tsx <RateVolume data={periods} curve="step" />`" > <span className="inline-flex flex-col gap-1.5"> <RateVolume data="[ { rate: 0.023, volume: 220 }, { rate: 0.028, volume: 160 }, { rate: 0.031, volume: 110 }, { rate: 0.036, volume: 66 }, { rate: 0.041, volume: 38 }, ]" format="{ style: "percent", maximumFractionDigits: 1 }" minVolume="50" title="Low-volume flag" width="220" height="26" /> <RateVolume data="[ { rate: 0.023, volume: 220 }, { rate: 0.028, volume: 160 }, { rate: 0.031, volume: 110 }, { rate: 0.036, volume: 66 }, { rate: 0.041, volume: 38 }, ]" format="{ style: "percent", maximumFractionDigits: 1 }" curve="step" title="Per-period step" width="220" height="26" /> </span> ``` ```tsx `" > <RateVolume data="[ { rate: 0.023, volume: 220 }, { rate: 0.041, volume: 38 }, ]" format="{ style: "percent", maximumFractionDigits: 1 }" volumeFormat="{ maximumFractionDigits: 0 }" locale="de-DE" title="Localized rate" width="180" height="26" /> ``` `format` and `volumeFormat` both take `Intl.NumberFormatOptions` and share the same `locale`. Under `de-DE` the rate label reads "4,1 %" and volumes group with a period ("3.800"), following that locale's own conventions rather than a hardcoded English format. ## Edge cases [#edge-cases] ```tsx `" > <RateVolume data="[ { rate: 0.02, volume: 200 }, { rate: Number.NaN, volume: 0 }, { rate: 0.03, volume: 180 }, ]" format="{ style: "percent", maximumFractionDigits: 0 }" title="Gap on zero volume" width="140" height="26" /> ``` ```tsx `"> <RateVolume data="[]" title="No periods yet" width="140" height="26" /> ``` A period with zero (or non-finite) volume renders no bar and no rate mark. A rate on **zero** volume is undefined regardless of the input, so the line gaps across that period instead of interpolating through one nothing was measured on, and the interactive entry announces it 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."** ## Four homes [#four-homes] **In a sentence** ```tsx <p> Conversion rate{" "} <span className="mc-inline"> <RateVolume data={periods} minVolume={50} summary={false} /> </span>{" "} — 4.2% on 1,240 sessions this week. </p> ``` **In a table cell** ```tsx <td> <RateVolume data={periods} minVolume={50} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">4.2%</span> <span className="unit">1,240 sessions</span> <RateVolume data={periods} minVolume={50} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Web <RateVolume data={periods} minVolume={50} /> </button> ``` ## 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 states the volume with every rate, 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) A cohort has run twelve weeks and the curve looks like it is flattening. RetentionCurve draws it as a **step** line (cohort periods are discrete) on a scale **locked to 0–100%**: the full range is the frame a share is read against, and a truncated floor would exaggerate or hide the drop. When the curve flattens, a dotted plateau marker appears. A `compare` series rides behind as a subordinate dashed ghost. ```tsx `" > <RetentionCurve data="[1, 0.72, 0.55, 0.47, 0.42, 0.4, 0.39, 0.385, 0.382, 0.38, 0.379, 0.378]" unit="week" label="last" title="W12 cohort" width="260" height="30" /> ``` ## Install [#install] ```tsx import { RetentionCurve } from "@microcharts/react/retention-curve"; <RetentionCurve data={cohort} unit="week" title="W12 cohort" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { RetentionCurve } from "@microcharts/react/retention-curve/interactive"; <RetentionCurve data={cohort} compare={industry} /> ``` ## When to use it [#when-to-use-it] Use it for a cohort retention curve in a KPI card, your decay against an industry benchmark, and spotting whether retention plateaus or keeps leaking. For a continuous signal use Sparkline; for one-number retention use Progress or Delta. ## Sizing [#sizing] **vs industry benchmark** ```tsx <RetentionCurve data={cohort} compare={industry} /> ``` **smooth (editorial)** ```tsx // step is the honest default for cohort data <RetentionCurve data={cohort} curve="smooth" /> ``` ## Variants [#variants] ```tsx <RetentionCurve data={[1, 0.72, 0.55, 0.47, 0.42, 0.4, 0.39, 0.385, 0.382, 0.38]} curve="smooth" />`" > <span className="inline-flex flex-col gap-1.5"> <RetentionCurve data="[1, 0.72, 0.55, 0.47, 0.42, 0.4, 0.39, 0.385, 0.382, 0.38]" compare="[1, 0.6, 0.44, 0.37, 0.33, 0.3, 0.29, 0.285, 0.282, 0.28]" unit="week" title="vs industry" width="220" height="28" /> <RetentionCurve data="[1, 0.72, 0.55, 0.47, 0.42, 0.4, 0.39, 0.385, 0.382, 0.38]" curve="smooth" unit="week" title="smooth" width="220" height="28" /> </span> ``` The compare series stays a dashed ghost behind your curve rather than a second competing line. `curve="smooth"` interpolates between periods, which implies values that were never measured; the step default doesn't. The plateau marker appears only when the mean period-over-period change across the tail falls below half a point, never as decoration. ## Edge cases [#edge-cases] ```tsx `"> <RetentionCurve data="[1]" unit="week" title="Week zero only" width="140" height="26" /> ``` ```tsx `" > <RetentionCurve data="[100, 72, 55, 47, 42, 40, 39]" unit="week" title="Percent-scale input" width="180" height="26" /> ``` ```tsx `" > <RetentionCurve data="[1, 0.72, 0.55, 0.47, 0.42, 0.4, 0.39]" unit="week" label="last" locale="de-DE" title="Localized retention" width="180" height="26" /> ``` 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> W12 cohort retention{" "} <span className="mc-inline"> <RetentionCurve data={cohort} summary={false} /> </span>{" "} — 38% at week 12, above benchmark. </p> ``` **In a table cell** ```tsx <td> <RetentionCurve data={cohort} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">38%</span> <span className="unit">retained</span> <RetentionCurve data={cohort} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Jan <RetentionCurve data={cohort} /> </button> ``` ## Accessibility [#accessibility] The accessible name states the final retention and, when one is 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. | | `compare` | `number[]` | Peer/industry curve, drawn as a subordinate dashed ghost. The catalog word for a second series to read the first against (DualSparkline, StarSpoke). | | `benchmark` | `number[]` | Deprecated alias for `compare`, still accepted and still winning when both are passed. | | `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 encodes each criterion twice: bar length is the score, bar thickness is that criterion's share of the total weight. A heavy criterion scoring poorly therefore carries more ink than a light one scoring well, which a table of the same numbers hides. There is no total bar and none can be added; the summary names the highest and lowest criterion instead of a weighted average, because one number throws away the structure. A `target` draws a single pass line across every row. ```tsx `" > <RubricStrip data="[ { label: "Correctness", score: 0.92, weight: 3 }, { label: "Coverage", score: 0.78, weight: 2 }, { label: "Clarity", score: 0.65, weight: 1 }, { label: "Style", score: 0.41, weight: 1 }, ]" target="0.7" title="Model eval" width="240" height="44" /> ``` ## Install [#install] ```tsx import { RubricStrip } from "@microcharts/react/rubric-strip"; <RubricStrip data={[ { label: "Correctness", score: 0.92, weight: 3 }, { label: "Coverage", score: 0.78, weight: 2 }, { label: "Clarity", score: 0.65, weight: 1 }, { label: "Style", score: 0.41, weight: 1 }, ]} title="Model eval" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { RubricStrip } from "@microcharts/react/rubric-strip/interactive"; <RubricStrip data={criteria} target={0.70} /> ``` ## When to use it [#when-to-use-it] Use it for model and code-review scorecards and for weighted vendor comparison. For a single criterion use Bullet; for parts of a whole use SegmentedBar. ## Sizing [#sizing] **table cell** ```tsx <RubricStrip data={row.criteria} labels={false} width={60} height={24} /> ``` **with target** ```tsx <RubricStrip data={criteria} target={0.7} /> ``` ## Variants [#variants] ```tsx `" > <RubricStrip data="[ { label: "Lint", score: 1 }, { label: "Types", score: 1 }, { label: "Tests", score: 0.8 }, { label: "Docs", score: 0.5 }, ]" title="Checklist" width="240" height="44" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <RubricStrip data="[{ label: "Bonus", score: 2, weight: 1 }]" title="Clamped" width="200" height="16" /> ``` ```tsx `"> <RubricStrip data="[{ label: "Overall fit", score: 0.83 }]" title="Single criterion" width="200" height="16" /> ``` A score outside the domain clamps to a full-length bar rather than overflowing the track, and the component logs a dev warning. Omit `weight` and every criterion carries the same thickness, which is what the unweighted variant above shows. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Model eval scorecard{" "} <span className="mc-inline"> <RubricStrip data={criteria} summary={false} /> </span>{" "} — 78% weighted, accuracy weakest. </p> ``` **In a table cell** ```tsx <td> <RubricStrip data={criteria} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">78%</span> <span className="unit">weighted score</span> <RubricStrip data={criteria} /> </div> ``` **In a tab header** ```tsx <button className="tab"> GPT-4 <RubricStrip data={criteria} /> </button> ``` ## Accessibility [#accessibility] The accessible name reports the count and 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 puts every value in a sample on one line as its own tick, so the full spread and the places the values crowd read straight off the marks. Every tick is one real observation: no binning, no jitter, no thinning. Coincident values darken through three opacity tiers (35%, 60%, 85%), so a lone tick stays visible while a stack reads as density; the tiers are bucketed because a browser paints one path's stroke as a single operation, so the overlap compositing you might expect never happens. `markValue` adds a full-height highlighted tick for one raw value against the field, which is the strongest single read the strip offers; the regular ticks are inset by one unit so that highlight reads taller without escaping the box. ```tsx `" > <span className="text-lg"> {"Your offer sits "} <RugStrip data="[42, 48, 51, 53, 55, 58, 61, 63, 66, 71, 55, 52, 49, 58, 62, 75, 83]" markValue="78" title="Pay band" style="{ width: "7em", height: "0.9em" }" /> {" inside the band."} </span> ``` ## Install [#install] ```tsx import { RugStrip } from "@microcharts/react/rug-strip"; <RugStrip data={salaries} markValue={yourOffer} title="Pay band" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { RugStrip } from "@microcharts/react/rug-strip/interactive"; <RugStrip data={salaries} markValue={yourOffer} /> ``` ## When to use it [#when-to-use-it] Use it for a "you are here" read in a pay band, raw spread beside a stat, or margin composition under a Sparkline. Past about 400 observations, reach for HistogramStrip: a rug promises raw marks and never downsamples. For trends over time use Sparkline. ## Sizing [#sizing] **you vs the field** ```tsx <RugStrip data={salaries} markValue={78000} domain={[40000, 120000]} /> ``` **fixed domain across rows** ```tsx // same scale per row or the rugs lie <RugStrip data={salaries} domain={[0, 200]} /> <RugStrip data={salaries.map((v) => v * 1.9)} domain={[0, 200]} /> ``` ## Variants [#variants] ```tsx `"> <RugStrip data="[42, 48, 55, 58, 61, 66, 71, 75]" markValue="62" title="Field plus you" style="{ width: 140, height: 14 }" /> ``` ```tsx `" > <RugStrip data="[50, 50, 50, 50, 62, 71]" title="Duplicates" style="{ width: 140, height: 14 }" /> ``` ```tsx `"> <RugStrip data="[42, 48, 55, 61, 71]" orientation="vertical" title="Vertical rug" style="{ width: 14, height: 80 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <RugStrip data="[]" title="No observations" style="{ width: 140, height: 14 }" /> ``` Empty data draws a centered axis line rather than a blank hole, so the strip still reads as a rug with nothing on it instead of a layout gap. ```tsx `"> <RugStrip data="[52]" title="One observation" style="{ width: 140, height: 14 }" /> ``` 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 `"> <RugStrip data="[42, 48, 55, 61, 71]" markValue="120" title="Above the field" style="{ width: 140, height: 14 }" /> ``` A `markValue` outside the observed domain clamps to the nearest edge. It never escapes the box, and it never rescales the field around itself. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Their $62k offer sits{" "} <span className="mc-inline"> <RugStrip data={salaries} markValue={62} height={18} summary={false} /> </span>{" "} inside the band, not at either edge. </p> ``` **In a table cell** ```tsx <td> <RugStrip data={salaries} markValue={62} width={64} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$62k</span> <span className="unit">inside the range</span> <RugStrip data={salaries} markValue={62} width={90} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Engineering <RugStrip data={salaries} markValue={62} width={40} height={14} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the distribution: **"8 values from 42 to 75, median 59.5."** The interactive entry walks the observations in sorted order and announces each one 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 splits one bar into adjacent segments, one per labeled category, that always sum to the whole: the read is what a thing is made of, and in what proportions. Segments keep your data order by default, because sequences like funnel stages and weekday mixes carry meaning that ranking destroys; `order="desc"` ranks them when they have none. Past `maxSegments` the tail rolls into a labeled "Other" rather than being dropped, and summary percents use largest-remainder rounding so they sum to exactly 100. ```tsx <SegmentedBar data="[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 90 }, { label: "Edge", value: 30 }, { label: "Arc", value: 20 }, ]" title="Browser share" width="220" height="20" /> ``` ## Install [#install] ```tsx import { SegmentedBar } from "@microcharts/react/segmented-bar"; <SegmentedBar data={mix} title="Browser share" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { SegmentedBar } from "@microcharts/react/segmented-bar/interactive"; <SegmentedBar data={mix} /> ``` ## When to use it [#when-to-use-it] Use it for a traffic mix per table row or composition in a KPI card. For precise cross-row comparison use MiniBar; for negative parts use Waterfall. This is also the replacement for the pie chart the catalog doesn't ship — see [Design notes](/docs/design-notes). ## Sizing [#sizing] **table cell** ```tsx <SegmentedBar data={row.mix} width={60} height={10} /> ``` **value labels** ```tsx <SegmentedBar data={[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 90 }, { label: "Edge", value: 30 }, { label: "Arc", value: 20 }, ]} label="value" style={{ width: 160 }} /> ``` ## Variants [#variants] ```tsx `" > <SegmentedBar data="[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 140 }, ]" title="With percents" width="220" height="20" /> ``` ```tsx `" > <SegmentedBar data="[ { label: "Firefox", value: 90 }, { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, ]" order="desc" title="Ranked" width="220" height="20" /> ``` ```tsx `" > <SegmentedBar data="[ { label: "Chrome", value: 12400 }, { label: "Safari", value: 6200 }, ]" label="value" locale="de-DE" title="Localized values" width="220" height="20" /> ``` `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 `" > <SegmentedBar data="[ { label: "Chrome", value: 620 }, { label: "Safari", value: 240 }, { label: "Firefox", value: 90 }, { label: "Edge", value: 30 }, { label: "Arc", value: 12 }, { label: "Brave", value: 8 }, ]" label="percent" title="Six sources, five segments" width="220" height="20" /> ``` ```tsx `"> <SegmentedBar data="[{ label: "Chrome", value: 1 }]" label="percent" title="Single category" width="100" height="20" /> ``` 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. A part-to-whole cannot contain negative parts, so negative values are excluded from the bar entirely (not clamped to zero) and the component logs a dev warning steering toward Waterfall. ## Four homes [#four-homes] **In a sentence** ```tsx <p> This week's sessions skew Chrome{" "} <span className="mc-inline"> <SegmentedBar data={mix} width={90} height={14} summary={false} /> </span>{" "} — 62% Chrome, 24% Safari, the rest long tail. </p> ``` **In a table cell** ```tsx <td> <SegmentedBar data={row.mix} width={90} height={12} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">62%</span> <span className="unit">sessions on Chrome</span> <SegmentedBar data={mix} width={200} height={20} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Marketing <SegmentedBar data={mix} width={54} height={12} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the full composition: **"Chrome 62%, Safari 24%, Firefox 9%, Edge 3%, Other 2%."** The interactive entry roves the segments, announcing each one's share and value. The Other segment adds the total it stands for and how many categories were folded into it (**"Other: 2%, 20 over 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) Errors arrive in bursts, and the question is when they hit and how hard. Seismogram centers its ticks on a midline and flares them symmetrically: density is presence, length is intensity. The centered trace keeps the strip from inviting the bar-to-bar magnitude comparison it is too small to support, and keeps it visually distinct from SparkBar. Long series collapse via max-per-bucket only, so a spike always survives, and the summary is computed from the raw values rather than the buckets. ```tsx `" > <Seismogram data="[1, 2, 1, 3, 2, 6, 2, 1, 0, 2, 1, 4, 9, 3, 1, 2, 0, 1, 3, 2, 7, 2, 1, 0, 2, 1, 5, 11, 3, 1]" title="Error bursts" width="240" height="36" /> ``` ## Install [#install] ```tsx import { Seismogram } from "@microcharts/react/seismogram"; <Seismogram data={burstsPerMinute} title="Error bursts" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Seismogram } from "@microcharts/react/seismogram/interactive"; <Seismogram data={burstsPerMinute} /> ``` ## When to use it [#when-to-use-it] Use it for error bursts per service, alert density, and activity texture in table rows. For level tracking use Sparkline; for labeled events use EventTimeline. ## Sizing [#sizing] **table row** ```tsx <Seismogram data={service.bursts} width={120} height={14} /> ``` **barcode (presence only)** ```tsx // heights are noise? declare it — uniform ticks say "when", not "how hard" <Seismogram data={burstsPerMinute} mode="barcode" /> ``` **flag anomalies** ```tsx // spikes at or above the threshold flare in the alert token <Seismogram data={service.bursts} anomaly={6} /> ``` ## Variants [#variants] ```tsx `" > <Seismogram data="[1, 0, 2, 1, 3, 0, 1, 2, 0, 1, 1, 0, 2, 1, 3, 1, 0, 1, 2, 1]" mode="barcode" title="Deploys" width="180" height="22" /> ``` In `barcode` mode every tick is the same length: it records presence and carries no magnitude. ```tsx `" > <Seismogram data="[3, -1, 4, -2, 1, 5, -3, 2, -1, 4, -2, 6, -4, 1, -2, 3, -1, 2]" positive="up" title="Net flow shocks" width="200" height="30" /> ``` ```tsx `" > <Seismogram data="[1, 2, 1, 3, 2, 6, 2, 1, 0, 2, 1, 4, 9, 3, 1, 2, 0, 1, 3, 2, 7, 2, 1, 0, 2, 1, 5, 11, 3, 1, 2, 1]" anomaly="6" title="Error bursts" width="240" height="36" /> ``` `anomaly` flags spikes at or above its threshold in the alert token, on top of the tick length that already shows them, so the flag never rests on color alone. ```tsx `" > <Seismogram data="[3, -1, 4, -2, 12500, 5, -3, 2, -1, 4]" positive="up" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Localized magnitudes" width="200" height="30" /> ``` `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 `" > <Seismogram data="[0, 0, 0, 0, 0, 0]" title="Quiet" width="160" height="20" /> ``` ```tsx (i === 210 ? 9 : i % 17 === 0 ? 2 : 0))} />`" > <Seismogram data="Array.from({ length: 300 }, (_, i) => (i === 210 ? 9 : i % 17 === 0 ? 2 : 0))" title="Long history" width="160" height="24" /> ``` All-zero data renders the baseline at rest rather than a blank hole. A series longer than the pixel width collapses by max-per-bucket, so a single sharp spike is never averaged away. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout errors this hour{" "} <span className="mc-inline"> <Seismogram data={burstsPerMinute} width={90} height={16} summary={false} /> </span>{" "} — 29 events, spiking to 11 at minute 28. </p> ``` **In a table cell** ```tsx <td> <Seismogram data={svc.bursts} domain={[0, 12]} width={90} height={16} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">29</span> <span className="unit">events/hr · peak 11</span> <Seismogram data={burstsPerMinute} anomaly={6} width={200} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout-api <Seismogram data={svc.bursts} domain={[0, 12]} width={54} height={14} /> </button> ``` ## 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."). A quiet slot reads as the zero it is ("Point 1 of 5: 0."), and only a missing sample announces "no data". Home and End jump to the first and 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 mirrors two histograms around a center line: **before** upward (muted), **after** downward (accent), on **shared bin edges**. Bar heights are per-side proportions (each side's counts over its own n) on one shared scale, so a bigger sample on one side cannot fake a shift. The mirror carries **identity**, not valence — up is "before", not "good". The median shift is the precise takeaway, and the medians are never smoothed or trimmed. ```tsx <ShiftHistogram data="{ before: Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20), after: Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="The fix" width="260" height="30" /> ``` ## Install [#install] ```tsx import { ShiftHistogram } from "@microcharts/react/shift-histogram"; <ShiftHistogram data={{ before, after }} title="The fix" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { ShiftHistogram } from "@microcharts/react/shift-histogram/interactive"; <ShiftHistogram data={{ before, after }} /> ``` ## When to use it [#when-to-use-it] Use it for a "the fix, proven" read in a KPI card, before/after distributions in an experiments table, and showing that a change is more than a mean move. For a single distribution use HistogramStrip; for two labelled arms use ABStrips. ## Sizing [#sizing] **overlay for similar shapes** ```tsx const before = Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20); const after = Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20); <ShiftHistogram data={{ before, after }} mode="overlay" /> ``` **no real shift** ```tsx const before = Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20); <ShiftHistogram data={{ before, after: before }} /> ``` ## Variants [#variants] ```tsx 120 + (i % 40) - 20); const after = Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20); <ShiftHistogram data={{ before, after }} mode="overlay" /> <ShiftHistogram data={{ before, after: before }} />`" > <span className="inline-flex flex-col gap-2"> <ShiftHistogram data="{ before: Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20), after: Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" mode="overlay" title="Overlay" width="240" height="26" /> <ShiftHistogram data="{ before: Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20), after: Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20), }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="No shift" width="240" height="26" /> </span> ``` ```tsx `" > <ShiftHistogram data="{ before: [12000, 12400, 12800], after: [9600, 10000, 10400] }" locale="de-DE" title="Localized medians" width="200" height="26" /> ``` The median shift label and accessible summary follow `format`/`locale`. Under `de-DE` the before median above reads "12.400" rather than the English "12,400". `mode="overlay"` draws the after bins above the axis as an outline over the before fill instead of mirroring them below it; the bins stay shared either way. ## Edge cases [#edge-cases] ```tsx 120 + (i % 40) - 20); <ShiftHistogram data={{ before, after: [] }} format={(n) => Math.round(n) + " ms"} title="Awaiting after sample" />`" > <ShiftHistogram data="{ before: Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20), after: [], }" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="Awaiting after sample" width="240" height="26" /> ``` ```tsx `"> <ShiftHistogram data="{ before: [], after: [] }" title="No data yet" width="200" height="26" /> ``` With one side empty the chart still renders the populated side's bins alone, and the summary names the gap: **"Median 116 ms; no after sample."** It never reports a fabricated shift. With both sides empty nothing is plottable and the accessible name says **"No data."** ## Four homes [#four-homes] **In a sentence** ```tsx <p> The fix, proven{" "} <span className="mc-inline"> <ShiftHistogram data={{ before, after }} summary={false} /> </span>{" "} — latency distribution shifted left after deploy. </p> ``` **In a table cell** ```tsx <td> <ShiftHistogram data={{ before, after }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">−20ms</span> <span className="unit">after fix</span> <ShiftHistogram data={{ before, after }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> checkout <ShiftHistogram data={{ before, after }} /> </button> ``` ## Accessibility [#accessibility] Side identity rides on position (up is before, down is after), so it survives grayscale and forced-colors. The accessible name states the median shift: **"Median fell from 116 ms to 92 ms."** It appends "On N / M samples" when the sample 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 draws one line per category between two moments: East 40 to 47, West 55 to 41, and any crossing between them. Both columns share one y-domain, since per-column normalization would fake convergence. Lines stay neutral until you declare `positive`, because a rank change is not automatically good or bad. ```tsx `" > <Slope data="[ { label: "East", from: 40, to: 47 }, { label: "West", from: 55, to: 41 }, { label: "South", from: 30, to: 33 }, ]" title="Before vs after" width="130" height="90" /> ``` ## Install [#install] ```tsx import { Slope } from "@microcharts/react/slope"; <Slope data={cohorts} title="Before vs after" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Slope } from "@microcharts/react/slope/interactive"; <Slope data={cohorts} /> ``` ## When to use it [#when-to-use-it] Use it for before/after experiments, rank shuffles, and two-moment comparisons, up to about 7 categories. A two-point line says nothing about the path between the moments, so use Sparkline when that path is the point. ## Sizing [#sizing] **KPI before/after** ```tsx <Slope data={cohorts} label="both" width={140} height={96} /> ``` **one vs the field** ```tsx <Slope data={cohorts} highlight="West" /> ``` ## Variants [#variants] ```tsx <Slope data={rows} positive="up" highlight="West" />`" > <span className="inline-flex gap-6"> <Slope data="[ { label: "East", from: 40, to: 47 }, { label: "West", from: 55, to: 41 }, { label: "Mid", from: 20, to: 35 }, ]" label="both" title="Labeled" width="180" height="100" /> <Slope data="[ { label: "East", from: 40, to: 47 }, { label: "West", from: 55, to: 41 }, { label: "Mid", from: 20, to: 35 }, ]" positive="up" highlight="West" title="Valence" width="130" height="90" /> </span> ``` End labels drop deterministically rather than 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. 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. ## Edge cases [#edge-cases] ```tsx `" > <Slope data="[ { label: "East", from: 40, to: 47 }, { label: "New", from: NaN, to: 41 }, ]" title="New entrant" width="130" height="70" /> ``` ```tsx `"> <Slope data="[{ label: "East", from: 40, to: 47 }]" title="One category" width="90" height="60" /> ``` ```tsx `" > <Slope data="[ { label: "East", from: 4000, to: 4700 }, { label: "West", from: 5500, to: 4100 }, ]" label="both" locale="de-DE" title="Localized values" width="150" height="90" /> ``` 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 is no second point to connect to, so nothing is interpolated, and the row is announced as "incomplete". With a `locale`, both column labels and the announced values follow that locale's own grouping. ## Four homes [#four-homes] **In a sentence** ```tsx <p> West's renewal rate slid from best to worst region{" "} <span className="mc-inline"> <Slope data={[{ label: "West", from: 55, to: 41 }]} width={40} height={18} summary={false} /> </span>{" "} — down 25%. </p> ``` **In a table cell** ```tsx <td> <Slope data={[{ label: "East", from: 40, to: 47 }]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">20% → 35%</span> <span className="unit">Mid region, up 75%</span> <Slope data={cohorts} positive="up" highlight="Mid" width={140} height={90} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Regions <Slope data={cohorts} width={64} height={20} /> </button> ``` ## 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: one bar per period, always anchored at zero, because a bar that starts anywhere else misstates its magnitude. Each period is its own count — a deploy total, a daily total — rather than a sample on a continuous curve. In `bar` mode the endpoint bar takes the accent, since "where did it land" is usually the first question a run of counts answers. `winloss` mode discards magnitude on purpose and collapses each value to its sign: wins above the mid-line, losses below, and a tie (`0`) as a thin neutral dash on it. There every bar keeps its own win/loss/tie color, endpoint included, because the sign is what matters at each position. Sign is doubled by position and color wherever signed data appears, so direction survives forced-colors. ```tsx <SparkBar data="[4, 6, 2, 8, 5, 9]" width="120" height="32" title="Deploys per day" /> ``` ## Install [#install] ```tsx import { SparkBar } from "@microcharts/react/sparkbar"; <SparkBar data={[4, 6, 2, 8, 5, 9]} title="Deploys per day" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { SparkBar } from "@microcharts/react/sparkbar/interactive"; <SparkBar data={[4, 6, 2, 8, 5, 9, 3, 7, 6, 10]} mode="bar" /> ``` ## When to use it [#when-to-use-it] Use it for discrete magnitudes, win–loss streaks, and period-over-period counts. For a continuous trend shape use [Sparkline](/docs/charts/sparkline), which is also the better choice at 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. **default** ```tsx // data alone → an intrinsic 80×20 box <SparkBar data={[4, 6, 2, 8, 5, 9]} /> ``` **fixed size** ```tsx // width & height are viewBox units — they also set the pixel box <SparkBar data={[4, 6, 2, 8, 5, 9]} width={200} height={48} /> ``` **responsive** ```tsx // omit width/height, let CSS drive the width — the viewBox keeps the ratio <div style={{ width: "100%", maxWidth: 320 }}> <SparkBar data={[4, 6, 2, 8, 5, 9]} style={{ width: "100%", height: "auto" }} /> </div> ``` ## Variants [#variants] ```tsx `"> <SparkBar data="[4, 6, 2, 8, 5, 9]" width="130" height="32" title="Magnitude" /> ``` ```tsx `"> <SparkBar data="[1, -1, 1, 1, -1, 1, -1]" width="130" height="32" mode="winloss" title="Win–loss streak" /> ``` ```tsx `" > <SparkBar data="[1, -1, 0, 1, 0, -1, 1]" width="130" height="32" mode="winloss" title="Win–loss–tie streak" /> ``` ```tsx `"> <SparkBar data="[4, 6, 2, 8, 5, 9]" width="130" height="32" label="last" title="With endpoint label" /> ``` ```tsx `"> <SparkBar data="[4200, 6100, 2800, 8600]" width="130" height="32" label="last" locale="de-DE" title="Localized" /> ``` 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 `"> <SparkBar data="[]" title="No deploys yet" width="100" height="32" /> ``` ```tsx `"> <SparkBar data="[6]" title="One deploy" width="40" height="32" /> ``` 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, and `winloss` mode treats it the same way a longer streak would: a lone win, loss, or tie. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Deploys peaked at nine mid-week{" "} <span className="mc-inline"> <SparkBar data={deploys} width={70} height={16} summary={false} /> </span>{" "} — quieter since. </p> ``` **In a table cell** ```tsx <td> <SparkBar data={services[0].deploys} width={56} height={16} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">5 / 7</span> <span className="unit">last 7 runs</span> <SparkBar data={ciRuns} mode="winloss" width={80} height={26} /> </div> ``` **In a tab header** ```tsx <button className="tab"> CI <SparkBar data={ciRuns} mode="winloss" width={44} height={14} /> </button> <button className="tab"> Release <SparkBar data={releaseRuns} mode="winloss" width={44} height={14} /> </button> ``` ## Accessibility [#accessibility] The accessible name is generated 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) A quarter of revenue is six numbers in a row, and what you want from them is the shape. Sparkline draws those ordered values as one line, sized for a sentence, a table cell, or a KPI card. The default is the line plus an accent dot on the last point, which marks where "now" is; fill, a normal-range band, and min/max dots are props you turn on. ```tsx <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" title="Weekly revenue" /> ``` ## Install [#install] ```tsx import { Sparkline } from "@microcharts/react/sparkline"; <Sparkline data={[3, 5, 4, 8, 6, 9]} title="Weekly revenue" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Sparkline } from "@microcharts/react/sparkline/interactive"; <Sparkline data={[6, 14, 20, 12, 12, 14, 11, 19, 27, 23, 25, 25]} curve="smooth" dots="minmax" label="last" /> ``` ## When to use it [#when-to-use-it] Use it for an inline trend, a table-cell trend, a KPI sparkline, and dense dashboards. It won't answer part-to-whole comparisons or 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. **default** ```tsx // data alone → an intrinsic 80×20 box <Sparkline data={[3, 5, 4, 8, 6, 9]} /> ``` **fixed size** ```tsx // width & height are viewBox units — they also set the pixel box <Sparkline data={[3, 5, 4, 8, 6, 9]} width={200} height={48} /> ``` **responsive** ```tsx // omit width/height, let CSS drive the width — the viewBox keeps the ratio <div style={{ width: "100%", maxWidth: 320 }}> <Sparkline data={[3, 5, 4, 8, 6, 9]} style={{ width: "100%", height: "auto" }} /> </div> ``` ## Variants [#variants] Shape, fill, a normal-range band, and dots are props on the same data. Each is off by default: at word size an extra fill or band takes pixels away from the line itself. Open the Code tab on any demo to see the call. ```tsx `"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" curve="smooth" title="Smooth curve" /> ``` `curve="smooth"` interpolates with monotone cubic tangents, so the curve stays inside the range its own points span. A spike back down to zero cannot bow below zero on the way, and a filled area cannot cross its baseline. ```tsx `"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" title="Filled area" /> ``` ```tsx `"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="120" height="32" band="[4, 8]" title="Normal-range band" /> ``` ```tsx `"> <Sparkline data="[3, 5, 4, 8, 6, 9]" width="130" height="32" dots="minmax" label="last" title="Min/max dots" /> ``` ```tsx `" > <Sparkline data="[3, 5, 4, 8, 6, 9, 2, 7]" width="130" height="36" dots="minmax" label="minmax" title="Extremes labelled" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <Sparkline data="[3, 5, null, null, 6, 9, 4, 7]" width="130" height="32" title="Series with gaps" /> ``` ```tsx `"> <Sparkline data="[7]" width="80" height="32" title="One value" /> ``` ```tsx `"> <Sparkline data="[5, 5, 5, 5, 5, 5]" width="130" height="32" title="Flat series" /> ``` A `null` means "no measurement here": the line breaks at the gap and never interpolates across it, so an outage doesn't render 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, because a zero-span domain maps to the middle of the range, so flat data reads as level rather than 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. An envelope is used instead of a stride sample so that a single spike at point 1,500 of 2,000 still renders. Pass `maxPoints={Infinity}` to opt out. ```tsx i === 1500 ? 98 : 50 + Math.sin(i / 40) * 24 + ((i * 13) % 7), )} />`" > <Sparkline data="Array.from({ length: 2000 }, (_, i) => (i === 1500 ? 98 : 50 + Math.sin(i / 40) * 24 + ((i * 13) % 7)))" width="220" height="40" title="Two thousand points, one spike" /> ``` ## 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). **In a sentence** ```tsx <p> p95 latency this week{" "} <span className="mc-inline"> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={16} dots="none" summary={false} /> </span>{" "} — trending down. </p> ``` **In a table cell** ```tsx <tr> <td>checkout-api</td> <td> <Sparkline data={[48, 45, 44, 40, 38, 36, 33, 31]} width={64} height={18} dots="none" summary={false} /> </td> <td>31 ms</td> </tr> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">1,600</span> <span className="unit">concurrent, now</span> <Sparkline data={[1240, 1310, 1290, 1420, 1380, 1510, 1470, 1600]} width={90} height={28} fill summary={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> CPU <Sparkline data={[62, 65, 61, 68, 70, 66, 72, 75]} width={40} height={14} dots="none" summary={false} /> </button> ``` ## 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}` with no `title` 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 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, so the radius encodes time only, never value: an outer mark is a later date, not a bigger number. The value becomes a five-step opacity, and busy stretches render as darker arcs. Two adjacent turns place different years at the same angle; read that radial adjacency as calendar alignment. This is a **pattern instrument**: opacity is the weakest ordered channel, so for an exact day's value reach for `ActivityGrid` or `HeatStrip`. ```tsx <SpiralYear data="Array.from({ length: 52 }, (_, i) => { const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2)); return i === 29 ? 480 : s; })" title="Seasonality" size="80" /> ``` ## Install [#install] ```tsx import { SpiralYear } from "@microcharts/react/spiral-year"; <SpiralYear data={byWeek} title="Seasonality" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { SpiralYear } from "@microcharts/react/spiral-year/interactive"; <SpiralYear data={byWeek} /> ``` ## When to use it [#when-to-use-it] Use it for the seasonal shape of a year, for spotting a busy season or a quiet stretch, or as a compact "the year in one square". For an exact day's value use `ActivityGrid` or `HeatStrip`, and for a non-cyclic trend use `Sparkline`. Past about three years the turns pack too tightly to read. ## Sizing [#sizing] **arc marks for a continuous-ribbon feel** ```tsx <SpiralYear data={byWeek} mark="arc" /> ``` **three steps for the smallest sizes** ```tsx <SpiralYear data={byWeek} steps={3} /> ``` ## Variants [#variants] Month ticks are on by default: without a temporal anchor the spiral is texture, and the ticks are what make "when" readable. Five opacity steps is the ceiling, since ordered opacity supports about five discriminable levels; `steps={3}` drops to three for tiny sizes. ```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 }); <SpiralYear data={byWeek} mark="arc" /> <SpiralYear data={byWeek} steps={3} />`" > <span className="inline-flex gap-6 items-center"> <SpiralYear data="Array.from({ length: 52 }, (_, i) => { const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2)); return i === 29 ? 480 : s; })" mark="arc" size="64" summary="false" /> <SpiralYear data="Array.from({ length: 52 }, (_, i) => { const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2)); return i === 29 ? 480 : s; })" steps="3" size="56" summary="false" /> </span> ``` `locale` changes no in-chart mark, since opacity steps carry the value and no number is printed. 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; }); <SpiralYear data={byWeek} locale="de-DE" />`" > <SpiralYear data="Array.from({ length: 52 }, (_, i) => { const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2)); return i === 29 ? 1480 : s; })" locale="de-DE" title="Localized summary" size="64" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <SpiralYear data="[80, 120, null, 90, 140]" title="With a gap" size="56" /> ``` ```tsx `"> <SpiralYear data="[]" title="Empty" size="56" /> ``` A `null` day or week leaves a gap in the spiral, distinct from a faint step-one mark, because missing is not the same as low. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Revenue seasonality{" "} <span className="mc-inline"> <SpiralYear data={byWeek} summary={false} /> </span>{" "} — summer swell, outlier week 29. </p> ``` **In a table cell** ```tsx <td> <SpiralYear data={byWeek} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">30</span> <span className="unit">summer peak</span> <SpiralYear data={byWeek} /> </div> ``` **In a tab header** ```tsx <button className="tab"> 2024 <SpiralYear data={byWeek} /> </button> ``` Year spiral needs card scale — weekly rings collapse below ~36px. ## 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, and announces each mark with its period and value through a polite live region. 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. | | `size` | `number` | Spiral box edge in viewBox units (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). # SpreadBand (/docs/charts/spread-band) The gutter of the chart below reads **+8**: Organic is eight ahead of Paid, and it took that lead at point 3. SpreadBand fills the signed gap between a subject and its reference and splits the fill at every crossing, so the current lead and the last flip both render. The reference line is dashed, thinner, and neutral. Direction is carried by the sign of the fill and by the text, never by color alone. Both series share one domain: no dual axes, no per-series normalization. ```tsx <SpreadBand data="[ { a: 8, b: 12 }, { a: 9, b: 12 }, { a: 11, b: 13 }, { a: 12, b: 13 }, { a: 14, b: 13 }, { a: 15, b: 14 }, { a: 17, b: 14 }, { a: 18, b: 14 }, { a: 20, b: 15 }, { a: 21, b: 15 }, { a: 23, b: 16 }, { a: 24, b: 16 }, ]" seriesLabels="["Organic", "Paid"]" title="Organic vs paid" width="220" height="30" /> ``` ## Install [#install] ```tsx import { SpreadBand } from "@microcharts/react/spread-band"; <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} title="Organic vs paid" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { SpreadBand } from "@microcharts/react/spread-band/interactive"; <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} /> ``` ## When to use it [#when-to-use-it] Use it for lead-vs-reference in KPI cards, and for actual-vs-plan where the flip matters. Three or more series belong in SparkGroup; unpaired series or different units belong in separate charts, never on dual axes. ## Sizing [#sizing] **lead vs reference in a cell** ```tsx {rows.map((r) => ( <SpreadBand key={r.id} data={r.pairs} seriesLabels={["Us", "Market"]} title={r.name} /> ))} ``` **actual vs plan with the gap** ```tsx <SpreadBand data={pairs} seriesLabels={["Actual", "Plan"]} label="gap" /> ``` ## Variants [#variants] ```tsx `" > <SpreadBand data="[ { a: 8, b: 12 }, { a: 12, b: 13 }, { a: 14, b: 13 }, { a: 17, b: 14 }, { a: 20, b: 15 }, { a: 24, b: 16 }, ]" seriesLabels="["Organic", "Paid"]" label="none" title="No gutter label" width="220" height="30" /> ``` ```tsx `" > <SpreadBand data="[ { a: 18, b: 12 }, { a: 16, b: 12 }, { a: 14, b: 13 }, { a: 11, b: 13 }, { a: 9, b: 14 }, { a: 8, b: 14 }, ]" seriesLabels="["Latency", "Budget"]" positive="down" title="Lower is better" width="220" height="30" /> ``` ```tsx `" > <SpreadBand data="[ { a: 8000, b: 12000 }, { a: 9000, b: 12000 }, { a: 11000, b: 13000 }, { a: 12000, b: 13000 }, { a: 14000, b: 13000 }, { a: 15000, b: 14000 }, { a: 17000, b: 14000 }, { a: 18000, b: 14000 }, { a: 20000, b: 15000 }, { a: 21000, b: 15000 }, { a: 23000, b: 16000 }, { a: 24000, b: 16000 }, ]" seriesLabels="["Organic", "Paid"]" locale="de-DE" title="Localized values" width="230" height="30" /> ``` 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 `" > <SpreadBand data="[ { a: 10, b: 5 }, { a: 12, b: 6 }, { a: 14, b: 7 }, ]" seriesLabels="["A", "B"]" title="Never crosses" width="200" height="30" /> ``` ```tsx `" > <SpreadBand data="[ { a: 12, b: 12 }, { a: 15, b: 15 }, { a: 14, b: 14 }, { a: 18, b: 18 }, ]" title="Level throughout" width="200" height="30" /> ``` ```tsx `"> <SpreadBand data="[ { a: 12, b: 10 }, { a: 14, b: null }, { a: 13, b: 12 }, { a: 16, b: 13 }, ]" seriesLabels="["A", "B"]" title="Shared gaps" width="200" height="30" /> ``` 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 instead of inventing a value. Identical series read "The two series are level — no gap." A subject that never falls behind reports "never crossed." The fill splits exactly at the interpolated crossings, so a flip lands where the lines cross, even when that falls between two samples. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Organic vs paid{" "} <span className="mc-inline"> <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} summary={false} /> </span>{" "} — organic leads by 18% on median. </p> ``` **In a table cell** ```tsx <td> <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">+18%</span> <span className="unit">organic vs paid</span> <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Organic <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} /> </button> ``` ## 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) Six accounts sit in a portfolio table and each one is somewhere between just-signed and fully adopted. SproutRow gives each item 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 key. The four stages are fixed and discrete: no half-stages, no interpolation, and presets recolor the glyphs without reshaping one. ```tsx <SproutRow data="[ { label: "Acme", value: 3 }, { label: "Beta", value: 2 }, { label: "Gamma", value: 3 }, { label: "Delta", value: 1 }, { label: "Echo", value: 0 }, { label: "Foxtrot", value: 2 }, ]" title="Account health" height="30" step="24" /> ``` The key, shown once: **seed → sprout → leaf → bloom**. ## Install [#install] ```tsx import { SproutRow } from "@microcharts/react/sprout-row"; <SproutRow data={accounts} title="Account health" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { SproutRow } from "@microcharts/react/sprout-row/interactive"; <SproutRow data={accounts} /> ``` ## When to use it [#when-to-use-it] Use it for account or project maturity across a small set, a health column in a portfolio table, or per-item lifecycle in a KPI card. Continuous values belong in MiniBar and trends in Sparkline; past about twelve items the row stops being readable. ## Sizing [#sizing] **with category labels** ```tsx <SproutRow data={accounts} labels /> ``` **missing ≠ seed (null draws a soil tick only)** ```tsx <SproutRow data={[{ label: "A", value: 2 }, { label: "B", value: null }, { label: "C", value: 0 }]} /> ``` ## Variants [#variants] Labels are off by default, since the row usually sits beside its own row label and the heights carry the ordering. `label="value"` prints each item's stage number instead. ```tsx <SproutRow data={[{ label: "A", value: 2 }, { label: "B", value: null }, { label: "C", value: 0 }]} />`" > <span className="inline-flex gap-6 items-center"> <SproutRow data="[ { label: "A", value: 0 }, { label: "B", value: 1 }, { label: "C", value: 2 }, { label: "D", value: 3 }, ]" label="value" height="26" step="20" /> <SproutRow data="[ { label: "A", value: 2 }, { label: "B", value: null }, { label: "C", value: 0 }, ]" height="24" step="22" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <SproutRow data="[]" title="No accounts" height="20" step="20" /> ``` ```tsx `"> <SproutRow data="[ { label: "A", value: 2 }, { label: "B", value: null }, ]" title="One account missing" height="20" step="20" /> ``` ```tsx `"> <SproutRow data="[ { label: "A", value: -3 }, { label: "B", value: 12 }, ]" title="Clamped stages" height="20" step="20" /> ``` Empty data draws just the frame. A `null` value renders only the soil tick, visibly distinct from a seed: seed is stage 0 and gets its own glyph, and "no data yet" is a different state from "just planted". Values outside `[0, 3]` round and clamp to the nearest real stage, seed or bloom, instead of drawing a fractional or out-of-bounds glyph. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Account health this week{" "} <span className="mc-inline"> <SproutRow data={accounts} height={18} summary={false} /> </span>{" "} — two at bloom, one still seed. </p> ``` **In a table cell** ```tsx <td> <SproutRow data={[{ label: "Acme", value: 3 }]} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">4 / 6</span> <span className="unit">at leaf or better</span> <SproutRow data={accounts} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Accounts <SproutRow data={accounts} height={14} /> </button> ``` ## 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 and announces each as **"Acme: bloom, stage 4 of 4."**, the stage name plus a 1-of-4 index. A missing item reads "…: no data." 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). | | `fontSize` | `number` | Type size of the value and stage labels, in viewBox units. Defaults from `height`. | | `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) Traffic splits between mobile, web, and API, and the question is whether that mix is moving. StackedArea takes up to three labeled series over the same ordered points and normalizes each to a share before stacking it, so layer thickness is share and the stack always sums to 100%. That keeps the read on composition rather than on magnitude and composition together, and the shift over time is what the shape shows. Three series is the hard cap: beyond that, thickness at 16 px stops being readable. ```tsx <StackedArea data="[ { label: "Mobile", values: [30, 34, 36, 40, 44, 47, 52, 56, 58, 60, 63, 66] }, { label: "Web", values: [50, 48, 47, 45, 42, 41, 38, 36, 35, 33, 32, 30] }, { label: "API", values: [20, 18, 17, 15, 14, 12, 10, 8, 7, 7, 5, 4] }, ]" title="Traffic mix" width="220" height="30" /> ``` ## Install [#install] ```tsx import { StackedArea } from "@microcharts/react/stacked-area"; <StackedArea data={mix} title="Traffic mix" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { StackedArea } from "@microcharts/react/stacked-area/interactive"; <StackedArea data={mix} /> ``` ## When to use it [#when-to-use-it] Use it for traffic or revenue mix in KPI cards and share-shift reads in a sentence. Four or more series need a different chart; for exact values over time use a SparkGroup of Sparklines. ## Sizing [#sizing] **mix rows** ```tsx {regions.map((r) => ( <StackedArea key={r.id} data={r.mix} title={r.name} /> ))} ``` **ridge skin** ```tsx <StackedArea data={mix} mode="ridge" /> ``` ## Variants [#variants] `order="asc"` puts the smallest series on top, where curvature distorts thickness the least. ```tsx <StackedArea data={mix} order="asc" /> <StackedArea data={mix} locale="de-DE" label="last" />`" > <span className="inline-flex flex-col gap-3"> <StackedArea data="[ { label: "Mobile", values: [30, 34, 36, 40, 44, 47, 52, 56, 58, 60, 63, 66] }, { label: "Web", values: [50, 48, 47, 45, 42, 41, 38, 36, 35, 33, 32, 30] }, { label: "API", values: [20, 18, 17, 15, 14, 12, 10, 8, 7, 7, 5, 4] }, ]" mode="ridge" title="Ridge skin" width="220" height="30" /> <StackedArea data="[ { label: "Mobile", values: [30, 34, 36, 40, 44, 47, 52, 56, 58, 60, 63, 66] }, { label: "Web", values: [50, 48, 47, 45, 42, 41, 38, 36, 35, 33, 32, 30] }, { label: "API", values: [20, 18, 17, 15, 14, 12, 10, 8, 7, 7, 5, 4] }, ]" order="asc" title="Ascending order" width="220" height="30" /> <StackedArea data="[ { label: "Mobile", values: [30, 34, 36, 40, 44, 47, 52, 56, 58, 60, 63, 66] }, { label: "Web", values: [50, 48, 47, 45, 42, 41, 38, 36, 35, 33, 32, 30] }, { label: "API", values: [20, 18, 17, 15, 14, 12, 10, 8, 7, 7, 5, 4] }, ]" locale="de-DE" label="last" title="Localized shares" width="220" height="30" /> </span> ``` `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 `"> <StackedArea data="[ { label: "A", values: [10] }, { label: "B", values: [5] }, ]" title="Single column" width="160" height="30" /> ``` ```tsx `" > <StackedArea data="[ { label: "A", values: [0, 0, 0, 0] }, { label: "B", values: [5, 5, 5, 5] }, ]" title="A series at zero" width="160" height="30" /> ``` A single column still stacks normally: share is well-defined from one point, so it renders as a vertical slice with no width to run across. A series pinned at zero collapses to a zero-height layer instead of distorting the others, and the remaining series still sum to 100% of what's left. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Traffic mix this year{" "} <span className="mc-inline"> <StackedArea data={mix} width={80} height={16} summary={false} /> </span>{" "} — mobile overtook web, now 66% of sessions. </p> ``` **In a table cell** ```tsx <td> <StackedArea data={mix} width={60} height={18} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">47%</span> <span className="unit">subscriptions, down from 62%</span> <StackedArea data={mix} width={120} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Traffic <StackedArea data={mix} height={14} /> </button> ``` ## 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%."**), and the readout chip shows that same breakdown, one row per band. 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) The chart below prints five spokes from one center, each spoke's length one metric: Speed at 0.9, Cost at 0.3. That is an entity's profile, and a row of them shows which entity in a set is the odd one out. There is no connecting polygon, ever: the enclosed area of a radar chart misstates magnitude and changes with axis order, and the research favors contour-free marks for spotting outliers. One `domain` governs every spoke in a glyph, so normalizing mixed-unit metrics is the caller's job and a set of small multiples stays comparable. ```tsx <StarSpoke data="[ { label: "Speed", value: 0.9 }, { label: "Power", value: 0.6 }, { label: "Range", value: 0.5 }, { label: "Cost", value: 0.3 }, { label: "Ease", value: 0.7 }, ]" dots="tips" title="Product profile" size="110" /> ``` ## Install [#install] ```tsx import { StarSpoke } from "@microcharts/react/star-spoke"; <StarSpoke data={metrics} title="Product profile" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { StarSpoke } from "@microcharts/react/star-spoke/interactive"; <StarSpoke data={metrics} /> ``` ## When to use it [#when-to-use-it] Use it for entity profiles in small multiples and for skill or capability comparison. Fewer than 3 metrics read better as PairedBars, and precise values belong in MiniBar. ## Sizing [#sizing] **small multiple** ```tsx <StarSpoke data={row.metrics} size={28} /> ``` **vs baseline** ```tsx <StarSpoke data={metrics} compare={baseline} dots="tips" /> ``` ## Variants [#variants] ```tsx `" > <StarSpoke data="[ { label: "Speed", value: 0.9 }, { label: "Power", value: 0.6 }, { label: "Range", value: 0.5 }, { label: "Cost", value: 0.3 }, { label: "Ease", value: 0.7 }, ]" compare="[0.5, 0.5, 0.5, 0.5, 0.5]" dots="tips" title="vs baseline" size="110" /> ``` `locale` changes no in-chart mark, since tip labels are metric names rather than formatted numbers. It does localize the extremes named in the accessible summary: ```tsx `" > <StarSpoke data="[ { label: "Speed", value: 1900 }, { label: "Power", value: 600 }, { label: "Range", value: 500 }, ]" domain="[0, 2000]" locale="de-DE" title="Localized summary" size="110" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <StarSpoke data="[ { label: "Reliability", value: 0.9 }, { label: "Throughput", value: 0.6 }, { label: "Cost efficiency", value: 0.5 }, ]" title="Small, long labels" size="48" /> ``` ```tsx `"> <StarSpoke data="[]" title="Empty" size="64" /> ``` Every spoke sits at a fixed clock position, the first at 12 o'clock and the rest clockwise, so the axis order never shifts between instances. That fixed order plus the default-on guide spokes keep a profile readable where a metric's label doesn't seat. Tip labels seat at the rim rather than at 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, which keeps rim labels at distinct angles. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Product profile{" "} <span className="mc-inline"> <StarSpoke data={metrics} summary={false} /> </span>{" "} — strong on speed, weak on cost. </p> ``` **In a table cell** ```tsx <td> <StarSpoke data={metrics} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.9</span> <span className="unit">top dimension</span> <StarSpoke data={metrics} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Product A <StarSpoke data={metrics} /> </button> ``` Best at KPI/card scale — spoke tips need room to read. ## 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). | | `size` | `number` | Star box edge in viewBox units (default 80). | | `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) A weather table has one row per station and five fields to fit in each cell. StationGlyph packs the whole 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. Each field rides its own channel, so nothing has to be inferred from a shared one, and the numerals sit where a forecaster expects them. ```tsx <StationGlyph station="KSFO" cloud="0.75" wind="{ direction: 225, magnitude: 15 }" temp="16" dewpoint="9" pressure="1013" size="48" /> ``` ## Install [#install] ```tsx import { StationGlyph } from "@microcharts/react/station-glyph"; <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} temp={16} dewpoint={9} pressure={1013} /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { StationGlyph } from "@microcharts/react/station-glyph/interactive"; <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} temp={16} dewpoint={9} pressure={1013} /> ``` ## When to use it [#when-to-use-it] Use it for a dense weather station model, or any multi-field reading that has to fit one cell. A single value belongs in Delta and a trend over time in Sparkline. ## Sizing [#sizing] **clear + calm** ```tsx <StationGlyph station="STN" cloud={0} wind={{ direction: 0, magnitude: 0 }} temp={22} dewpoint={8} /> ``` **overcast + gale** ```tsx <StationGlyph station="KJFK" cloud={1} wind={{ direction: 300, magnitude: 45 }} temp={4} dewpoint={2} pressure={988} /> ``` ## Variants [#variants] ```tsx `"> <StationGlyph station="KJFK" cloud="1" wind="{ direction: 300, magnitude: 45 }" temp="4" dewpoint="2" pressure="988" size="48" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <StationGlyph station="STN" cloud="0.3" wind="{ direction: 0, magnitude: 0 }" size="44" /> ``` ```tsx `"> <StationGlyph cloud="0.4" size="44" /> ``` ```tsx `"> <StationGlyph size="44" /> ``` ```tsx `" > <StationGlyph station="EDDF" cloud="0.6" wind="{ direction: 90, magnitude: 22 }" temp="-3" dewpoint="-9" pressure="1024" format="{ maximumFractionDigits: 0 }" locale="de-DE" size="48" /> ``` Sky cover is disc area and wind is the barb: direction as angle, speed quantized into barbs so the per-barb quantum holds. Wind speed is quantized rather than drawn to scale for the same reason WindBarb quantizes it, that a barb count reads exactly where a tiny length difference does not. Below the quarter-step threshold no barb is drawn at all. Absent fields don't render: no barb, no numeral, and no stop for the keyboard. The disc is the one field with no absent state, so omitting `cloud` looks and reads exactly like a clear sky. `format`/`locale` flow through every corner numeral (temp, dew point, pressure) and through the wind speed and numerals named in the accessible summary: one formatter, one locale, no per-field overrides. ## Four homes [#four-homes] **In a sentence** ```tsx <p> KSFO conditions{" "} <span className="mc-inline"> <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} summary={false} /> </span>{" "} — overcast, SW 15 kt, 16°C. </p> ``` **In a table cell** ```tsx <td> <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">16°C</span> <span className="unit">· 1013 hPa</span> <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> KSFO <StationGlyph station="KSFO" cloud={0.75} wind={{ direction: 225, magnitude: 15 }} /> </button> ``` ## 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. 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. | | `size` | `number` | Disc + barb square edge in viewBox units (default 48). | | `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 prints one mark for one state: a filled circle for `ok`, a triangle for `warn`, a diamond for `error`, a hollow ring for `off`, a half-filled circle for `busy`. Every state pairs a distinct silhouette with a semantic color, so the five stay distinct in grayscale, in print, and in forced-colors, where color-only dots all render as the same gray circle. That pairing is also what keeps two states from collapsing into the same mark for the 1-in-12 colorblind readers a colored disc fails. ```tsx <span className="text-lg"> {"The API is "} <StatusDot status="ok" title="API" style="{ width: "0.6em", height: "0.6em" }" /> {" operational."} </span> ``` ## Install [#install] ```tsx import { StatusDot } from "@microcharts/react/status-dot"; <StatusDot status="ok" title="API" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { StatusDot } from "@microcharts/react/status-dot/interactive"; <StatusDot status="ok" /> ``` ## When to use it [#when-to-use-it] Use it for service lists, inline state in a sentence, and monitoring rows. It carries no quantity and no trend, and past \~6 states the 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. ## Sizing [#sizing] **inline in a sentence** ```tsx // em-sized, it sits on the text midline The API is <StatusDot status="ok" style={{ width: "0.6em", height: "0.6em" }} /> operational. ``` **custom vocabulary** ```tsx <StatusDot status="degraded" states={{ degraded: { glyph: "triangle", token: "--mc-cat-1", label: "degraded" } }} /> ``` ## Variants [#variants] ```tsx `" > <StatusDot status="ok" title="Live" style="{ width: 18, height: 18 }" /> ``` ```tsx `" > <StatusDot status="degraded" states="{ degraded: { glyph: "triangle", token: "--mc-cat-1", label: "degraded" } }" title="Search" style="{ width: 18, height: 18 }" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <StatusDot status="degraded" title="Unknown key" style="{ width: 18, height: 18 }" /> ``` An unknown status key renders the `off` ring and logs a dev warning, so a typo never renders as a plausible-looking wrong state. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Checkout is{" "} <span className="mc-inline"> <StatusDot status="ok" style={{ width: "0.7em", height: "0.7em" }} summary={false} /> </span>{" "} operational; Billing is{" "} <span className="mc-inline"> <StatusDot status="warn" style={{ width: "0.7em", height: "0.7em" }} summary={false} /> </span>{" "} degraded. </p> ``` **In a table cell** ```tsx <td> <StatusDot status="warn" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">2 / 5</span> <span className="unit">fully healthy</span> <StatusDot status="ok" /> <StatusDot status="warn" /> <StatusDot status="error" /> <StatusDot status="ok" /> <StatusDot status="off" /> </div> ``` **In a tab header** ```tsx <button className="tab"> Staging <StatusDot status="busy" pulse /> </button> ``` ## Accessibility [#accessibility] The accessible name is the state's label, **"Status: ok."**, composed with your `title` to read "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<string, { glyph; token; label }>` | 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 collapses a sequence of pass/fail outcomes 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 accent bar at the right. The record streak carries a small triangle tick, so "are we near our best" reads without counting. Height and opacity encode run type, never magnitude, which leaves width as the only channel answering how long a run is. ```tsx `" > <StreakSpark data="[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1]" label="both" title="Deploy streak" width="300" height="48" /> ``` ## Install [#install] ```tsx import { StreakSpark } from "@microcharts/react/streak-spark"; // 1 = passing build, 0 = failing build <StreakSpark data={[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1]} title="Deploy streak" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { StreakSpark } from "@microcharts/react/streak-spark/interactive"; <StreakSpark data={[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1]} /> ``` ## When to use it [#when-to-use-it] Use it for pass/fail run histories, uptime and incident-free streaks, and the current run against a record. A continuous magnitude belongs in SparkBar and a single completion ratio in Progress. ## Sizing [#sizing] **default** ```tsx // data alone → an intrinsic 96×20 box <StreakSpark data={[1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1]} /> ``` **fixed size** ```tsx // width & height are viewBox units — they also set the pixel box <StreakSpark data={[1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1]} width={200} height={48} label="both" /> ``` **responsive** ```tsx // omit width/height, let CSS drive the width — the viewBox keeps the ratio <div style={{ width: "100%", maxWidth: 320 }}> <StreakSpark data={[1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1]} style={{ width: "100%", height: "auto" }} /> </div> ``` ## 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 `" > <StreakSpark data="[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1]" label="both" title="Current and record" width="300" height="48" /> ``` ```tsx `" > <StreakSpark data="[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0]" positive="down" label="both" title="Incident-free streak" width="300" height="48" /> ``` ```tsx = threshold — e.g. daily uptime %, target 99.5 <StreakSpark data={[99.9, 99.8, 99.6, 98.2, 99.7, 99.9, 99.6, 97.0, 99.8, 99.9]} threshold={99.5} label="both" />`" > <StreakSpark data="[99.9, 99.8, 99.6, 98.2, 99.7, 99.9, 99.6, 97.0, 99.8, 99.9]" threshold="99.5" label="both" title="Uptime streak" width="300" height="48" /> ``` ```tsx 1)} format={{ useGrouping: true }} locale="de-DE" />`" > <StreakSpark data="Array.from({ length: 1204 }, () => 1)" format="{ useGrouping: true }" locale="de-DE" label="current" title="Localized run length" width="220" height="48" /> ``` 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 `"> <StreakSpark data="[1]" title="One build" width="160" height="24" /> ``` A lone outcome is one current run of length 1, and it is its own record: the summary reads **"Current run 1 passing, unbroken."** ```tsx `"> <StreakSpark data="[0, 0, 0, 0, 0]" title="Rough week" width="200" height="24" /> ``` 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."** ## Four homes [#four-homes] **In a sentence** ```tsx <p> Deploy streak{" "} <span className="mc-inline"> <StreakSpark data={data}${size} summary={false} /> </span>{" "} — 7 greens in a row, last fail 3 days ago. </p> ``` **In a table cell** ```tsx <td> <StreakSpark data={data}${size} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">7</span> <span className="unit">consecutive passes</span> <StreakSpark data={data}${size} /> </div> ``` **In a tab header** ```tsx <button className="tab"> prod <StreakSpark data={data}${size} /> </button> ``` ## 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) The chart below prints 23 as four clusters of five plus three loose strokes, the way a person counts. The count reads back exactly: there is no scale to calibrate and no color to decode. Past `total` the marks stop growing and a `+N` numeral carries the rest, 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 <TallyMarks value="23" title="Signatures" height="20" /> ``` ## Install [#install] ```tsx import { TallyMarks } from "@microcharts/react/tally-marks"; <TallyMarks value={23} title="Signatures" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TallyMarks } from "@microcharts/react/tally-marks/interactive"; <TallyMarks value={23} /> ``` ## When to use it [#when-to-use-it] Use it for a small running count in a sentence or cell, a live event or score counter, or an editorial hand-tallied context. Large magnitudes belong in MiniBar, trends over time in Sparkline, and proportions in Progress. ## Sizing [#sizing] **hand-drawn pen for an editorial count** ```tsx <TallyMarks value={17} pen="drawn" /> ``` **cap the width — the numeral tells the truth past total** ```tsx <TallyMarks value={38} total={20} /> ``` ## Variants [#variants] The ruled pen is the default, because evenly ruled strokes count back fastest in a product table. The `drawn` pen adds a seeded, deterministic jitter for an editorial voice: the same count always renders identically, on the server and after hydration, and the jitter only changes how each stroke is drawn, never the count. ```tsx <TallyMarks value={38} total={20} />`" > <span className="inline-flex gap-4 items-center"> <TallyMarks value="17" pen="drawn" height="20" /> <TallyMarks value="38" total="20" height="20" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <TallyMarks value="0" height="20" /> ``` ```tsx `"> <TallyMarks value="-5" height="20" /> ``` ```tsx `"> <TallyMarks value="5000" total="5000" height="20" /> ``` A negative value clamps to 0 and a non-physical value saturates at the 200-mark ceiling. 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Petition signatures{" "} <span className="mc-inline"> <TallyMarks value={12} summary={false} /> </span>{" "} — 23 collected, 27 to goal. </p> ``` **In a table cell** ```tsx <td> <TallyMarks value={12} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">23</span> <span className="unit">of 50 goal</span> <TallyMarks value={12} /> </div> ``` **In a tab header** ```tsx <button className="tab"> D1 <TallyMarks value={12} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the exact count, **"17 counted."**, and stays 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) Airspeed is 142 and climbing, and the reading you need is the current one. TapeGauge parks the value at a fixed center pointer and scrolls the scale past it, so the current reading is always in the same place. It is the aviation primary-flight-display tape at word size, and it suits any single live reading: airspeed, throughput, temperature, queue depth. Semantic zones mark the safe, caution, and danger bands. A stack of chevrons encodes the rate of change on a channel separate from the level, so a fast climb and a high value never read as the same thing. The visible span stays fixed while the value updates, so a jump renders as motion of the scale rather than a silent rescale. ```tsx <TapeGauge value="142" rate="1" zones="[ { from: 100, to: 130, tone: "pos" }, { from: 130, to: 150, tone: "warn" }, { from: 150, to: 200, tone: "neg" }, ]" span="60" title="Airspeed" width="46" height="72" /> ``` ## Install [#install] ```tsx import { TapeGauge } from "@microcharts/react/tape-gauge"; <TapeGauge value={142} rate={1} zones={zones} span={60} title="Airspeed" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TapeGauge } from "@microcharts/react/tape-gauge/interactive"; <TapeGauge value={142} rate={1} zones={zones} /> ``` ## When to use it [#when-to-use-it] Use it for a live changing reading with a safe/caution band, where you need the value and its trend together. A history you want to scan belongs in Sparkline, and a single static number in Delta. ## Sizing [#sizing] **KPI card** ```tsx <TapeGauge value={142} rate={1} zones={zones} span={60} title="Airspeed" /> ``` **horizontal cell** ```tsx <TapeGauge value={142} rate={-1} zones={zones} span={60} orientation="horizontal" /> ``` ## Variants [#variants] ```tsx // label="none" drops the in-chart readout for an external number <TapeGauge value={142} rate={1} zones={zones} span={60} label="none" />`" > <span className="inline-flex items-end gap-6"> <TapeGauge value="142" rate="-1" zones="[ { from: 100, to: 130, tone: "pos" }, { from: 130, to: 150, tone: "warn" }, { from: 150, to: 200, tone: "neg" }, ]" span="60" orientation="horizontal" title="Airspeed" width="160" height="30" /> <TapeGauge value="142" rate="1" zones="[ { from: 100, to: 130, tone: "pos" }, { from: 130, to: 150, tone: "warn" }, { from: 150, to: 200, tone: "neg" }, ]" span="60" label="none" title="Airspeed" width="30" height="44" /> </span> ``` ```tsx `" > <TapeGauge value="14200" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Localized reading" width="46" height="68" /> ``` `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 `" > <TapeGauge value="NaN" title="Sensor offline" width="46" height="68" /> ``` ```tsx `" > <TapeGauge value="142" rate="1" span="40" title="Airspeed, unzoned" width="46" height="68" /> ``` A non-finite `value` renders an empty gauge: no pointer, no readout. `zones` are optional, and without them the gauge still tracks level and rate. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Airspeed reading{" "} <span className="mc-inline"> <TapeGauge value={142} rate={1} zones={zones} span={60} orientation="horizontal" summary={false} /> </span>{" "} — 142 knots, rising into caution band. </p> ``` **In a table cell** ```tsx <td> <TapeGauge value={142} rate={1} zones={zones} span={60} orientation="horizontal" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">142</span> <span className="unit">knots ↑</span> <TapeGauge value={142} rate={1} zones={zones} span={60} orientation="horizontal" /> </div> ``` **In a tab header** ```tsx <button className="tab"> Cruise <TapeGauge value={142} rate={1} zones={zones} span={60} orientation="horizontal" /> </button> ``` ## 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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 fills a ticked tube from the bottom of its range, and the ticks calibrate the read, which is what buys the precision. The fill always anchors at the bottom of the domain: never re-zeroed, never log. An optional target line crosses the tube to mark the goal. `domain` defaults to `[0, 100]` because a calibrated instrument needs a stated range, and auto-fitting would move the scale under the reader. The bulb is instrument chrome and always full; it is not data, so its area means nothing. ```tsx <Thermometer value="72" target="80" title="Fundraiser" height="56" /> ``` ## Install [#install] ```tsx import { Thermometer } from "@microcharts/react/thermometer"; <Thermometer value={72} target={80} title="Fundraiser" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Thermometer } from "@microcharts/react/thermometer/interactive"; <Thermometer value={72} target={80} /> ``` ## When to use it [#when-to-use-it] Use it for a fundraising or goal-progress read, a capacity or utilization gauge in a cell, or any value against a stated range. Trends belong in Sparkline and proportions of a whole in SegmentedBar; it reads a single value, so many series need a different chart. ## Sizing [#sizing] **horizontal, in a table cell** ```tsx <Thermometer value={62} orientation="horizontal" bulb={false} /> ``` **explicit calibration ticks** ```tsx <Thermometer value={72} domain={[32, 100]} ticks={[32, 50, 68, 86, 100]} /> ``` ## Variants [#variants] Vertical with the bulb is the default, since a filling tube reads as progress toward the goal. `orientation="horizontal"` fits a table cell where a vertical tube cannot, and `bulb={false}` drops the reservoir where the instrument metaphor is too much for the context. ```tsx <Thermometer value={72} domain={[32, 100]} ticks={[32, 50, 68, 86, 100]} />`" > <span className="inline-flex gap-6 items-center"> <Thermometer value="62" orientation="horizontal" bulb="false" width="110" /> <Thermometer value="72" domain="[32, 100]" ticks="[32, 50, 68, 86, 100]" height="60" /> </span> ``` ## Edge cases [#edge-cases] ```tsx `"> <Thermometer value="130" domain="[0, 100]" title="Overrun" height="48" /> ``` ```tsx `"> <Thermometer value="0" title="Empty" height="48" /> ``` ```tsx `" > <Thermometer value="7234" domain="[0, 10000]" target="8000" format="{ maximumFractionDigits: 0 }" locale="de-DE" label="value" title="Fundraiser" height="56" /> ``` A value beyond the domain clamps the fill, and the accessible name still reports the true number, so the reading is never silently clipped. 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Fundraiser progress{" "} <span className="mc-inline"> <Thermometer value={72} target={80} orientation="horizontal" bulb={false} summary={false} /> </span>{" "} — $72K raised, $8K from goal. </p> ``` **In a table cell** ```tsx <td> <Thermometer value={72} target={80} orientation="horizontal" bulb={false} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$72K</span> <span className="unit">of $80K goal</span> <Thermometer value={72} target={80} orientation="horizontal" bulb={false} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Annual <Thermometer value={72} target={80} orientation="horizontal" bulb={false} /> </button> ``` ## 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). Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark does not print it; `readout={false}` drops the chip and keeps everything else. ## 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). | | `fontSize` | `number` | Type size of the tick and value numerals, in viewBox units (default 8). | | `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) 72% in range, 9% below, 19% above. TimeInRange prints the share of a period that stayed inside its corridor and which side the rest missed on, with the in-range percent leading. Zones sit in a fixed order (below, in, above), so the strip reads by position first and color second. They are never sorted by magnitude, because which side a miss landed on is half the reading. ```tsx <TimeInRange data="{ below: 9, in: 72, above: 19 }" title="Time in range" width="240" height="22" /> ``` ## Install [#install] ```tsx import { TimeInRange } from "@microcharts/react/time-in-range"; <TimeInRange data={{ below: 9, in: 72, above: 19 }} title="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. ## Try it [#try-it] ```tsx import { TimeInRange } from "@microcharts/react/time-in-range/interactive"; <TimeInRange data={{ below: 9, in: 72, above: 19 }} /> ``` ## When to use it [#when-to-use-it] Use it for SLO and uptime corridors, glucose-style time-in-range, and thermal or budget bands. To rank parts use SegmentedBar; for a single ratio use Progress. ## Sizing [#sizing] **table cell** ```tsx <TimeInRange data={row.tir} width={60} height={10} /> ``` **clinical column** ```tsx <TimeInRange data={tir} orientation="vertical" label="all" /> ``` ## Variants [#variants] ```tsx `" > <TimeInRange data="{ severeBelow: 2, below: 7, in: 72, above: 15, severeAbove: 4 }" orientation="vertical" label="all" title="Vertical" width="28" height="130" /> ``` ```tsx `"> <TimeInRange data="{ severeBelow: 3, below: 8, in: 64, above: 18, severeAbove: 7 }" title="Severity tiers" width="240" height="22" /> ``` Severity tiers are drawn in the same hue at greater ink weight, so a severe zone never depends on color alone. ## Edge cases [#edge-cases] ```tsx `" > <TimeInRange data="{ below: 0, in: 0, above: 0 }" title="No readings yet" width="160" height="16" /> ``` ```tsx `" > <TimeInRange data="{ below: 3, in: 90, above: 7 }" label="all" title="Mostly in range" width="60" height="10" /> ``` Percent labels use largest-remainder rounding, so they always sum to 100 and the summary reads the same integers as the labels. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Glucose time-in-range{" "} <span className="mc-inline"> <TimeInRange data={{ below: 9, in: 72, above: 19 }} summary={false} /> </span>{" "} — 72% in range, 9% below. </p> ``` **In a table cell** ```tsx <td> <TimeInRange data={{ below: 9, in: 72, above: 19 }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">72%</span> <span className="unit">glucose TIR</span> <TimeInRange data={{ below: 9, in: 72, above: 19 }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Patient A <TimeInRange data={{ below: 9, in: 72, above: 19 }} /> </button> ``` ## Accessibility [#accessibility] The accessible name leads with the headline, then the misses: **"72% in range, 7% below, 15% above, 2% severe low, 4% severe high."** The interactive entry roves the zones, and 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 underlines each token by how confident the model was. The text itself is the chart, and confidence resolves to three discrete tiers rather than a continuous ramp. Confident tokens get no mark at all, so reading stays primary and only the uncertain words draw the eye. The two flagged tiers differ in stroke as well as color: unsure is a solid underline, guessing a dotted one. ```tsx `" > <TokenConfidence data="[ { token: "The", confidence: 0.98 }, { token: " Treaty", confidence: 0.93 }, { token: " of", confidence: 0.99 }, { token: " Westphalia", confidence: 0.71 }, { token: " was", confidence: 0.96 }, { token: " signed", confidence: 0.9 }, { token: " in", confidence: 0.97 }, { token: " 1648", confidence: 0.44 }, { token: ", ending the Thirty", confidence: 0.63 }, { token: " Years' War", confidence: 0.85 }, { token: " over", confidence: 0.31 }, { token: " a decade", confidence: 0.38 }, { token: ".", confidence: 0.99 }, ]" title="Model answer" style="{ fontSize: "1.05rem" }" /> ``` ## Install [#install] ```tsx import { TokenConfidence } from "@microcharts/react/token-confidence"; <TokenConfidence data={tokens} title="Model answer" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TokenConfidence } from "@microcharts/react/token-confidence/interactive"; <TokenConfidence data={tokens} tiers={[0.50, 0.80]} /> ``` ## When to use it [#when-to-use-it] Use it for LLM answers in chat or transcripts, and for flagging text a reader should review. To audit exact probabilities use [CalibrationStrip](/docs/charts/calibration-strip); for a single score use 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 `" > <TokenConfidence data="[ { token: "Likely", confidence: 0.92 }, { token: " Paris", confidence: 0.62 }, { token: ", maybe", confidence: 0.3 }, { token: " Lyon", confidence: 0.28 }, ]" style="{ fontSize: "1.05rem" }" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <TokenConfidence data="[ { token: "The", confidence: 0.95 }, { token: " answer", confidence: 0.99 }, { token: " is", confidence: 0.97 }, { token: " Paris", confidence: 0.9 }, ]" style="{ fontSize: "1.05rem" }" /> ``` Every token clears the `hi` threshold, so nothing is flagged and the sentence renders as plain, unmarked text. No marks means no word here needs a second look. ```tsx `"> <TokenConfidence data="[]" style="{ fontSize: "1.05rem" }" /> ``` An empty `data` array renders nothing and reports **"No tokens."** to assistive tech. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Model answer confidence{" "} <span className="mc-inline"> <TokenConfidence data={tokens} summary={false} /> </span>{" "} — high on facts, dips on dates. </p> ``` **In a table cell** ```tsx <td> <TokenConfidence data={tokens} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">98%</span> <TokenConfidence data={tokens} /> </div> ``` **In a tab header** ```tsx <button className="tab"> facts <TokenConfidence data={tokens} /> </button> ``` ## 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. Hovering or focusing a flagged token also floats that reading over it as a chip (`guessing 0.22`): the underline says the token is flagged, the chip gives its tier and number. `readout={false}` drops the chip. ## 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) A request took 214 ms and you need to know which spans set that number. TraceFold draws one rect per span, placed by its start time, sized by its duration, and stacked by its nesting depth. Widths sit on one shared linear time scale, never normalized per row, so a wide span is a slow span. The critical path, the chain of spans that bound the total, is accented and every other span muted. ```tsx \`\${Math.round(n)} ms\`} title="Request trace" />`" > <TraceFold data="[ { label: "request", start: 0, duration: 214, depth: 0 }, { label: "db.query", start: 10, duration: 86, depth: 1, parent: 0 }, { label: "auth", start: 0, duration: 8, depth: 1, parent: 0 }, { label: "render", start: 96, duration: 60, depth: 1, parent: 0 }, { label: "serialize", start: 156, duration: 40, depth: 1, parent: 0 }, { label: "index-scan", start: 12, duration: 70, depth: 2, parent: 1 }, { label: "decode", start: 82, duration: 12, depth: 2, parent: 1 }, { label: "log", start: 200, duration: 14, depth: 1, parent: 0 }, { label: "gc", start: 90, duration: 5, depth: 2, parent: 1 }, ]" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="Request trace" width="300" height="48" /> ``` ## Install [#install] ```tsx import { TraceFold } from "@microcharts/react/trace-fold"; <TraceFold data={spans} title="Request trace" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TraceFold } from "@microcharts/react/trace-fold/interactive"; <TraceFold data={spans} /> ``` ## When to use it [#when-to-use-it] Use it for request traces, flame charts, and p95-exemplar latency breakdowns. For a single duration use Bullet; for a time series use Sparkline. ## Sizing [#sizing] **endpoint cell** ```tsx <TraceFold data={row.spans} labels={false} width={80} height={24} /> ``` **structure audit** ```tsx <TraceFold data={spans} emphasis="none" /> ``` ## Variants [#variants] ```tsx \`\${Math.round(n)} ms\`} />`" > <TraceFold data="[ { label: "request", start: 0, duration: 214, depth: 0 }, { label: "db.query", start: 10, duration: 86, depth: 1, parent: 0 }, { label: "render", start: 96, duration: 60, depth: 1, parent: 0 }, { label: "index-scan", start: 12, duration: 70, depth: 2, parent: 1 }, ]" emphasis="none" format="{ style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 0 }" title="Structure" width="300" height="44" /> ``` `emphasis="none"` drops the accent when the nesting, not the latency, is what you are reading. The critical-path walk itself is deterministic: ties resolve to the earliest start. ```tsx `" > <TraceFold data="[ { label: "request", start: 0, duration: 14868, depth: 0 }, { label: "db.query", start: 1200, duration: 9234, depth: 1, parent: 0 }, ]" format="{ style: "unit", unit: "millisecond", unitDisplay: "narrow", maximumFractionDigits: 0 }" locale="de-DE" title="Localized durations" width="220" height="32" /> ``` `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 `"> <TraceFold data="[{ label: "request", start: 0, duration: 42, depth: 0 }]" title="Single span" width="160" height="24" /> ``` ```tsx `" > <TraceFold data="[ { label: "request", start: 0, duration: 120, depth: 0 }, { label: "db.query", start: 10, duration: 70, depth: 1, parent: 0 }, { label: "index-scan", start: 12, duration: 40, depth: 2, parent: 1 }, ]" title="Squeezed" width="200" height="24" /> ``` A zero-duration span keeps a one-unit floor so it stays visible. That is the only width distortion in the chart. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Request trace{" "} <span className="mc-inline"> <TraceFold data={spans} summary={false} /> </span>{" "} — 214 ms total, DB span dominates. </p> ``` **In a table cell** ```tsx <td> <TraceFold data={spans} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">214ms</span> <span className="unit">total trace</span> <TraceFold data={spans} /> </div> ``` **In a tab header** ```tsx <button className="tab"> /api <TraceFold data={spans} /> </button> ``` ## Accessibility [#accessibility] The accessible name gives 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 shows how growth accumulated period over period, the way a tree records it: one ring per period, oldest at the center, each ring's **thickness** carrying 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 rather than the wedges. The current period is drawn 1.5× heavier in the accent color, weight and color together. For exact per-period reads use `SparkBar`. ```tsx <TreeRings data="[8, 12, 10, 18, 22, 15, 20, 14]" label="last" unit="years" periodWord="year" title="Account age" size="40" /> ``` ## Install [#install] ```tsx import { TreeRings } from "@microcharts/react/tree-rings"; <TreeRings data={years} unit="years" periodWord="year" title="Account age" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TreeRings } from "@microcharts/react/tree-rings/interactive"; <TreeRings data={years} /> ``` ## When to use it [#when-to-use-it] Use it for account or company age, a cohort-age marker in a table cell, or a per-period growth story in a KPI card. Avoid it for exact per-period reads (SparkBar), for more than twenty-four periods, and for non-cumulative series. ## Sizing [#sizing] **filled annuli for print / e-ink** ```tsx <TreeRings data={years} rings="fill" /> ``` **cohort age — total sets the expected lifetime** ```tsx <TreeRings data={years} total={200} /> // this account is part-grown ``` ## Variants [#variants] ```tsx <TreeRings data={years} total={200} />`" > <span className="inline-flex gap-6 items-center"> <TreeRings data="[8, 12, 10, 18, 22, 15, 20, 14]" rings="fill" size="44" /> <TreeRings data="[8, 12, 10, 18, 22, 15, 20, 14]" total="200" size="44" /> </span> ``` Stroke rings are the default: at twenty-four pixels, hairlines keep the disc quiet and let the accent ring stay loud. `rings="fill"` draws filled annuli instead. Pass `total` for the cohort-age story, and the disc fills only Σdata/total of the radius, so a young account reads as part-grown. ```tsx `"> <TreeRings data="[820, 1240, 990, 1810]" label="last" locale="de-DE" title="Localized growth" size="44" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <TreeRings data="[8, 12, 0, 18, 22]" title="A flat quarter" size="44" /> ``` ```tsx `"> <TreeRings data="[]" title="New account" size="44" /> ``` TreeRings enforces no minimum visual thickness. A near-zero period looks near-zero, and a zero-value period collapses its two boundaries onto each other. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Account age{" "} <span className="mc-inline"> <TreeRings data={years} summary={false} /> </span>{" "} — 8 years, last year the thickest ring. </p> ``` **In a table cell** ```tsx <td> <TreeRings data={years} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">8 yr</span> <span className="unit">account tenure</span> <TreeRings data={years} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Acme <TreeRings data={years} /> </button> ``` ## 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 center 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"). | | `size` | `number` | Rings box edge in viewBox units (default 24). | | `fontSize` | `number` | Type size of the gutter label, in viewBox units. Defaults from `size`. | | `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) A latency number moved 8% and the table cell has room for one glyph. TrendArrow encodes the direction in the shape: up, down, or a flat bar. Color reinforces the shape, so the read survives grayscale, print, and forced-colors. The glyph never scales with magnitude: an arrow twice as long would claim precision this size can't carry. When the magnitude matters, reach for [Delta](/docs/charts/delta) instead. ```tsx `" > <span className="text-lg"> {"Latency "} <TrendArrow value="-0.08" positive="down" format="{ style: "percent", maximumFractionDigits: 0 }" title="Latency vs last week" style="{ width: "1em", height: "1em" }" /> {" improved again this week."} </span> ``` ## Install [#install] ```tsx import { TrendArrow } from "@microcharts/react/trend-arrow"; <TrendArrow value={-0.08} positive="down" title="Latency vs last week" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { TrendArrow } from "@microcharts/react/trend-arrow/interactive"; <TrendArrow value={0.12} format={{ style: "percent", maximumFractionDigits: 0 }} /> ``` ## When to use it [#when-to-use-it] Use it for table direction columns, dense dashboards, and inline movement cues. For exact magnitudes use Delta; for the shape of a series use 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. **matches the text** ```tsx // size the glyph in em so it rides the surrounding type <span> Errors <TrendArrow value={0.4} positive="down" style={{ width: "1em", height: "1em" }} /> </span> ``` **with the number** ```tsx // showValue widens the viewBox for the formatted value <TrendArrow value={0.12} showValue format={{ style: "percent" }} style={{ height: 22 }} /> ``` ## Variants [#variants] Three glyph weights for three densities, a declared noise floor, and the number itself when the glyph alone is too terse. The default arrow keeps its shaft and head legible at 8 px and in forced-colors, where lighter marks blur. ```tsx <TrendArrow value={1} glyph="triangle" /> <TrendArrow value={1} glyph="chevron" />`" > <span className="inline-flex items-center gap-3"> <TrendArrow value="1" glyph="arrow" title="Arrow" style="{ width: 20, height: 20 }" /> <TrendArrow value="1" glyph="triangle" title="Triangle" style="{ width: 20, height: 20 }" /> <TrendArrow value="1" glyph="chevron" title="Chevron" style="{ width: 20, height: 20 }" /> </span> ``` ```tsx `" > <span className="inline-flex items-center gap-3"> <TrendArrow value="0.01" flatBand="0.02" title="Within noise" style="{ width: 20, height: 20 }" /> </span> ``` `flatBand` is where you declare your noise floor, so a ±0.1% wiggle reads as no change instead of a trend. ```tsx `"> <TrendArrow value="0.12" format="{ style: "percent", maximumFractionDigits: 0 }" title="Weekly change" style="{ height: 22 }" /> ``` ```tsx `"> <TrendArrow value="-0.08" positive="down" title="Latency" style="{ width: 20, height: 20 }" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <TrendArrow value="0" title="No change" style="{ height: 20 }" /> ``` ```tsx `"> <TrendArrow value="Number.NaN" title="Invalid" style="{ height: 20 }" /> ``` ```tsx `"> <TrendArrow value="12483" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Signups" style="{ height: 22 }" /> ``` `format`/`locale` reach `showValue`'s number and the accessible summary's magnitude together — "12,483" becomes "12.483" in German grouping. ## Four homes [#four-homes] **In a sentence** ```tsx <p> P95 latency{" "} <span className="mc-inline"> <TrendArrow value={0.09} positive="down" format={{ style: "percent" }} summary={false} /> </span> {" "} is up 9% since the deploy. </p> ``` **In a table cell** ```tsx <td> <TrendArrow value={-0.22} positive="down" format={{ style: "percent" }} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.6%</span> <span className="unit">of requests</span> <TrendArrow value={-0.22} positive="down" showValue format={{ style: "percent" }} /> </div> ``` **In a tab header** ```tsx <button className="tab"> API <TrendArrow value={-0.08} positive="down" glyph="chevron" /> </button> ``` ## Accessibility [#accessibility] The default accessible name is the 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) Activity piled up somewhere in the 140s, and a time axis cannot tell you which level. VolumeProfile 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 the only accented mark. The value area (the levels holding most of the activity) is shaded behind it. ```tsx `" > <VolumeProfile data="[ { level: 134, weight: 3 }, { level: 136, weight: 6 }, { level: 138, weight: 11 }, { level: 140, weight: 18 }, { level: 142, weight: 26 }, { level: 144, weight: 20 }, { level: 146, weight: 12 }, { level: 148, weight: 7 }, { level: 150, weight: 4 }, ]" title="Volume by price" width="120" height="90" /> ``` ## Install [#install] ```tsx import { VolumeProfile } from "@microcharts/react/volume-profile"; <VolumeProfile data={profile} title="Volume by price" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { VolumeProfile } from "@microcharts/react/volume-profile/interactive"; <VolumeProfile data={profile} /> ``` ## When to use it [#when-to-use-it] Use it for volume-at-price, level-of-activity distributions, and load by tier. For a time series, or whenever the timing is what matters, use Sparkline. ## Sizing [#sizing] **level-activity cell** ```tsx <VolumeProfile data={profile} label="none" width={32} height={32} /> ``` **right side (pair with trend)** ```tsx <VolumeProfile data={profile} align="right" /> ``` ## Variants [#variants] ```tsx `" > <VolumeProfile data="[ { level: 138, weight: 11 }, { level: 140, weight: 18 }, { level: 142, weight: 26 }, { level: 144, weight: 20 }, { level: 146, weight: 12 }, ]" align="right" title="Right side" width="120" height="70" /> ``` `valueArea` defaults to `0.7`, and the summary states the fraction it used. It is a stated convention, not an implied confidence interval. ## Edge cases [#edge-cases] ```tsx `" > <VolumeProfile data="[142, 142, 142, 142]" title="Single level" width="80" height="40" /> ``` ```tsx `" > <VolumeProfile data="[]" title="Empty" width="80" height="40" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Trades at{" "} <span className="mc-inline"> <VolumeProfile data={profile} label="none" width={28} height={28} summary={false} /> </span>{" "} — POC 142. </p> ``` **In a table cell** ```tsx <td><VolumeProfile data={profile} label="none" width={32} height={32} /></td> ``` **In a KPI card** ```tsx <div className="kpi"><span className="figure">142</span><VolumeProfile data={profile} width={80} height={56} /></div> ``` **In a tab header** ```tsx <button className="tab">AAPL <VolumeProfile data={profile} label="none" width={28} height={28} /></button> ``` Best at KPI/card scale — profile needs a near-square seat. ## 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 activity mass alongside its share 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) 60 opening, five signed steps, 87 at the close. Waterfall is the P\&L bridge in a table cell: one bar per delta, each starting where the last one left off. Sign is encoded by vertical direction from the running level AND by valence color, never color alone. ```tsx <Waterfall data="[ { label: "Product", value: 42 }, { label: "Services", value: 18 }, { label: "Refunds", value: -12 }, { label: "Opex", value: -26 }, { label: "FX", value: 5 }, ]" open="60" title="Net income bridge" width="220" height="30" /> ``` ## Install [#install] ```tsx import { Waterfall } from "@microcharts/react/waterfall"; <Waterfall data={steps} open={60} title="Net income bridge" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Waterfall } from "@microcharts/react/waterfall/interactive"; <Waterfall data={steps} open={60} /> ``` ## When to use it [#when-to-use-it] Use it for P\&L bridges in table cells and net-change decomposition in KPI cards. For unordered category comparison use MiniBar, and keep any one bridge under about 8 steps. ## Sizing [#sizing] **no step labels** ```tsx <Waterfall data={steps} open={60} label="none" /> ``` **P&L rows** ```tsx {quarters.map((q) => ( <Waterfall key={q.id} data={q.steps} open={q.open} title={q.name} /> ))} ``` **cost bridge (down is good)** ```tsx <Waterfall data={steps.map((d) => ({ label: d.label, value: -d.value }))} positive="down" /> ``` ## Variants [#variants] ```tsx <Waterfall data={steps} open={60} totalBar={false} />`" > <span className="inline-flex flex-col gap-3"> <Waterfall data="[ { label: "Product", value: 42 }, { label: "Services", value: 18 }, { label: "Refunds", value: -12 }, { label: "Opex", value: -26 }, { label: "FX", value: 5 }, ]" open="60" title="Net income bridge" width="240" height="30" /> <Waterfall data="[ { label: "Product", value: 42 }, { label: "Services", value: 18 }, { label: "Refunds", value: -12 }, { label: "Opex", value: -26 }, { label: "FX", value: 5 }, ]" open="60" totalBar="false" title="Steps only" width="220" height="30" /> </span> ``` The zero-anchored total bar stays on by default, because floating bars alone can't be checked against a real total. Hairline connectors carry the running level between steps without adding ink. ```tsx `" > <Waterfall data="[ { label: "Cloud", value: -8 }, { label: "Headcount", value: 14 }, { label: "Vendors", value: -5 }, { label: "Travel", value: -3 }, ]" open="40" positive="down" title="Cost bridge" width="220" height="30" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <Waterfall data="[ { label: "Q1", value: -20 }, { label: "Q2", value: -35 }, { label: "Q3", value: -10 }, ]" open="0" title="All losses" width="180" height="30" /> ``` ```tsx `" > <Waterfall data="[ { label: "Product", value: 30 }, { label: "Flat", value: 0 }, { label: "Refunds", value: -12 }, ]" open="50" title="Zero step" width="180" height="30" /> ``` ## Four homes [#four-homes] **In a sentence** ```tsx <p> Net income bridged from $60k to $87k this quarter{" "} <span className="mc-inline"> <Waterfall data={steps} open={60} width={100} height={16} summary={false} /> </span>{" "} — Product and Services carried it past Refunds and Opex. </p> ``` **In a table cell** ```tsx <td> <Waterfall data={unit.steps} open={unit.start} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">$87k</span> <span className="unit">from $60k</span> <Waterfall data={steps} open={60} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Revenue <Waterfall data={steps} open={60} /> </button> ``` ## 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 buckets a high-frequency signal and draws each bucket's maximum, never its mean, so one spike survives the squeeze down to sparkline width. Averaging would erase the spike someone is looking for. The auto domain is symmetric at ±max|data|, and 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" />`" > <Waveform data="Array.from({ length: 200 }, (_, i) => (i === 126 ? 0.82 : Math.sin(i / 3) * 0.15 + Math.sin(i / 11) * 0.35) * (1 - Math.abs(i - 100) / 260))" progress="0.63" title="Voice memo" width="300" height="30" /> ``` ## Install [#install] ```tsx import { Waveform } from "@microcharts/react/waveform"; <Waveform data={samples} title="Voice memo" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { Waveform } from "@microcharts/react/waveform/interactive"; <Waveform data={samples} progress={0.63} /> ``` ## When to use it [#when-to-use-it] Use it for voice-memo and audio scrubbers, and for high-frequency log or request volume. For exact values use Sparkline; for categorical state use Hypnogram. ## Sizing [#sizing] **table cell** ```tsx <Waveform data={row.samples} width={60} height={14} /> ``` **envelope** ```tsx <Waveform data={samples} mode="envelope" /> ``` ## Variants [#variants] ```tsx Math.sin(i / 3) * 0.4 * (1 - Math.abs(i - 100) / 220))} mode="envelope" />`" > <Waveform data="Array.from({ length: 200 }, (_, i) => Math.sin(i / 3) * 0.4 * (1 - Math.abs(i - 100) / 220))" mode="envelope" title="Envelope" width="280" height="30" /> ``` ```tsx `"> <Waveform data="[0.2, 0.5, 0.82, 0.4, 0.1]" locale="de-DE" title="Localized peak" width="90" height="22" /> ``` With a `locale`, the announced peak follows that locale's own decimal mark: "0,82" in German, not "0.82". Comparing loudness across rows needs an explicit shared `domain`. Without one, each chart fits its own max, and quiet data is rescaled to look loud. ## Edge cases [#edge-cases] ```tsx `"> <Waveform data="[0.6]" title="One sample" width="60" height="20" /> ``` ```tsx `"> <Waveform data="[0, 0, 0, 0, 0, 0, 0, 0]" title="Silent" width="100" height="20" /> ``` 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` behaves the same as a true zero for any bucket where it is 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. A genuine gap and real silence therefore render identically here, unlike Sparkline's line break. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Voice memo amplitude{" "} <span className="mc-inline"> <Waveform data={samples} progress={0.63} width={90} height={16} summary={false} /> </span>{" "} — peak 0.82 at 63%. </p> ``` **In a table cell** ```tsx <td> <Waveform data={row.samples} width={72} height={16} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">0.82</span> <span className="unit">peak · 63% through</span> <Waveform data={samples} progress={0.63} width={200} height={30} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Standup memo <Waveform data={samples} width={54} height={14} /> </button> ``` ## 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) A model has the home side at 98% after three lead changes. WinProbWorm plots that series on a **fixed 0–100 axis that is never truncated**, so a 2-point edge always looks like a 2-point edge. It 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. A win-probability curve is a modelled read, so the summary always says so, "per the supplied model". ```tsx <WinProbWorm data="[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]" sides="["home", "away"]" title="Win probability" width="220" height="28" /> ``` ## Install [#install] ```tsx import { WinProbWorm } from "@microcharts/react/win-prob-worm"; <WinProbWorm data={[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]} sides={["home", "away"]} title="Win probability" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { WinProbWorm } from "@microcharts/react/win-prob-worm/interactive"; <WinProbWorm data={game} sides={["home", "away"]} /> ``` ## When to use it [#when-to-use-it] Use it for live win or election probability where the lead flips, for a modelled forecast whose crossings are the point, and for any 0–100 probability you must not truncate. For a raw score or margin use Sparkline, and for a value that isn't a bounded 0–100 probability use another chart. ## Sizing [#sizing] **a wire-to-wire lead never crosses 50** ```tsx <WinProbWorm data={decided} sides={["home", "away"]} /> ``` **a nailbiter hovers around the line** ```tsx <WinProbWorm data={nailbiter} sides={["home", "away"]} /> ``` ## Variants [#variants] ```tsx <WinProbWorm data={[50, 53, 49, 52, 48, 51, 47, 50, 46, 49, 45, 48, 52]} sides={["home", "away"]} />`" > <span className="inline-flex gap-4 items-center"> <WinProbWorm data="[50, 58, 66, 74, 80, 86, 90, 93, 96, 98, 99]" sides="["home", "away"]" width="160" height="24" /> <WinProbWorm data="[50, 53, 49, 52, 48, 51, 47, 50, 46, 49, 45, 48, 52]" sides="["home", "away"]" width="160" height="24" /> </span> ``` The endpoint carries the current number. At a taller size the biggest single momentum swing also gets a hair connector and a signed delta; drop that with `markSwing={false}`, and drop the endpoint probability with `label="none"`. ```tsx `" > <WinProbWorm data="[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]" sides="["home", "away"]" markSwing="false" label="none" width="220" height="28" /> ``` ## Edge cases [#edge-cases] ```tsx `" > <WinProbWorm data="[50, 55.5, 61.2, 58.4, 63.5]" sides="["Ja", "Nein"]" format="{ minimumFractionDigits: 1 }" locale="de-DE" title="Modelled probability" width="180" height="26" /> ``` ```tsx `" > <WinProbWorm data="[50, 50, 50, 50, 50]" sides="["home", "away"]" title="Tied" width="160" height="24" /> ``` ```tsx `"> <WinProbWorm data="[72]" sides="["home", "away"]" title="One reading" width="110" height="24" /> ``` 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, since 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, and `data={[]}` renders the frame with the "no data" summary. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Win probability{" "} <span className="mc-inline"> <WinProbWorm data={winProb} sides={["home", "away"]} summary={false} /> </span>{" "} — home team leads 62%, swing at Q3. </p> ``` **In a table cell** ```tsx <td> <WinProbWorm data={winProb} sides={["home", "away"]} label="none" /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">98%</span> <span className="unit">win probability</span> <WinProbWorm data={winProb} sides={["home", "away"]} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Finals G1 <WinProbWorm data={winProb} sides={["home", "away"]} label="none" /> </button> ``` ## 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."** A constant series reads "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) Southwest at 32, in one character. WindBarb points the shaft along the bearing and counts the magnitude in barbs: a half barb, a full barb, and a pennant are fixed quanta, calibrated to the chart's `step`, so counting them gives the magnitude without a scale to measure against. The per-barb quantum is stated next to every example below. ```tsx <WindBarb direction="225" magnitude="32" step="10" label="value" title="Wind" size="64" /> ``` ## Install [#install] ```tsx import { WindBarb } from "@microcharts/react/wind-barb"; <WindBarb direction={225} magnitude={32} step={10} title="Wind" /> ``` Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent. ## Try it [#try-it] ```tsx import { WindBarb } from "@microcharts/react/wind-barb"; <WindBarb direction={225} magnitude={32} /> ``` ## 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 `"> <span style="{ display: "inline-flex", gap: "0.5rem", flexWrap: "wrap", justifyContent: "center" }"> <WindBarb direction="0" magnitude="35" title="0°" size="40" summary="false" /> <WindBarb direction="45" magnitude="35" title="45°" size="40" summary="false" /> <WindBarb direction="90" magnitude="35" title="90°" size="40" summary="false" /> <WindBarb direction="135" magnitude="35" title="135°" size="40" summary="false" /> <WindBarb direction="180" magnitude="35" title="180°" size="40" summary="false" /> <WindBarb direction="225" magnitude="35" title="225°" size="40" summary="false" /> <WindBarb direction="270" magnitude="35" title="270°" size="40" summary="false" /> <WindBarb direction="315" magnitude="35" title="315°" size="40" summary="false" /> </span> ``` ```tsx `"> <span style="{ display: "inline-flex", gap: "0.75rem", flexWrap: "wrap", justifyContent: "center", alignItems: "center", }" > <WindBarb direction="90" magnitude="5" label="value" title="5" size="44" summary="false" /> <WindBarb direction="90" magnitude="15" label="value" title="15" size="44" summary="false" /> <WindBarb direction="90" magnitude="32" label="value" title="32" size="44" summary="false" /> <WindBarb direction="90" magnitude="55" label="value" title="55" size="44" summary="false" /> <WindBarb direction="90" magnitude="80" label="value" title="80" size="44" summary="false" /> </span> ``` ## When to use it [#when-to-use-it] Use it for wind or current direction plus strength, traffic flow, net migration, and request routing. For an exact magnitude add `label`; for a time series use Sparkline. ## Sizing [#sizing] **table cell** ```tsx <WindBarb direction={row.dir} magnitude={row.speed} size={18} /> ``` **with label** ```tsx <WindBarb direction={45} magnitude={25} label="value" /> ``` ## Variants [#variants] ```tsx `"> <WindBarb direction="0" magnitude="1" title="Calm" size="44" /> ``` ```tsx `"> <WindBarb direction="45" magnitude="25" mode="arrow" title="Arrow" size="44" /> ``` ```tsx `"> <WindBarb direction="225" magnitude="32" step="10" label="value" title="Wind" size="44" /> ``` ## Edge cases [#edge-cases] ```tsx `"> <WindBarb direction="0" magnitude="0" title="Calm" size="40" /> ``` ```tsx `"> <WindBarb direction="90" magnitude="-25" title="Flipped" size="40" /> ``` ```tsx `"> <WindBarb direction="225" magnitude="5000" step="10" label="value" title="Extreme" size="40" /> ``` ```tsx `"> <WindBarb direction="225" magnitude="32" step="10" label="value" format="{ maximumFractionDigits: 0 }" locale="de-DE" title="Wind" size="44" /> ``` `format`/`locale` reach the `label="value"` numeral and the accessible summary's magnitude together: the component builds one formatter and reuses it for both. 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. ## Four homes [#four-homes] **In a sentence** ```tsx <p> Wind at KSFO{" "} <span className="mc-inline"> <WindBarb direction={225} magnitude={32} summary={false} /> </span>{" "} — SW 32 kt, three full barbs. </p> ``` **In a table cell** ```tsx <td> <WindBarb direction={225} magnitude={32} /> </td> ``` **In a KPI card** ```tsx <div className="kpi"> <span className="figure">225°</span> <span className="unit">· 32 kt</span> <WindBarb direction={225} magnitude={32} /> </div> ``` **In a tab header** ```tsx <button className="tab"> Coastal <WindBarb direction={225} magnitude={32} /> </button> ``` ## Accessibility [#accessibility] The accessible name is the full reading: **"Southwest (225°), magnitude 32."** Calm renders as **"Calm."** The glyph is self-describing, so rows of barbs get their interaction from the host table or list rather than 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. | | `size` | `number` | Barb box edge in viewBox units (default 32). | 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).