Skip to content

Mini chart

A sparkline, area or bar chart sized for a row, a stat or a cell.

Status
Stable
Category
Data display
Adoption
Not used yet
import { MiniChart } from "@oration/canon/components/mini-chart";
packages/canon/src/components/mini-chart.tsx

Supplier calls, 14 days

1,284 today

Sep 15Sep 28
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's trend prop, 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

Series are drawn in ink steps: a 1.5px line in Ink 65, an area at 8%, bars at Ink 45. A tone turns semantic only when the series is a verdict, and it is never indigo.

The Label-Beside-Color Rule

A green, amber or red sparkline sits beside a status label that says the verdict in words: On track, Slowing, Rising.

The Tabular Figures Rule

The interactive readout and the figures beside a chart are set in tabular figures.

Anatomy#

  1. Line. A 1.5px monotone curve with round caps, in Ink 65 or the tone's solid color. It never overshoots the data.
  2. Area. type="area" fills under the line at 8% ink, or 14% of a semantic tone.
  3. Marker. A 4.5px dot on the latest point (showLast) or any point (highlightIndex), with a 1.5px card-colored ring.
  4. 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.
  5. 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.

line
area
bar
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.

In a stat64 by 24
In a table cell72 by 20
Default96 by 28
In a panel180 by 44
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.

No marker
Latest point marked
One day marked
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.

QueueWaitingFirst-response SLALast 7 days
Payment status496%Within target
Remittance questions988%Near target
Vendor onboarding1771%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#

No data
One point
No change
Sparse bars
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>    );}
States
StateTreatment
RestThe series in its tone, no marker.
MarkedshowLast or highlightIndex adds the dot on a line or area, or emphasizes one bar.
HoverWith 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 visibleA 2px Focus Indigo outline 2px outside the chart. Focus marks the latest point and shows its readout.
Keyboard scrubArrow keys move the marked point; the readout and a polite live region update with each step.
No dataAn empty data array draws a dashed Firm Hairline across the middle.
One point or no changeOne 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 width when 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.
  • interactive makes the chart focusable (tabIndex=0) and adds pointer scrubbing, a crosshair and a readout. labels names each point in the readout; formatValue formats 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.
  • decorative hides the chart from assistive tech and turns off focus. Stat and the health bar tooltip use it, because their text already says what the line shows.
  • tone="current" draws in currentColor, 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#

Do. Draw series in ink. The shape is the message.
Don't. Color a sparkline indigo with tone="current" and a text-primary wrapper. Indigo is for action and selection, never for data.
Failed paymentsRising
Do. Use a semantic tone when the series is a verdict, and say the verdict beside it.
Calls
Don't. Paint call volume green. More calls isn't good or bad news, and the color claims it is.

Content#

  • ariaLabel names the series and the window: Supplier calls per day, Sep 15 to Sep 28. Not Chart or Sparkline.
  • labels are short dates or periods: Sep 21, Mon, Sep 21, Week of Sep 14.
  • formatValue adds 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 your ariaLabel. Pass decorative when 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.
Keyboard interactions
KeysAction
TabFocuses 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.
HomeMarks the first point.
EndMarks the last point.

Design tokens#

Design tokens
TokenUsed for
--foregroundInk tone: line at 65%, area at 8%, bars at 45%
--successSuccess tone
--warningWarning tone
--destructiveDanger tone
--border-strongCrosshair and the no-data dash
--cardRing around the marker
--ringFocus outline

API reference#

MiniChart

The sparkline. data-slot="mini-chart".

Props of MiniChart
PropTypeDefaultDescription
dataRequirednumber[]No defaultThe series, oldest first.
ariaLabelRequiredstringNo defaultNames the series and window. Ignored when decorative.
type"line" | "area" | "bar""line"Line for trends, area for volume, bar for discrete counts.
widthnumber96Width in pixels.
heightnumber28Height in pixels.
tone"ink" | "success" | "warning" | "danger" | "current""ink"Ink unless the series is a verdict. current follows the text color.
showLastbooleanfalseMarks the latest point or bar.
highlightIndexnumberNo defaultMarks one point or bar by index.
interactivebooleanfalseAdds hover and keyboard scrubbing with a crosshair and readout.
labelsstring[]No defaultA label per point for the readout, such as dates.
formatValue(value: number) => stringtoLocaleString("en-US")Formats values in the readout.
decorativebooleanfalseHides it from assistive tech when nearby text carries the data.
classNamestringNo defaultMerged 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.