charts
Server-rendered SVG charts — no JavaScript, no external library. Pure functions that return SVG strings, composable with any layout component. All colours use design tokens and respond to light/dark theme.
Bar chart
Vertical bars with optional grid, value labels, and a zero baseline. All colour variants available.
barChart({
data: [
{ label: 'Jan', value: 42 },
{ label: 'Feb', value: 78 },
{ label: 'Mar', value: 55 },
{ label: 'Apr', value: 91 },
{ label: 'May', value: 63 },
{ label: 'Jun', value: 84 },
],
color: 'accent',
})With value labels
barChart({ data, color: 'success', showValues: true })Large dataset with tight gap
barChart({ data: traffic, color: 'blue', gap: 0.15 })Negative values
barChart({
data: [
{ label: 'Q1', value: 24 },
{ label: 'Q2', value: -8 },
{ label: 'Q3', value: 41 },
{ label: 'Q4', value: -15 },
],
color: 'warning',
showValues: true,
})Line chart
Connected data points with optional dots, area fill, and grid lines.
lineChart({
data: [
{ label: 'Jan', value: 42 },
...
],
color: 'accent',
})With area fill
lineChart({ data, color: 'accent', area: true })No dots, area fill, success colour
lineChart({ data, color: 'success', area: true, showDots: false })Donut chart
Ring chart with multiple segments. Each item can override its colour. Pass label and sublabel for a centred annotation.
donutChart({
label: '73%',
sublabel: 'satisfied',
data: [
{ label: 'Satisfied', value: 73, color: 'success' },
{ label: 'Neutral', value: 18, color: 'muted' },
{ label: 'Unsatisfied', value: 9, color: 'error' },
],
})Thinner ring
donutChart({
size: 180, thickness: 22,
label: '4', sublabel: 'segments',
data: [
{ label: 'A', value: 40, color: 'accent' },
{ label: 'B', value: 30, color: 'blue' },
{ label: 'C', value: 20, color: 'success' },
{ label: 'D', value: 10, color: 'warning' },
],
})Sparkline
Minimal inline trend line — pass a plain array of numbers. Designed to sit alongside stat() tiles or inside table cells.
sparkline({ data: [12,18,14,22,19,28,24,31], color: 'accent', area: true })
sparkline({ data: [31,24,28,19,22,14,18,12], color: 'error', area: true })Composition
Charts compose with card(), stat(), grid() — drop any chart into any content slot.
Monthly signups
Daily traffic
grid({
cols: 2,
content:
card({ title: 'Monthly signups', content: barChart({ data, height: 180 }) }) +
card({ title: 'Daily traffic', content: lineChart({ data, color: 'blue', area: true, height: 180 }) }),
})Sparkline in stat tiles
Revenue
$18.2k
+12%
Users
4,821
+8.4%
Churn
2.1%
−0.3%
card({
content: stat({ label: 'Revenue', value: '$18.2k', change: '+12%', trend: 'up' }) +
`<div style="margin-top:.75rem">${sparkline({ data, color: 'success', area: true })}</div>`,
})Props
barChart()
| Prop | Type | Default | |
|---|---|---|---|
data | array | — | { label, value }[] |
height | number | 220 | SVG height in px |
color | string | 'accent' | accent · success · warning · error · blue · muted |
showValues | boolean | false | Value labels above each bar |
showGrid | boolean | true | Horizontal grid lines |
gap | number | 0.25 | Gap between bars as fraction 0–0.9 |
lineChart()
| Prop | Type | Default | |
|---|---|---|---|
data | array | — | { label, value }[] |
height | number | 220 | SVG height in px |
color | string | 'accent' | accent · success · warning · error · blue · muted |
area | boolean | false | Fill area under the line |
showDots | boolean | true | Dots at each data point |
showGrid | boolean | true | Horizontal grid lines |
donutChart()
| Prop | Type | Default | |
|---|---|---|---|
data | array | — | { label, value, color? }[] — color per segment |
size | number | 200 | Diameter in px |
thickness | number | 40 | Ring thickness in px |
label | string | — | Large centre text |
sublabel | string | — | Smaller text below centre label |
sparkline()
| Prop | Type | Default | |
|---|---|---|---|
data | number[] | — | Plain array of numbers |
width | number | 80 | SVG width in px |
height | number | 32 | SVG height in px |
color | string | 'accent' | accent · success · warning · error · blue · muted |
area | boolean | false | Fill area under the line |