Skip to content

Series chart ​

Values per bucket on a shared x axis: a moment, a number or a category. Errors and warnings per hour over a day, with a deploy and a stretch the collector was down:

tsx
import { Chart } from '@adecore/charts';
import { DEPLOY, GAP, errorsPerHour } from '../shared/chart-data.ts';

const buckets = errorsPerHour();

export default function ErrorsPerHourDemo() {
    return (
        <Chart.Root data={buckets} x="at" xScale="time" y={{ integer: true }} label="Events per hour" height={220} className="w-full">
            <Chart.Grid />
            <Chart.Band from={GAP.start} to={GAP.end} label="No data from the collector" />
            <Chart.XAxis />
            <Chart.YAxis />
            <Chart.Area value="errors" label="Errors" tone="error" />
            <Chart.Line value="warnings" label="Warnings" tone="needsYou" />
            <Chart.Marker at={DEPLOY.at} label={`Deploy ${DEPLOY.version}`} tone="accent" />
            <Chart.Tooltip />
            <Chart.Legend />
            <Chart.Table />
        </Chart.Root>
    );
}
tsx
<Chart.Root data={buckets} x="at" xScale="time" y={{ integer: true }} label="Events per hour" height={220}>
    <Chart.Grid />
    <Chart.Band from={gap.start} to={gap.end} label="No data from the collector" />
    <Chart.XAxis />
    <Chart.YAxis />
    <Chart.Area value="errors" label="Errors" tone="error" />
    <Chart.Line value="warnings" label="Warnings" tone="needsYou" />
    <Chart.Marker at={deploy.at} label={`Deploy ${deploy.version}`} tone="accent" />
    <Chart.Tooltip />
    <Chart.Legend />
    <Chart.Table />
</Chart.Root>

Chart.Root ​

Each row of data is one bucket. x names its position, as a key of the row or a function of it. xScale says what the position is:

  • band (the default): a category per row, such as a day or a model, with the buckets side by side.
  • time: a moment, as a Date or in milliseconds. The rows run from the earliest to the latest. Each row is a bucket that starts at its x and lasts one step, the smallest distance between two rows unless the root gets one; its point stands in the middle of it, where a column would. Columns stand on this scale and on band, not on linear.
  • linear: a number, the rows from low to high.

Rows that are samples at a moment, not buckets that last a while, take step={0}: each point stands at its own x and the cursor takes the nearest sample. xOptions ({ min, max }, a ChartXOptions) fixes the ends of a time or linear axis, such as the last five minutes up to the newest sample, so the plot does not stretch while the first samples come in. Rows outside the ends are drawn past them; leave them out of data.

The value axis is linear or log (yScale). It covers every series, stacked ones at their top, starts at zero on a linear scale and rounds its ends out to a tick. y fixes the ends ({ min, max }), and { integer: true } keeps the ticks of a count on whole numbers. { include: [1] } makes the axis cover a value the data may not reach and still round out above it, so a share that stays near zero does not fill the plot; larger data still raises the top. headroom says what the top leaves above the highest value: up to the next tick ('tick', the default, so a value on a tick touches the top), a whole step more when the value lands on a tick ('step'), or nothing ('none'), so the highest column fills the plot.

The width follows the container; height (200 by default) is the height of the plot and the x axis together.

