Skip to content
GitSiteEmail

Chart is a family of data-visualisation components. They share one data model, one palette, one animation system, and one set of slots, so the knowledge you build on one chart applies to the rest.

ComponentUse it for
Chart.PieParts of a whole with a small number of slices.
Chart.DonutSame as pie, with a total or hovered value in the middle.
Chart.BarComparing values across categories and series.
Chart.LineA value traced across ordered categories.
Chart.AreaSame as line when filled magnitude or summed series matter.
Chart.SpiderSeveral measures at once, where the overall shape is the point.

Every chart takes the same data array. Each item is a series, and each series holds data points.

interface ChartPoint {
type: string; // category key; it is the identity used across data changes
value: number;
color?: string; // overrides the series colour
label?: string; // display text, defaults to `type`
meta?: Record<string, unknown>; // forwarded to tooltips and callbacks
}
interface ChartSeries {
points: ChartPoint[];
id?: string; // stable identity across data changes; defaults to the index
label?: string;
color?: string;
class?: string;
style?: JSX.CSSProperties;
}

Pie, donut, and spider read one series. Bar, line, and area read as many as you give them.

data is reactive: feed it from a signal and the chart follows every change.

Two rules are worth knowing:

  • Categories are the union of every series. If one series declares a category that another omits, it counts as 0, so series do not need to match exactly. A line chart can treat this differently with connectNulls.
  • type is identity. It is how a point is matched to its previous version when the data changes, which is what allows smooth animation instead of snapping.

Marks take their colour from a built-in eight-slot categorical palette, assigned by index:

point.color ?? series.color ?? palette[index % palette.length]

The palette is tuned for colourblind separation on both light and dark surfaces. Its order matters: neighbouring colours are the most distinct, because neighbouring marks are the ones people compare. To use your own colours, pass a palette:

<Chart.Bar data={data()} palette={['#e5484d', '#30a46c', '#3987e5']} />

Hiding a series from the legend does not change the colours of the others.

Animation is off by default. Pass an animation object and the chart tweens between data sets:

<Chart.Bar data={data()} animation={{ duration: 300, easing: 'ease-out' }} />
FieldDefaultEffect
duration300Milliseconds. 0 snaps.
easing'ease-out''linear', 'ease-in', 'ease-out', 'ease-in-out', or a custom (t) => number.
delay0Milliseconds before the animation starts.
stagger0Extra delay for each mark, for a cascading entrance.
onStart / onEnd—Lifecycle callbacks.

What you get:

  • Values interpolate, not pictures. Geometry is rebuilt every frame, so every intermediate state is real geometry rather than a morph between two shapes.
  • Marks enter from nothing. A new slice grows in place instead of popping in.
  • Marks leave gracefully. A removed slice shrinks away instead of blinking out.
  • Interrupting is safe. Changing the data mid-animation continues from the current state without a snap-back.
  • Lines reveal themselves. A line or area given an animation on mount reveals from one end.

With no animation prop, a mounted chart does zero per-frame work.

Interactivity is also off by default. A chart with no interactive prop attaches no listeners and sets pointer-events: none, so input passes straight through to the game underneath.

<Chart.Bar data={data()} interactive onPointClick={(e) => inspect(e.point.type)}>
<Chart.Tooltip content={(e) => <div>{e.series.label}: {e.point.value}</div>} />
</Chart.Bar>
ValueBehaviour
omitted / falseNo listeners.
trueUses the correct hit model for the chart type.
"svg"Native SVG pointer events, one listener per mark.
"overlay"A single transparent overlay resolves the hovered mark mathematically.

true resolves to native SVG events for pie, donut, and bar, and to overlay mode for line, area, and spider:

  • A 2px line stroke is effectively impossible to target, so those charts resolve the nearest vertex and snap the tooltip to it, which makes the crosshair readable.
  • A spider series is a single closed path spanning every category, so native hit-testing can only answer “which series” and not “which point”.

Props that set a size — such as a corner radius, a stroke width, or the donut hole — accept either a number or a CSS length string:

ValueMeaning
4 or '4px'Pixels. Fixed at every resolution.
'0.25rem'Relative to the root font size, so it scales with the rest of the UI.
'1vh', '1vw', '1vmin', '1vmax'Relative to the viewport.
'60%'A share of something the chart measures. This is only valid where the prop says it is.

Most defaults are in rem, so unchanged charts scale with your UI. Prefer rem for your own values too, unless you want a fixed size at every resolution.

Every chart type takes these, alongside its own props.

Prop NameTypeDefaultDescription
styleJSX.CSSProperties{}Inline styles applied to the chart root.
classstring""Additional CSS classes applied to the chart.
dataChartSeries[][]The series to plot. Reactive.
palettestring[]built-inOverrides the default categorical palette.
animationChartAnimationundefinedEnables tweening between data sets. Omit for no animation.
interactiveboolean | 'svg' | 'overlay'falseEnables hover, tooltip, and click behavior. See Interactivity.
onPointHover(event: ChartPointEvent | null) => voidundefinedFires when the hovered point changes; null on leave.
onPointClick(event: ChartPointEvent) => voidundefinedFires when a point is clicked.
onSeriesToggle(seriesIndex: number, visible: boolean) => voidundefinedFires when an entry is toggled from the legend.

