Skip to main content

Multi-host CPU

One line per host, from a single long-form series. The question is comparative — is this one box, or is it the fleet? — and the answer depends on being able to see a host go missing as well as go busy.

The chart

Find the flat-topped block early in the day — that's batch-01's nightly batch. Then find where web-02 stops: it doesn't drop to zero, it just stops, and the dashed connector spanning the hole is the chart saying "no data", not "no load".

All clock times below are UTC, which is how the fixture is defined; the axis renders in your local timezone, so the shapes will sit at a different hour than the prose names.

The data

Modelled, not measured. Nobody publishes fleet CPU openly at a useful resolution, so this is simulated — but modelled on the process, not a sine with noise on it. Four hosts, 24 hours from midnight UTC on an ordinary Wednesday, scraped every 2 minutes (720 points per host).

The structure the chart exists to show:

  • a diurnal load curve with a genuine trough around 04:00 — a broad daytime plateau, a slow morning ramp and a faster evening decay, not a symmetric wave;
  • a nightly batch window on batch-01, 02:00–04:30, scheduled into that trough for exactly the reason you'd expect;
  • a noisy-neighbour burst on api-01 at 14:00–14:30;
  • a node recycle on web-02 at 15:20 that leaves 24 minutes of missing scrapes — genuinely absent rows, not zeros;
  • noise whose size scales with the signal, because that's how load noise behaves.

Every generator is seeded and takes its timestamps from a fixed epoch — never Math.random(), never Date.now() — so the chart renders byte-identically on the server, on the client, and on every visit.

The series arrives in the shape ops telemetry actually arrives in: long form, one row per host per scrape, with the host as a column.

time | host | cpu
2026-05-13T00:00:00Z | web-01 | 0.21
2026-05-13T00:00:00Z | web-02 | 0.19
2026-05-13T00:00:00Z | api-01 | 0.24
...

Build it

partitionBy is the seam between that long shape and the several series a multi-line chart draws. No reshape step of your own:

const series = fleetCpu();
const byHost = useMemo(() => series.partitionBy('host').toMap(), [series]);

Then one layer per partition:

<Layers>
{FLEET_HOSTS.map((host, i) => (
<LineChart
key={host}
series={byHost.get(host)}
column="cpu"
axis="pct"
as={HOST_ROLES[i]}
legend={host}
/>
))}
</Layers>

Four hosts is inside the categorical palette, so each takes its own hue via a line role — primary, secondary, context, slow. Past about four series the rule flips to a tonal ramp (see Traffic by interface): four competing colours read as four things, eight read as a pie chart.

That gets you the shape but lies about the restart — by default the line is drawn straight through the hole, which looks like a box that idled for 24 minutes. gaps is what says otherwise:

<LineChart series={byHost.get(host)} column="cpu" gaps="dashed"/>

The line now breaks and a faint dashed connector spans the absence, so it reads as an absence. This works only because the column is declared required: false — a plain number column can't hold "missing", and the gap would have to be a zero.

Options to try

OptionWhat it doesReach for it when
gaps="dashed" / "none" / "break"How a missing run is drawnAlways decide this explicitly on scraped data
<YAxis format=".0%" min={0} max={1}>Pins the axis to 0–100% instead of the dataUtilisation, where "how close to full" beats "how much it varied"
<Legend placement>Names the linesMore than two series — position alone doesn't identify a host
cursor="line" vs "crosshair"Reads all series at one time vs one pointComparing hosts at an instant — "line" is the right one here
One <ChartRow> per hostSmall multiples instead of overlayThe lines overlap so much that the overlay is unreadable

See also