PropTypeDefault
datareadonly Row[]Required. One row per bucket.
xkeyof Row | (row, index) => ChartXInputRequired.
labelstringRequired. The name a screen reader reads, and the caption of the table.
xScale'band' | 'time' | 'linear''band'
stepnumberThe length of a bucket on a time scale, in milliseconds. Set it when rows may be missing at the start, and for columns, which one row cannot draw without it. 0 for samples.
xOptionsChartXOptionsThe ends of a time or linear x axis.
yScale'linear' | 'log''linear'
yChartYOptions{}min, max, include, integer, step and headroom.
heightnumber200Pixels.
growbooleanfalseTakes the height the container gives the chart, never less than height, such as a chart that fills a panel or a dialog. The container needs a height of its own.
marginChartMarginPixels around the plot, beside what the axes take. top is 8, for something drawn over the plot; bottom is 4 under an x axis and 0 without one; left and right are 6 on a time or linear scale without buckets, so the dot at the end of a line keeps its edge, and 0 otherwise. A chart that draws to its edges, such as a small line in a panel, sets them to 0.
surface'base' | 'raised''base'The surface the chart stands on. The halo that keeps a value label, a rule label or a guide value readable over the series, and the ring around a point, are drawn in it, so a chart in a dialog or a popover passes 'raised'.
interactivebooleantrueFalse makes the chart an image with its label, without a tab stop, keys or a live region; see keyboard and screen reader.
cursor'full' | 'line' | 'none''full'What marks the cursor: a lit bucket on a band scale and a line with a dot on every series on a time or linear one, the line alone, or nothing past the dimmed columns and the tooltip. Picked buckets stay lit.
format(value: number) => stringWrites a value in the tooltip, the live region and the table, and on a y axis without a format of its own. A chart without a y axis needs it to read a duration as one.
rowLabelkeyof Row | (row, index) => stringWhat a person reads for a bucket in the tooltip, the live region and the table, in place of what the x axis writes: "Low" under a column labeled "L", or "Fri, Oct 9" under "Oct 9". On a point chart it names the point beside its group.
activeX, onActiveXChangeChartX | nullThe cursor, controlled. Leaving the chart calls onActiveXChange(null); the app decides whether the cursor goes.
group, groupLabel, groupGap, minBucketWidthGroups of columns, see Columns and groups.
selected, onActivate, dimmedColumns a person picks, see Columns and groups.
className, refOn the outer box.

A ChartX is a number (a moment in milliseconds) or the key of a category; a ChartXInput may also be a Date. ChartXScale, ChartYScale, ChartYOptions, ChartAccessor and ChartRootProps are exported as types.

Grid and axes ​

Chart.Grid draws a line on every tick of the value axis in --border-soft, with the ground in --border-strong, so columns and areas stand on a line that reads. axis takes 'y' (the default), 'x' or 'both'. dashed dashes every line but the ground, all of them in --border so the dashes still read as a line. lines="ground" draws the ground alone, for columns that carry their value over them and need no scale.

Chart.YAxis writes the ticks beside the plot, on the left or the right (side). It measures its widest label and gives the plot the rest of the width. Chart.XAxis writes them under it. On a time scale the labels are the clock within a day, the day once the ticks are a day apart, and both in between, from @adecore/ui/format. Labels that would overlap thin out, and a label that would hang past the end of the chart is left out, except on a tick at an end of the axis, such as the last decade of a log scale, which moves inward. A label that would come nearer to the one before it than its width gives way, an end label excepted, and an axis without rows writes nothing. On a time scale of columns, align="bucket" writes the start of each bucket under its middle instead of round moments on their ticks, so the label of a day stands under its own column.

format writes a value or an x. The format of an axis is also how the tooltip, the live region and the table write it. A Chart.YAxis on each side keeps its own format, ticks and title over the one value scale. The tooltip writes a value in the format of the left one and the scale takes its ticks, or those of the right one where the left has none. ticks asks for a number of ticks. label on Chart.XAxis is what the tooltip, the live region and the table call the x of a point where the title is too long to read out each time, and logLabel replaces "logarithmic" after the title of a log axis, or drops it as an empty string. title names what an axis measures: on Chart.YAxis it stands above the plot where the axis starts, such as a unit ("Index points, higher is better"), and the plot moves down to make room; on Chart.XAxis it stands centered under the labels, and a log scale adds "logarithmic" to it. onThinnedChange hears whether an axis with a label per bucket left some out for lack of room, so an app can say that the table has them all. A second Chart.XAxis writes its row under the first, and thin={false} keeps every label of a band scale; see a ranked row. ChartGridProps, ChartXAxisProps and ChartYAxisProps are the props.

