X Data

Star on GitHubFeedback
X DataNative HTML and a hookot-chart

Chart

Hairlines and dots against one very large number that rolls to the bar you point at.

Report an issue with Chart

Example

4,210visits in September

The studio’s site grew all year, 4,210 visits in September, its best month yet.

4,210visits in September
One dot, 500 visits
September24new clients15returning
September24new clients15returning39enquiries in all
1,180readers in London
Rest
4,210visits in September
Pointed at
4,210visits in September
spark, rest

Visits reached 4,210 visits in September

spark, pointed at

Visits reached 4,210 visits in September

isotype, rest
4,210visits in September
One dot, 500 visits
grouped, pointed at
September24new clients15returning
stacked, pointed at
September24new clients15returning39enquiries in all
horizontal, pointed at
410readers in Mexico City
isotype, pointed at
4,210visits in September
One dot, 500 visits

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.

npx shadcn@latest add https://thedirectors.agency/ui/r/chart.json

Usage

chart-example.tsx
import { Chart } from "@/components/ui/chart"const visits = [{ label: "January", value: 1820 },{ label: "February", value: 2140 },{ label: "March", value: 2600 },{ label: "April", value: 2380 },{ label: "May", value: 3050 },{ label: "June", value: 2890 },{ label: "July", value: 3400 },{ label: "August", value: 3120 },{ label: "September", value: 4210 },]// Enquiries by month, new against returning clients.const enquiries = [{ label: "April", new: 14, returning: 9 },{ label: "May", new: 18, returning: 11 },{ label: "June", new: 12, returning: 13 },{ label: "July", new: 21, returning: 10 },{ label: "August", new: 9, returning: 7 },{ label: "September", new: 24, returning: 15 },]const clients = [{ key: "new", label: "new clients" },{ key: "returning", label: "returning" },]const readers = [{ label: "Lisbon", value: 640 },{ label: "London", value: 1180 },{ label: "Mexico City", value: 410 },{ label: "Osaka", value: 860 },{ label: "Tallinn", value: 230 },]export default function Example() {return (<div className="grid gap-y-16"><Chart data={visits} label="Visits by month, 2026" unit="visits" className="max-w-[44rem]" /><p className="max-w-[34rem]" style={{ fontSize: "var(--ot-mp)", lineHeight: "var(--ot-mp-lh)", color: "var(--ot-graphite)" }}>The studio&rsquo;s site grew all year, <Chart variant="spark" data={visits} label="Visits by month, 2026" unit="visits" />, its best month yet.</p><Chart variant="isotype" data={visits} label="Visits by month, 2026" unit="visits" className="max-w-[44rem]" /><Chart data={enquiries} series={clients} label="Enquiries by month, new and returning clients" className="max-w-[44rem]" /><Chart data={enquiries} series={clients} stacked label="Enquiries by month, stacked" unit="enquiries" className="max-w-[44rem]" /><Chart orientation="horizontal" data={readers} now={1} label="Readers by city, September" unit="readers" className="max-w-[44rem]" /></div>)}

Contract

  • Underneath: our own markup, plus a hook.
  • Anatomy: <figure class="ot-chart" data-variant="stems | spark | isotype" style="--n: 9; --m: 1"> holding .ot-chart-read (.ot-chart-value, a large rolling number, and its label), .ot-chart-plot of <button class="ot-chart-bar" style="--v: 0–1" aria-label>, then .ot-chart-axis. data-now marks the current bar. Spark renders spans instead (phrasing content, so it can sit in a <p>), the plot first and the reading after it; isotype's bars each hold <i> dots (data-on, data-top) and it ends with .ot-chart-key.
  • stems (the default): hairlines and dots against one very large number, as the posters set tiny data beside giant type. The number is as large as the chart allows (16% of its width, between --ot-ff and --ot-fff), so the scale contrast is SPECTRA's. Pointing at or focusing a bar inks its line, swells its dot and rolls the number to it; leaving rolls it back to now. The current month is the accent.
  • spark: Tufte's sparkline, a chart the size of a word, set inside the sentence that talks about it. The stems stand on the baseline as tall as a capital, spanning the data's own range so the shape shows at that size; the accent dot sits on now, as Tufte marks the last point in red, and the number follows in the text's own size at weight 500, with "visits in September" after it as part of the line. Pointing rolls the number, as in stems. The stems are small targets, which the inline exception allows; each is still a labelled button for the keyboard.
  • isotype: Otto Neurath's rule, count, don't scale. Each bar is a column of dots and one dot is a fixed amount (each, by default a tenth of the ceiling): what was had in ink, the room left to the ceiling in hairline rings, so the grid of discs is the picture, as in "28 December". Now's top dot is the one accent. A key under the axis says what a dot counts ("One dot, 500 visits"). Pointing at a column sets every other column back to rule-strong and counts its dots up from the bottom, one after another, as the number rolls.
  • Series (series, stems): two or more read at once, each its own voice. The stems are solid, dotted and dashed, topped by a ring, a disc and a ring; the readout names them roman, italic and pencil, as "It has to be design." pairs the faces, each name after a short swatch of its line. Grouped (the default), a column's stems stand side by side a --ot-space-2 apart, now's first dot the accent; stacked, one stem per column is cut into a length per series with a dot at every joint, and the top joint is the accent. Each is a .ot-chart-mark (data-series, --b and --v from 0 to 1) inside the column's button. Written for up to three.
  • The readout for several series: the one giant number becomes a short list (.ot-chart-read[data-list]: .ot-chart-at, then .ot-chart-list of .ot-chart-rows, each a .ot-chart-value and a .ot-chart-name). The figures stand flush right in one column at --ot-mf to --ot-f, weight 200, as on a ledger, and each rolls on its own, only if it changed. Stacked, a hairline is ruled under the figures and the sum stands beneath it ("39 enquiries in all"), a sum set as on paper.
  • Horizontal (orientation, stems; data-orientation="horizontal"): a ledger. The whole names stand in a column at the start, pencil, and each line runs out from an ink baseline on the start edge to its dot, one row (--ot-space-6) each, rules at the quarters; the name the number reads is inked. Series stack or group along the row the same way.
  • Keyboard: Tab enters the plot at the bar the number is on (one tab stop, a roving tabindex); Left and Right, or Up and Down, move to the next bar, Home and End jump to the ends. Right to left, Left and Right follow the page. Focus rolls the number as pointing does.

