Numeric & category X axes

Not every X is a timestamp. A load test runs over concurrency levels, a ride over distance; a breakdown runs over regions, a poll over options. Line, bar and band series plot — and stream — over numbers or names as well as time.

01 — PICK THE AXIS TYPE
axis.x.type is 'time' by default. 'numeric' maps plain numbers, 'category' maps names. It is read once, when the chart is created — remount the chart (a React key) to switch.
<ChartContainer axis={{ x: { type: 'numeric' } }}>…</ChartContainer>
<ChartContainer axis={{ x: { type: 'category' } }}>…</ChartContainer>
02 — KEY THE POINTS BY X
Points carry x in place of time. On a numeric axis the spacing can be irregular — the line draws at each x and the crosshair snaps to the nearest point. A point whose x doesn't fit the axis is skipped with a dev warning.
<LineSeries data={[{ x: 1, value: 120 }, { x: 16, value: 940 }, { x: 64, value: 1235 }]} />
03 — CATEGORIES COME FROM THE DATA
Each category gets one band, in the order the series first name it. Bars fill their band; axis.x.padding sets the gap between bands (inner) and at the ends (outer), as fractions of a step. Drag and wheel are off on a category axis — set axis.x.panZoom to change that.
<ChartContainer axis={{ x: { type: 'category', padding: { inner: 0.3, outer: 0.2 } } }}>
<BarSeries data={[{ x: 'Europe', value: 42 }, { x: 'Asia', value: 35 }]} />
</ChartContainer>
04 — FORMAT THE X
<XAxis format> gets the number or the category name and formats the tick labels, the crosshair label and the tooltip header. crosshairMove and pointClick report it as x.
<XAxis format={(x) => `${Number(x).toFixed(1)} km`} />
05 — STREAM A NUMERIC AXIS LIKE TIME
Keep the points in state and pass them to data. The diff keys on x: a point with a new, larger x is appended with its entrance animation, and .slice(-n) rolls the window while auto-scroll keeps the newest point in view. x must increase from point to point, as time does. Imperatively, appendData and keepLast do the same.
setRide((prev) => {
const last = prev[prev.length - 1];
 
return [...prev, { x: last.x + 0.2, value: 284 }].slice(-80);
});
 
<LineSeries data={ride} />
06 — UPDATE ANY CATEGORY
Pass the new array: every bar that keeps its band eases to its new value, and the Y axis eases with it. A new name adds a band and the bands refit so every category stays in view — a category axis doesn't scroll. Imperatively, pass the whole array to setSeriesData; updateData replaces only the layer's last point.
setVotes((prev) =>
prev.some((v) => v.x === pick)
? prev.map((v) => (v.x === pick ? { ...v, value: v.value + 1 } : v))
: [...prev, { x: pick, value: 1 }], // a new band
);
Load testconcurrency → req/s
0204060103050
01000
Live ridedistance, km → elevation, m
2.0 km4.0 km6.0 km1.0 km3.0 km5.0 km
400
Users by regionmonthly active users, M
EUROPEASIAAMERICASAFRICAOCEANIA
50
Live pollvotes per language
TypeScriptPythonRustGo
10