Lines and areas ​

Chart.Line draws a line through value. A missing value (null, or anything that is no finite number) breaks the line, and a value with no neighbor gets a dot, so it does not vanish. curve is linear, monotone or step (ChartCurve); dashed dashes the line and dots puts a dot on every value. width is 2 pixels, or 1 for a small chart beside others.

Chart.Area is a line with its area filled down to zero. With stack it stands on the stacked areas before it instead, and the value axis covers the top of the pile. Its fill fades toward the ground; fill="flat" tints it evenly at 28%, as a stacked area always is. width sets the line along its top, and 0 leaves the area alone, such as a share under a line of the whole.

icon takes a Lucide icon, as Icon of @adecore/ui does, and draws it in the color of the series in place of its swatch, in the legend and the tooltip; Chart.Bar takes one too. The props are ChartLineProps and ChartAreaProps; tone is a ChartColor, a ChartTone or { color }.

A small chart of samples ​

A chart beside a number in a panel: the CPU of the machine as a 1px line and the share of one app as a flat area under it, sampled every ten seconds over the last ten minutes. step={0} makes each row a sample at its moment, xOptions fixes the window so it ends at the newest sample, and margin lets the plot reach its edges. It only decorates the number above it, so it is an image (interactive={false}), and cursor="line" marks the pointer with a line alone. onActiveXChange writes the hovered sample into that number:

tsx
import { useState } from 'react';
import { Chart, type ChartX } from '@adecore/charts';
import { formatClock, formatPercent } from '@adecore/ui/format';

const WINDOW = 10 * 60_000;
const END = Date.UTC(2026, 9, 24, 14, 30);

/* A sample every 10 seconds of the last ten minutes: the whole machine and the share of one app in it, with a stretch nobody measured. */
const samples = Array.from({ length: 61 }, (_, index) => {
    const at = END - WINDOW + index * 10_000;
    const unmeasured = index >= 24 && index < 28;
    const machine = 38 + 22 * Math.sin(index / 6) + (index % 7) * 2;
    return { at, machine: unmeasured ? null : machine, app: unmeasured ? null : machine * (0.35 + 0.15 * Math.cos(index / 9)) };
});

export default function ProcessLoadDemo() {
    const [hovered, setHovered] = useState<ChartX | null>(null);
    const sample = samples.find((entry) => entry.at === hovered);
    const latest = samples.at(-1)!;
    const write = (value: number | null): string => (value === null ? '-' : formatPercent(value));

    return (
        <div className="flex w-full max-w-80 flex-col gap-1">
            <div className="flex items-baseline gap-2 text-xs">
                <span className="font-medium text-text-muted">CPU</span>
                <span className="grow" />
                <span className="truncate text-text tabular-nums">
                    {sample === undefined ? write(latest.machine) : `${formatClock(sample.at)}: ${write(sample.machine)}, this app ${write(sample.app)}`}
                </span>
            </div>
            <Chart.Root
                data={samples}
                x="at"
                xScale="time"
                step={0}
                xOptions={{ min: END - WINDOW, max: END }}
                y={{ min: 0, max: 100 }}
                height={44}
                margin={{ top: 1, right: 0, bottom: 1, left: 0 }}
                interactive={false}
                cursor="line"
                label="CPU of the machine and of this app, the last ten minutes"
                onActiveXChange={setHovered}
                className="w-full"
            >
                <Chart.Grid lines="ground" dashed />
                <Chart.Area value="app" label="This app" tone="accent" width={0} fill="flat" />
                <Chart.Line value="machine" label="Machine" tone={{ color: 'var(--text-muted)' }} width={1} />
            </Chart.Root>
        </div>
    );
}
tsx
<Chart.Root
    data={samples}
    x="at"
    xScale="time"
    step={0}
    xOptions={{ min: end - WINDOW, max: end }}
    y={{ min: 0, max: 100 }}
    height={44}
    margin={{ top: 1, right: 0, bottom: 1, left: 0 }}
    interactive={false}
    cursor="line"
    label="CPU of the machine and of this app, the last ten minutes"
    onActiveXChange={setHovered}