Props

Props of Chart
PropTypeDefaultWhat it does
data{ label: string; value?: number; [key: string]: string | number | undefined }[]One bar per entry. With one series the number is value; with several, each series reads its own key. The first letter of each label sets the axis (the whole label, horizontal).
series{ key: string; label: string }[]stems: two or more series. Each has its own stem, solid, dotted or dashed, with a ring, a disc or a ring, and the one giant number becomes a short list of each at the bar you're on, figures flush right as on a ledger, named roman, italic and pencil. Spark and isotype read the first series only. Written for up to three.
stackedbooleanfalsestems with series: one stem per column cut into a length per series, a dot at every joint; the top joint is now's accent, and the readout sets a rule and the sum under the list. Otherwise the series stand side by side (grouped).
orientation"vertical" | "horizontal""vertical"stems: horizontal sets a ledger, the whole names in a column at the start and each line running out from a baseline on the start edge to its dot; the name the number reads is inked. Left and Right follow the page's direction.
labelstringNames the chart for assistive tech, such as Visits by month, 2026. Each bar is also a button labelled with its own value.
unitstringWhat the values count. The readout reads visits in September.
nownumberlastIndex of the bar the number rests on. It carries the accent.
maxnumberWhat a full-height bar means. Defaults to the next round number above the largest value.
format(value: number) => stringFormats the number and the bar labels. Defaults to en-GB grouping.
variant"stems" | "spark" | "isotype""stems"stems: hairlines and dots against one very large number, as large as the chart allows. spark: word-sized, set inside a sentence; stems as tall as a capital, spanning the data's own range, the accent dot on now, and the number following in the text's own size. isotype: each bar a column of dots, one dot a fixed amount, ink for what was had and hairline rings for the room left to the ceiling; now's top dot is the accent, a key says what a dot counts, and pointing at a column counts its dots up.
eachnumbera tenth of the ceilingisotype: what one dot counts. Values round to the nearest dot; the number and each bar's label stay exact.
ChartFrame layer(geometry: ChartGeometry) => ReactNodeOptional drawing beneath the plot's points. Geometry provides normalized [from, to] spans per series and the series list; LineChart and AreaChart use this shared frame.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
ChartPOINT / SPECTRATiny data against one giant number
Spark chartTufte's sparklines; "Healthy habits →" one line of thoughtA chart the size of a word, set in the sentence
Isotype chartNeurath's Isotype; "28 December" dotsCount, don't scale: a column of dots per month, rings for the room left
Series chart"It has to be design." two faces; a ledger's sumEach series its own line and voice; the giant number becomes a ruled list, the sum under the rule
Horizontal chartA ledger, contents-page namesNames in a column at the start, lines run out from the start edge

How it moves

How it moves
ComponentArticulationWhat moves
ChartRollThe number rolls to the month you point at, and back
Spark chartRollThe number in the sentence rolls to the stem you point at, and back
Isotype chartCountThe other columns step back; the pointed column's dots pop up from the bottom in turn with spiccato as the number rolls
Series chartRollEach figure in the list that changed rolls its own way; the stems ink