Charts are configured by their children rather than by a long prop list.

SlotAvailable onPurpose
Chart.LegendallSeries or slices. Clicking an entry toggles it.
Chart.TooltipallFollows the hovered point. Requires interactive.
Chart.LabelsallDirect value labels on the marks.
Chart.XAxis / Chart.YAxisbar, line, areaTick labels, formatting, and baseline.
Chart.Gridbar, line, areaGridlines.
Chart.<Type>.*per chartThe marks themselves. See each chart’s page.

Everything is opt-in. No Chart.Grid, no gridlines; no Chart.Legend, no legend.

Prop NameTypeDefaultDescription
classstring""CSS classes applied to the legend container.
styleJSX.CSSProperties{}Inline styles applied to the legend container.
position'top' | 'bottom' | 'left' | 'right''bottom'Which edge the legend sits on.
interactivebooleantrueWhether clicking an entry toggles it.
content(entry: LegendEntry) => JSX.ElementundefinedReplaces the markup of a single entry.

Pie and donut legends list slices; every other chart lists series. Hiding an entry does not change the colours of the others.

<Chart.Legend content={(entry) => <MyEntry color={entry.color} label={entry.label} />} />

Only rendered while a point is hovered, so it requires interactive.

Prop NameTypeDefaultDescription
classstring""CSS classes applied to the tooltip.
styleJSX.CSSProperties{}Inline styles applied to the tooltip.
position'top' | 'bottom' | 'left' | 'right' | 'auto''auto'Which side of the pointer the tooltip sits on. auto flips away from the viewport edge.
offsetChartLength'0.75rem'Distance between the pointer and the tooltip.
content(event: ChartPointEvent) => JSX.ElementundefinedReplaces the tooltip markup.

Direct labels drawn as HTML over the chart.

Prop NameTypeDefaultDescription
classstring""CSS classes applied to every label.
styleJSX.CSSProperties{}Inline styles applied to every label.
show'always' | 'hover' | 'never''always'When labels are visible.
format(point: ChartPoint, series: ChartSeries) => stringundefinedBuilds the label text. Defaults to the point’s label (then type) on a pie or donut slice and to its value elsewhere.
placement'inside' | 'outside' | 'hole''inside'Pie and donut only. 'hole' is donut only.
leaderLinesbooleanfalsePie and donut only. Connects each slice to its outside label.

The label position depends on the chart: inside or outside each slice on a pie, centered in each bar, and just above each vertex on line, area, and spider charts.

Which one is the value axis depends on the chart orientation: YAxis on a vertical chart and XAxis on a horizontal bar chart. Omit a slot to hide that axis entirely.

Prop NameTypeDefaultDescription
classstring""CSS classes applied to every tick label on this axis.
styleJSX.CSSProperties{}Inline styles applied to every tick label.
ticksnumber5A hint for how many ticks to aim for. Values are still chosen to be round.
format(value: number | string) => stringundefinedFormats the tick label.
linebooleantrueDraws the zero baseline.
hidebooleanfalseHides the axis while keeping its space reserved.
Prop NameTypeDefaultDescription
classstring""CSS classes applied to every gridline.
styleJSX.CSSProperties{}Inline styles applied to every gridline.
horizontalbooleantrueLines across the value axis.
verticalbooleanfalseLines along the category axis.
ticksnumber5How many gridlines to aim for.

ChartPoint and ChartSeries are described under the data model.

type ChartLength = number | string; // see "Lengths" above
interface ChartAnimation {
duration?: number; // ms; default 300, 0 snaps
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | ((t: number) => number);
delay?: number;
stagger?: number; // extra delay per mark
onStart?: () => void;
onEnd?: () => void;
}
interface ChartPointEvent {
point: ChartPoint;
series: ChartSeries;
seriesIndex: number;
pointIndex: number;
position: { x: number; y: number }; // within the chart, in pixels
}
interface LegendEntry {
label: string;
color: string;
seriesIndex: number;
pointIndex?: number; // set when the entry is a single slice
visible: boolean;
}

These charts are built to sit in a HUD that shares a frame with the game.

ConcernGuidance
Mark countOne SVG path per mark. Comfortable to roughly 200; the chart warns past 500.
AnimationOne signal write per frame regardless of mark count. Above roughly 200 animated marks, prefer no animation.
Hit-testing"svg" attaches three listeners per mark; "overlay" attaches one total. Above roughly 100 marks, prefer the overlay.
LabelsOne element per visible label. Above roughly 20 points, prefer <Chart.Labels show="hover" />.
IdleWith neither animation nor interactivity, a chart costs nothing per frame.
  • Show a legend whenever there are two or more series. Identity should never rely on colour alone.
  • With a handful of marks, direct labels (Chart.Labels) beat a legend because they place the name next to the thing.
  • Text uses text colours, never the series colour. A coloured mark next to a label carries the identity.
  • Avoid a second value axis. Two measures of different scales belong in two charts or should be indexed to a common base.