>
    <Chart.Grid lines="ground" dashed />
    <Chart.Area value="app" label="This app" tone="accent" width={0} fill="flat" />
    <Chart.Line value="machine" label="Machine" tone={{ color: 'var(--text-muted)' }} width={1} />
</Chart.Root>

Markers and bands ​

Chart.Marker draws a dashed vertical line at a moment, at, with a dot on top. A moment outside the x axis draws nothing. Its label is named in the tooltip of the bucket the moment falls in, so a deploy at 14:37 shows under the bucket of 14:00. Chart.Band shades a range from from to to with a hatch, named in the tooltip of every bucket it touches. A short range is still 4px wide, so it reads as a band and not as a marker. In the tooltip a marker has a dashed swatch and a band a square, both in their color, and detail adds a second line under the label, such as "Data may be missing."; the live region reads it too. The tooltip hands them to its children as ChartNotes. Both are muted unless they get a tone. On a band scale at, from and to are keys of buckets; a moment between two buckets needs a time scale, which takes columns too. The props are ChartMarkerProps and ChartBandProps.

Rules ​

Chart.Rule draws a horizontal line across the plot at value, such as a budget or a column picked as a reference, with its label and the value over its right end ("A · 71"): right aligned with the end of the line, 7px above it, in a halo of the surface so it reads over the columns, and under the line where there is no room above it. It draws above the series whatever its place among the children, and the value axis covers it, also where the data stays below it. tone colors it (muted by default) and dashed dashes it.

A rule holds for every bucket, so the tooltip does not repeat it; a screen reader hears it once, with the description of the chart. The props are ChartRuleProps.

tsx
<Chart.Rule value={300} label="Budget" tone="error" dashed />

Tooltip, legend and table ​

On a time scale the cursor takes the bucket under the pointer, the same bucket a marker there is named in; where a row is missing it shows nothing. On a linear scale it snaps to the nearest point. On a time or linear scale it draws a line with a dot on every series; on a band scale it lights the bucket. Leaving the chart clears it; with activeX controlled it calls onActiveXChange(null) instead. onActiveXChange hears the x of a bucket, once each time the pointer enters another one, and null over a missing row.

Chart.Tooltip is the card at the cursor: the x of the bucket, a row per series (with - where the series has no value), then its markers and bands. It hangs on the side of the cursor with the most room. placement fixes it in a corner of the plot instead (top-left, top-right, bottom-left or bottom-right, a ChartTooltipPlacement), where it moves to the other side when it would cover the bucket under the cursor. total adds the sum of the series as "Total", or under the name it is given (total="Runs"), and format writes the values of the card. children takes the bucket (ChartBucket, with a ChartBucketValue per series) and draws the card's contents instead.

Chart.Legend lists the series with their color, in the order of the children, above the plot. Chart.Table is a "Show values" toggle under it with the data as a table, which a screen reader reads also while it is closed. toggle={false} leaves the button out and keeps the table for a screen reader only; open, defaultOpen and onOpenChange hold it open from the app, such as a toggle it keeps in its own state. data (a ChartTableData of columns and rows, each row with a key, a label and a written value per column) shows rows of the app's own in place of the chart's, such as series the chart leaves out. The props are ChartTooltipProps, ChartLegendProps and ChartTableProps.

Durations and runs of an endpoint ​

Two charts of one endpoint over a day in buckets of 30 minutes, with the two deploys that slowed it down and fixed it again. The durations are lines with formatPreciseDuration of @adecore/ui/format on the axis and in the tooltip:

