Mini chart
A sparkline, area or bar chart sized for a row, a stat or a cell.
Supplier calls, 14 days
1,284 today
import { MiniChart } from "@oration/canon/components/mini-chart";export function Hero() { const calls = [ 982, 1044, 1120, 1010, 640, 512, 1190, 1106, 1180, 1232, 1098, 702, 588, 1284, ]; const days = [ "Tue, Sep 15", "Wed, Sep 16", "Thu, Sep 17", "Fri, Sep 18", "Sat, Sep 19", "Sun, Sep 20", "Mon, Sep 21", "Tue, Sep 22", "Wed, Sep 23", "Thu, Sep 24", "Fri, Sep 25", "Sat, Sep 26", "Sun, Sep 27", "Mon, Sep 28", ]; return ( <div className="flex w-[27rem] max-w-full flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border"> <div className="flex items-baseline justify-between gap-4"> <p className="text-sm font-semibold">Supplier calls, 14 days</p> <p className="text-13 font-medium tabular-nums">1,284 today</p> </div> <MiniChart data={calls} labels={days} type="area" width={400} height={64} interactive showLast formatValue={(value) => `${value.toLocaleString("en-US")} calls` } ariaLabel="Supplier calls per day, Sep 15 to Sep 28" /> <div className="flex justify-between text-xs text-muted-foreground tabular-nums"> <span>Sep 15</span> <span>Sep 28</span> </div> </div> );}Usage#
Mini chart is a sparkline in line, area or bar form, sized to sit in a stat, a table cell or a panel: supplier calls per day, containment over two weeks, first-response SLA by queue. It draws in Ink 65 by default and turns green, amber or red only when the series is itself a verdict. It shows shape, not values, so it always travels with the number and label it summarizes. The common mistake is coloring it for decoration, in indigo or in green for a series that isn't good or bad.
When to use
- Beside a figure to show its recent shape: calls today next to 14 days of calls.
- In a table column, one per row, to compare trends across queues, agents or suppliers.
- In a stat, through
Stat'strendprop, which draws a 64 by 24px line for you. - In a panel or insight card with
interactive, so people can read each day's value on hover or with the arrow keys. - As bars when the periods are discrete counts, such as transfers per day.
When not to use
- For a chart people read closely, with axes, a legend or several series. Use Chart
- For one share of a whole, such as containment out of all calls. Use Radial chart
- For a good, fair or poor verdict per day, such as uptime. Use Health bar
- For per-turn latency on a call. Use Latency sparkline
- For a value against a limit or quota. Use Meter
The Ink Fill Rule
The Label-Beside-Color Rule
The Tabular Figures Rule
Anatomy#
- Line. A 1.5px monotone curve with round caps, in Ink 65 or the tone's solid color. It never overshoots the data.
- Area.
type="area"fills under the line at 8% ink, or 14% of a semantic tone. - Marker. A 4.5px dot on the latest point (
showLast) or any point (highlightIndex), with a 1.5px card-colored ring. - Bars.
type="bar": one bar per point with 2px gaps (1px past 24 bars), from a zero baseline, at Ink 45 or 70% of a tone. - Emphasized bar. The marked bar at 80%; every other bar dims to 25% so it stands out.
Examples#
Types
Line for a trend, area when the volume under it matters, bars for discrete counts per period. Bars start at zero; line and area scale to the series.
import { MiniChart } from "@oration/canon/components/mini-chart";export function Types() { const containment = [ 64, 66, 65, 67, 68, 66, 69, 68, 70, 69, 71, 70, 72, 71, ]; return ( <div className="flex flex-wrap items-end justify-center gap-10"> {(["line", "area", "bar"] as const).map((type) => ( <figure key={type} className="flex flex-col items-center gap-2"> <MiniChart data={containment} type={type} width={140} height={40} ariaLabel={`Containment rate, last 14 days, as a ${type} chart`} /> <figcaption className="text-xs text-muted-foreground capitalize"> {type} </figcaption> </figure> ))} </div> );}Tones
Ink for any series that isn't a verdict. Success, warning and danger only when the series is one, always beside a status label that says it.
- Invoices receivedNo verdict, so ink
- Payment-run successOn track
- Supplier response timeSlowing
- Failed paymentsRising
import { MiniChart } from "@oration/canon/components/mini-chart";import { StatusLabel } from "@oration/canon/components/status-dot";export function Tones() { const series = [ { tone: "ink" as const, label: "Invoices received", status: null, data: [312, 298, 344, 330, 356, 318, 372, 340, 362, 388], }, { tone: "success" as const, label: "Payment-run success", status: { tone: "success" as const, text: "On track" }, data: [98.1, 98.4, 98.2, 98.8, 98.9, 99.0, 98.7, 99.1, 99.0, 99.2], }, { tone: "warning" as const, label: "Supplier response time", status: { tone: "warning" as const, text: "Slowing" }, data: [2.4, 2.6, 2.5, 2.8, 2.9, 3.0, 2.9, 3.1, 3.0, 3.2], }, { tone: "danger" as const, label: "Failed payments", status: { tone: "danger" as const, text: "Rising" }, data: [6, 5, 7, 6, 8, 9, 8, 11, 12, 14], }, ]; return ( <ul className="grid w-full max-w-xl gap-x-8 gap-y-4 sm:grid-cols-2"> {series.map((item) => ( <li key={item.label} className="flex items-center gap-3"> <div className="flex min-w-0 flex-1 flex-col gap-0.5"> <span className="truncate text-13 font-medium"> {item.label} </span> {item.status ? ( <StatusLabel tone={item.status.tone} className="text-xs" > {item.status.text} </StatusLabel> ) : ( <span className="text-xs text-muted-foreground"> No verdict, so ink </span> )} </div> <MiniChart data={item.data} tone={item.tone} showLast ariaLabel={`${item.label}, last 10 days`} /> </li> ))} </ul> );}Sizes
Width and height are pixels. 64 by 24 in a stat, 72 by 20 in a table cell, the 96 by 28 default beside a label, and wider in panels.
import { MiniChart } from "@oration/canon/components/mini-chart";export function Sizes() { const sla = [91, 93, 92, 95, 94, 96, 94, 95, 93, 94]; const sizes = [ { width: 64, height: 24, use: "In a stat" }, { width: 72, height: 20, use: "In a table cell" }, { width: 96, height: 28, use: "Default" }, { width: 180, height: 44, use: "In a panel" }, ]; return ( <div className="flex flex-wrap items-end justify-center gap-10"> {sizes.map((size) => ( <figure key={size.use} className="flex flex-col items-center gap-2 text-center" > <MiniChart data={sla} width={size.width} height={size.height} ariaLabel={`First-response SLA, last 10 days, ${size.use.toLowerCase()}`} /> <figcaption className="flex flex-col text-xs text-muted-foreground"> <span className="font-medium text-foreground"> {size.use} </span> <span className="tabular-nums"> {size.width} by {size.height} </span> </figcaption> </figure> ))} </div> );}Interactive
interactive adds a crosshair and a readout on hover, and arrow-key scrubbing when focused. labels and formatValue write the readout.
Transfer rate by day
Monday, Sep 21 spiked to 46% after the payment run posted late. Hover or focus the chart and use the arrow keys to read each day.
import { MiniChart } from "@oration/canon/components/mini-chart";export function Interactive() { const transferred = [ 22, 18, 20, 24, 19, 21, 46, 23, 20, 18, 22, 19, 21, 20, ]; const days = [ "Sep 15", "Sep 16", "Sep 17", "Sep 18", "Sep 19", "Sep 20", "Sep 21", "Sep 22", "Sep 23", "Sep 24", "Sep 25", "Sep 26", "Sep 27", "Sep 28", ]; return ( <div className="flex w-80 max-w-full flex-col gap-2 text-left"> <p className="text-13 font-medium">Transfer rate by day</p> <MiniChart data={transferred} labels={days} type="bar" width={320} height={48} highlightIndex={6} interactive formatValue={(value) => `${value}% transferred`} ariaLabel="Transfer rate by day, Sep 15 to Sep 28" /> <p className="text-xs text-muted-foreground"> Monday, Sep 21 spiked to 46% after the payment run posted late. Hover or focus the chart and use the arrow keys to read each day. </p> </div> );}Marking a point
showLast marks the latest value. highlightIndex marks the one your text is talking about, such as the day of a spike.
import { MiniChart } from "@oration/canon/components/mini-chart";export function Marking() { const aht = [288, 281, 279, 284, 270, 266, 272, 268, 262, 259, 263, 255]; return ( <div className="flex flex-wrap items-end justify-center gap-10"> <figure className="flex flex-col items-center gap-2"> <MiniChart data={aht} width={140} height={40} ariaLabel="Average handle time, last 12 days" /> <figcaption className="text-xs text-muted-foreground"> No marker </figcaption> </figure> <figure className="flex flex-col items-center gap-2"> <MiniChart data={aht} width={140} height={40} showLast ariaLabel="Average handle time, last 12 days, latest marked" /> <figcaption className="text-xs text-muted-foreground"> Latest point marked </figcaption> </figure> <figure className="flex flex-col items-center gap-2"> <MiniChart data={aht} width={140} height={40} highlightIndex={3} ariaLabel="Average handle time, last 12 days, Sep 20 marked" /> <figcaption className="text-xs text-muted-foreground"> One day marked </figcaption> </figure> </div> );}In a table
One chart per row compares queues at a glance. The danger tone appears only on the breaching queue, and every row says its verdict in words.
| Queue | Waiting | First-response SLA | Last 7 days |
|---|---|---|---|
| Payment status | 4 | 96% | Within target |
| Remittance questions | 9 | 88% | Near target |
| Vendor onboarding | 17 | 71% | Breaching |
import { MiniChart } from "@oration/canon/components/mini-chart";import { StatusLabel } from "@oration/canon/components/status-dot";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@oration/canon/components/table";export function InATable() { const queues = [ { name: "Payment status", waiting: 4, sla: "96%", trend: [92, 94, 93, 95, 96, 95, 96], tone: "ink" as const, status: { tone: "success" as const, text: "Within target" }, }, { name: "Remittance questions", waiting: 9, sla: "88%", trend: [93, 92, 90, 91, 89, 88, 88], tone: "ink" as const, status: { tone: "warning" as const, text: "Near target" }, }, { name: "Vendor onboarding", waiting: 17, sla: "71%", trend: [90, 86, 84, 80, 77, 74, 71], tone: "danger" as const, status: { tone: "danger" as const, text: "Breaching" }, }, ]; return ( <div className="w-full max-w-2xl rounded-xl bg-card shadow-border"> <Table className="text-13"> <TableHeader> <TableRow className="hover:bg-transparent"> <TableHead className="h-8 pl-4 text-xs text-muted-foreground"> Queue </TableHead> <TableHead className="h-8 text-right text-xs text-muted-foreground"> Waiting </TableHead> <TableHead className="h-8 text-right text-xs text-muted-foreground"> First-response SLA </TableHead> <TableHead className="h-8 pr-4 text-xs text-muted-foreground"> Last 7 days </TableHead> </TableRow> </TableHeader> <TableBody> {queues.map((queue) => ( <TableRow key={queue.name}> <TableCell className="pl-4 font-medium"> {queue.name} </TableCell> <TableCell className="text-right tabular-nums"> {queue.waiting} </TableCell> <TableCell className="text-right tabular-nums"> {queue.sla} </TableCell> <TableCell className="pr-4"> <span className="flex items-center gap-3"> <MiniChart data={queue.trend} width={72} height={20} tone={queue.tone} ariaLabel={`${queue.name} first-response SLA, last 7 days`} /> <StatusLabel tone={queue.status.tone} className="text-xs" > {queue.status.text} </StatusLabel> </span> </TableCell> </TableRow> ))} </TableBody> </Table> </div> );}States#
import { MiniChart } from "@oration/canon/components/mini-chart";export function EdgeCases() { const cases = [ { data: [] as number[], caption: "No data" }, { data: [42], caption: "One point" }, { data: [99.98, 99.98, 99.98, 99.98, 99.98], caption: "No change" }, { data: [0, 0, 3, 0, 1, 0, 0, 2], caption: "Sparse bars", bar: true }, ]; return ( <div className="flex flex-wrap items-end justify-center gap-10"> {cases.map((item) => ( <figure key={item.caption} className="flex flex-col items-center gap-2" > <MiniChart data={item.data} type={item.bar ? "bar" : "line"} width={120} height={32} showLast ariaLabel={`Failed payments, ${item.caption.toLowerCase()}`} /> <figcaption className="text-xs text-muted-foreground"> {item.caption} </figcaption> </figure> ))} </div> );}| State | Treatment |
|---|---|
| Rest | The series in its tone, no marker. |
| Marked | showLast or highlightIndex adds the dot on a line or area, or emphasizes one bar. |
| Hover | With interactive, a 1px Firm Hairline crosshair follows the pointer, the point under it is marked, and a Graphite Ink readout shows its label and value above. |
| Focus visible | A 2px Focus Indigo outline 2px outside the chart. Focus marks the latest point and shows its readout. |
| Keyboard scrub | Arrow keys move the marked point; the readout and a polite live region update with each step. |
| No data | An empty data array draws a dashed Firm Hairline across the middle. |
| One point or no change | One point draws only its marker. A flat series draws a level line through the middle instead of pinning to an edge. |
Behavior#
- Width and height are fixed pixel sizes (96 by 28 by default). The chart doesn't stretch to its container; measure the container and pass
widthwhen it has to fill. - Line and area scale to the series' own minimum and maximum, so small changes read as shape. Bars scale from zero, so their heights compare honestly.
interactivemakes the chart focusable (tabIndex=0) and adds pointer scrubbing, a crosshair and a readout.labelsnames each point in the readout;formatValueformats the number.- Arrow Right and Arrow Up step forward, Arrow Left and Arrow Down step back, Home and End jump to the first and last point.
decorativehides the chart from assistive tech and turns off focus.Statand the health bar tooltip use it, because their text already says what the line shows.tone="current"draws incurrentColor, for charts inside a tooltip or other inverted surface.monotonePath(points)is exported for drawing the same curve in your own SVG.
Do and don't#
tone="current" and a text-primary wrapper. Indigo is for action and selection, never for data.Content#
ariaLabelnames the series and the window: Supplier calls per day, Sep 15 to Sep 28. Not Chart or Sparkline.labelsare short dates or periods: Sep 21, Mon, Sep 21, Week of Sep 14.formatValueadds the unit, so the readout stands alone: 46% transferred, 1,284 calls, 820 ms.- Put the current figure and the window in text next to the chart: 1,284 today, Last 7 days.
Accessibility#
- By default the chart is
role="img"with yourariaLabel. Passdecorativewhen a nearby label and value already say what it shows, so it isn't announced twice. - An interactive chart is a single tab stop. Its readout is mirrored in a
role="status"region, so each arrow-key step is announced. - The readout is drawn with
aria-hidden; the live region carries the same text. - The chart only shows shape. Give the actual figure in text beside it; a screen reader can't read a curve.
- Semantic tones always sit beside a status label, so the verdict reads without color.
| Keys | Action |
|---|---|
| Tab | Focuses an interactive chart and marks its latest point. |
| → | Marks the next point. Arrow Up does the same. |
| ← | Marks the previous point. Arrow Down does the same. |
| Home | Marks the first point. |
| End | Marks the last point. |
Design tokens#
| Token | Used for |
|---|---|
--foreground | Ink tone: line at 65%, area at 8%, bars at 45% |
--success | Success tone |
--warning | Warning tone |
--destructive | Danger tone |
--border-strong | Crosshair and the no-data dash |
--card | Ring around the marker |
--ring | Focus outline |
API reference#
MiniChart
The sparkline. data-slot="mini-chart".
| Prop | Type | Default | Description |
|---|---|---|---|
dataRequired | number[] | No default | The series, oldest first. |
ariaLabelRequired | string | No default | Names the series and window. Ignored when decorative. |
type | "line" | "area" | "bar" | "line" | Line for trends, area for volume, bar for discrete counts. |
width | number | 96 | Width in pixels. |
height | number | 28 | Height in pixels. |
tone | "ink" | "success" | "warning" | "danger" | "current" | "ink" | Ink unless the series is a verdict. current follows the text color. |
showLast | boolean | false | Marks the latest point or bar. |
highlightIndex | number | No default | Marks one point or bar by index. |
interactive | boolean | false | Adds hover and keyboard scrubbing with a crosshair and readout. |
labels | string[] | No default | A label per point for the readout, such as dates. |
formatValue | (value: number) => string | toLocaleString("en-US") | Formats values in the readout. |
decorative | boolean | false | Hides it from assistive tech when nearby text carries the data. |
className | string | No default | Merged onto the outer span. |
monotonePath
(points: [number, number][]) => string. The monotone cubic path the line uses, for custom SVG. Also exported: toneStroke(tone), TONE_BASE, and the ChartTone and MiniChartTone types.
No props of its own.
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The chart has a fixed pixel width. The app wraps it to fill a container twice over: FluidMiniChart in the agent overview and a useElementWidth hook in the Copilot insight chart.
The interactive chart is role="img" with arrow-key scrubbing, so screen readers don't announce it as something that responds to keys; only the live region reveals it.
The readout is a custom span above the chart rather than the Tooltip component, so a parent with overflow: hidden, such as a table's scroll wrapper, clips it.