Chart
A multi-series line or area chart with real x and y axes, a time or numeric domain, and a cursor that reads every series at once.
Live demo
Chart plots one or more series against a shared x axis as an SVG that renders from data alone. Each series is a list of { at, value } samples; at may be a timestamp (epoch ms or a date string) or a plain number, and the axis labels itself accordingly — xScale is inferred by default and can be pinned to time or linear.
The domain is the data's own extent unless you set an explicit domain or a sliding window for a live feed, and samples outside it are dropped. Series take their colors from a scheme naming a palette resolved off the live theme, so a chart is coherent with the rest of the UI out of the box. Draw it as a line or an area, and join points linear, smooth, or step. The value axis derives round bounds and gridlines from the data (always including zero, and drawing a zero line when the data crosses it); yMin / yMax pin it instead. It reads out on hover and on keyboard: a crosshair snaps to the nearest x and a tooltip reports every series at that position at once, and the whole plot is mirrored into a visually-hidden data table so assistive tech reads the numbers, not the pixels. Set selectable to make it a drill-in surface: clicking (or pressing Enter at the cursor) fires a select event carrying the x position and each series' value there. A legend appears for multi-series data. With nothing to plot, it shows a muted No data message in place of the axes.
When to use
How this component composes with the rest of the set.
Props
19 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
Variants
line
The default: each series is a stroked path. Right when the shape of the trend is the message.
area
Fills the region beneath each line with a translucent wash. Right for volumes and totals, where the area under the curve means something.
States
cursor
The cursor is over a position: a crosshair snaps to it, the samples there swell, and the readout reports every series at once.
plot-focus
Keyboard focus on the plot draws a token ring; arrow keys then walk the cursor along the axis and the readout announces each position.
Anatomy
The named parts that make up the component, with their selectors.
chart
The <figure> root holding the plot, the legend, and the accessible data table.
plot
The focusable plot surface: the SVG, the crosshair, and the readout live inside it.
line
One series' stroked path; area adds a translucent fill beneath it.
point
A sample on a series, revealed by markers or when the cursor snaps to it.
axis
The x and y axis lines, their gridlines, tick labels, and optional axis titles.
guide
The crosshair that snaps to the cursor's x position.
legend
The series key, shown for multi-series charts; each item pairs a color swatch with the series name.
tooltip
The readout at the cursor: the x label, then one row per series that has a sample there.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--bg-0
--border-thick
--border-thin
--duration-fast
--ease-standard
--elevation-3
--fg-0
--fg-1
--fg-2
--fg-3
--font-sans
--leading-normal
--line
--line-2
--radius-md
--radius-sm
--ring
--space-1
--space-2
--space-3
--surface-overlay
--surface-overlay-border
--text-sm
--text-xs
--weight-semibold
Accessibility
Code
Time series
Two series over a time domain. The axis labels itself as a clock, and the cursor reads both series at once.
<xtyle-chart
label="Requests per second"
x-label="Time"
y-label="req/s"
height="280"></xtyle-chart>
<script>
const now = Date.now();
const at = (i) => now - (11 - i) * 60_000;
document.querySelector("xtyle-chart").series = [
{ name: "Edge", points: [18, 24, 21, 30, 44, 38, 52, 47, 61, 55, 68, 64].map((value, i) => ({ at: at(i), value })) },
{ name: "Origin", points: [10, 12, 15, 14, 22, 19, 26, 24, 31, 28, 34, 30].map((value, i) => ({ at: at(i), value })) },
];
</script>
<script lang="ts">
import { Chart } from "@xtyle/svelte";
const now = Date.now();
const at = (i: number) => now - (11 - i) * 60_000;
const series = [
{ name: "Edge", points: [18, 24, 21, 30, 44, 38, 52, 47, 61, 55, 68, 64].map((value, i) => ({ at: at(i), value })) },
{ name: "Origin", points: [10, 12, 15, 14, 22, 19, 26, 24, 31, 28, 34, 30].map((value, i) => ({ at: at(i), value })) },
];
</script>
<Chart {series} label="Requests per second" xLabel="Time" yLabel="req/s" height={280} />
---
import { Chart } from "@xtyle/astro";
const now = Date.now();
const at = (i: number) => now - (11 - i) * 60_000;
const series = [
{ name: "Edge", points: [18, 24, 21, 30, 44, 38, 52, 47, 61, 55, 68, 64].map((value, i) => ({ at: at(i), value })) },
{ name: "Origin", points: [10, 12, 15, 14, 22, 19, 26, 24, 31, 28, 34, 30].map((value, i) => ({ at: at(i), value })) },
];
---
<Chart series={series} label="Requests per second" xLabel="Time" yLabel="req/s" height={280} />
Area
variant="area" fills beneath each line, and curve="smooth" rounds the joins. Here on the thermal scale.
<xtyle-chart
variant="area"
curve="smooth"
scheme="thermal"
label="Memory in use"
y-label="GB"
height="240"></xtyle-chart>
<script>
document.querySelector("xtyle-chart").series = [
{ name: "Heap", points: [2.1, 2.4, 3.0, 2.8, 3.6, 4.1, 3.9, 4.6].map((value, i) => ({ at: i, value })) },
{ name: "Cache", points: [1.0, 1.2, 1.1, 1.6, 1.5, 2.0, 2.2, 2.1].map((value, i) => ({ at: i, value })) },
];
</script>
<script lang="ts">
import { Chart } from "@xtyle/svelte";
const series = [
{ name: "Heap", points: [2.1, 2.4, 3.0, 2.8, 3.6, 4.1, 3.9, 4.6].map((value, at) => ({ at, value })) },
{ name: "Cache", points: [1.0, 1.2, 1.1, 1.6, 1.5, 2.0, 2.2, 2.1].map((value, at) => ({ at, value })) },
];
</script>
<Chart {series} variant="area" curve="smooth" scheme="thermal" label="Memory in use" yLabel="GB" height={240} />
---
import { Chart } from "@xtyle/astro";
const series = [
{ name: "Heap", points: [2.1, 2.4, 3.0, 2.8, 3.6, 4.1, 3.9, 4.6].map((value, at) => ({ at, value })) },
{ name: "Cache", points: [1.0, 1.2, 1.1, 1.6, 1.5, 2.0, 2.2, 2.1].map((value, at) => ({ at, value })) },
];
---
<Chart series={series} variant="area" curve="smooth" scheme="thermal" label="Memory in use" yLabel="GB" height={240} />
Numeric domain
xScale="linear" plots against a plain number rather than a clock; curve="step" and markers suit discrete measurements.
<xtyle-chart
x-scale="linear"
curve="step"
markers
x-label="Concurrency"
y-label="ms"
label="Latency by concurrency"
height="240"></xtyle-chart>
<script>
document.querySelector("xtyle-chart").series = [
{ name: "p50", points: [1, 2, 4, 8, 16, 32].map((at, i) => ({ at, value: [12, 14, 19, 28, 46, 91][i] })) },
{ name: "p99", points: [1, 2, 4, 8, 16, 32].map((at, i) => ({ at, value: [30, 38, 55, 92, 170, 320][i] })) },
];
</script>
<script lang="ts">
import { Chart } from "@xtyle/svelte";
const load = [1, 2, 4, 8, 16, 32];
const series = [
{ name: "p50", points: load.map((at, i) => ({ at, value: [12, 14, 19, 28, 46, 91][i] })) },
{ name: "p99", points: load.map((at, i) => ({ at, value: [30, 38, 55, 92, 170, 320][i] })) },
];
</script>
<Chart {series} xScale="linear" curve="step" markers xLabel="Concurrency" yLabel="ms" label="Latency by concurrency" height={240} />
---
import { Chart } from "@xtyle/astro";
const load = [1, 2, 4, 8, 16, 32];
const series = [
{ name: "p50", points: load.map((at, i) => ({ at, value: [12, 14, 19, 28, 46, 91][i] })) },
{ name: "p99", points: load.map((at, i) => ({ at, value: [30, 38, 55, 92, 170, 320][i] })) },
];
---
<Chart series={series} xScale="linear" curve="step" markers xLabel="Concurrency" yLabel="ms" label="Latency by concurrency" height={240} />
Drill in
selectable fires select on click or Enter, carrying the cursor's x and every series' value there.
<xtyle-chart
id="deploys"
selectable
label="Error rate"
y-label="%"
height="220"></xtyle-chart>
<script>
const chart = document.getElementById("deploys");
chart.series = [{ name: "5xx", points: [0.2, 0.3, 1.9, 0.4, 0.3].map((value, at) => ({ at, value })) }];
chart.addEventListener("select", (e) => {
openIncident(e.detail.x, e.detail.points[0].value);
});
</script>
<script lang="ts">
import { Chart } from "@xtyle/svelte";
const series = [{ name: "5xx", points: [0.2, 0.3, 1.9, 0.4, 0.3].map((value, at) => ({ at, value })) }];
</script>
<Chart
{series}
selectable
label="Error rate"
yLabel="%"
height={220}
onselect={(e) => openIncident(e.detail.x, e.detail.points[0].value)}
/>
---
import { Chart } from "@xtyle/astro";
const series = [{ name: "5xx", points: [0.2, 0.3, 1.9, 0.4, 0.3].map((value, at) => ({ at, value })) }];
---
<Chart id="deploys" series={series} selectable label="Error rate" yLabel="%" height={220} />
<script>
document.getElementById("deploys").addEventListener("select", (e) => {
openIncident(e.detail.x, e.detail.points[0].value);
});
</script>