tsx
import { Chart } from '@adecore/charts';
import { formatPreciseDuration } from '@adecore/ui/format';
import { TRANSACTION_DEPLOYS, transactionPerHalfHour } from '../shared/chart-data.ts';

const buckets = transactionPerHalfHour();
const HALF_HOUR = 30 * 60_000;

export default function TransactionDurationDemo() {
    return (
        <Chart.Root data={buckets} x="at" xScale="time" step={HALF_HOUR} label="p50 and p95 duration over time" height={200} className="w-full">
            <Chart.Grid />
            <Chart.XAxis />
            <Chart.YAxis format={formatPreciseDuration} />
            <Chart.Line value="p50" label="p50" tone={{ color: 'var(--chart-3)' }} />
            <Chart.Line value="p95" label="p95" tone={{ color: 'var(--chart-1)' }} />
            {TRANSACTION_DEPLOYS.map((deploy) => (
                <Chart.Marker key={deploy.version} at={deploy.at} label={`Deploy ${deploy.version}`} tone="accent" />
            ))}
            <Chart.Tooltip />
            <Chart.Legend />
        </Chart.Root>
    );
}
tsx
<Chart.Root data={buckets} x="at" xScale="time" step={HALF_HOUR} label="p50 and p95 duration over time">
    <Chart.Grid />
    <Chart.XAxis />
    <Chart.YAxis format={formatPreciseDuration} />
    <Chart.Line value="p50" label="p50" />
    <Chart.Line value="p95" label="p95" />
    {deploys.map((deploy) => (
        <Chart.Marker key={deploy.version} at={deploy.at} label={`Deploy ${deploy.version}`} tone="accent" />
    ))}
    <Chart.Tooltip />
    <Chart.Legend />
</Chart.Root>

The runs are stacked columns on whole numbers, the failed ones in the error tone, with their sum named in the tooltip:

tsx
import { Chart } from '@adecore/charts';
import { TRANSACTION_DEPLOYS, transactionPerHalfHour } from '../shared/chart-data.ts';

const buckets = transactionPerHalfHour();
const HALF_HOUR = 30 * 60_000;

export default function TransactionRunsDemo() {
    return (
        <Chart.Root
            data={buckets}
            x="at"
            xScale="time"
            step={HALF_HOUR}
            y={{ integer: true }}
            label="Runs and failures over time"
            height={200}
            className="w-full"
        >
            <Chart.Grid />
            <Chart.XAxis />
            <Chart.YAxis />
            <Chart.Bars stack>
                <Chart.Bar value="succeeded" label="Succeeded" />
                <Chart.Bar value="failed" label="Failed" tone="error" />
            </Chart.Bars>
            {TRANSACTION_DEPLOYS.map((deploy) => (
                <Chart.Marker key={deploy.version} at={deploy.at} label={`Deploy ${deploy.version}`} tone="accent" />
            ))}
            <Chart.Tooltip total="Runs" />
            <Chart.Legend />
        </Chart.Root>
    );
}
tsx
<Chart.Root data={buckets} x="at" xScale="time" step={HALF_HOUR} y={{ integer: true }} label="Runs and failures over time">
    <Chart.Grid />
    <Chart.XAxis />
    <Chart.YAxis />
    <Chart.Bars stack>
        <Chart.Bar value="succeeded" label="Succeeded" />
        <Chart.Bar value="failed" label="Failed" tone="error" />
    </Chart.Bars>
    {deploys.map((deploy) => (
        <Chart.Marker key={deploy.version} at={deploy.at} label={`Deploy ${deploy.version}`} tone="accent" />
    ))}
    <Chart.Tooltip total="Runs" />
    <Chart.Legend />
</Chart.Root>

Put both in a Chart.Group and the cursor follows the pointer through both.

Licenses and third-party notices are listed in each package.