Skip to content

Plot overview

If you’ve reached for Observable Plot or ggplot2 before, insomni-plot will feel like home — you describe a chart by what the data means, not by what to draw. Map columns to channels, pick your marks, and let the library work out scales, axes, and layout. The twist: it compiles that spec into insomni layers and renders on the GPU, so the same declarative grammar you’d use for a 200-point scatter also drives a 200,000-point one without breaking a sweat.

Two things to know up front, because they shape everything else:

  • The Chart builder is immutable. Every method — .layer(), .scale(), .axes(), and friends — returns a new Chart. Your base spec is never mutated, so you can fork it freely (more on why that’s useful below).
  • Nothing renders until you ask. A Chart is just a description. It stays inert until you call .mount(canvas), .render(target), or .toSVG().

plot(spec) takes a PlotSpec<T> — at minimum { data } — and hands you back a Chart<T>. From there you add marks with .layer(...) and tune the rest:

import { plot, point } from "insomni-plot";
type Row = { weight: number; mpg: number; cylinders: number };
const data: Row[] = [
{ weight: 3.2, mpg: 21, cylinders: 6 },
{ weight: 1.6, mpg: 39, cylinders: 4 },
{ weight: 4.1, mpg: 15, cylinders: 8 },
];
// WebGPU needs a device — grab one before you mount.
const device = await (await navigator.gpu.requestAdapter())!.requestDevice();
const canvas = document.querySelector("canvas")!;
const chart = plot<Row>({ data })
.layer(point({ x: "weight", y: "mpg", color: "cylinders" }))
.axes({ x: { title: "Weight" }, y: { title: "MPG" } })
.title("Fuel economy");
const mounted = chart.mount(canvas, { device });
// Later — swap the data live, then tear it all down.
mounted.setData(data);
mounted.destroy();

An aesthetic is how a data column becomes something you can see — a position, a color, a size. Every channel (x, y, color, size, …) accepts three shapes, all unified under the Aes<T, V> type:

  • a column namex: "weight"
  • an accessorx: (d) => d.weight * 2
  • a constantcolor: "#888" or size: 4

Here’s a nicety you’ll appreciate: column-name autocomplete is type-narrowed. If a key’s value type doesn’t match what the channel expects, TypeScript rejects it at compile time — so you find out about x: "name" on a numeric axis before you ever hit the GPU. The full channel list lives in Scales & aesthetics.

Because every builder method returns a fresh Chart, a base spec is something you can branch from without fear:

const base = plot<Row>({ data }).axes({ x: { title: "t" } });
const dots = base.layer(point({ x: "t", y: "v" }));
const both = dots.layer(line({ x: "t", y: "v" }));
// `base` and `dots` are still exactly what they were.

Cheap to fork, cheap to share, cheap to re-derive. Faceting and small-multiples lean on this directly.

  • insomni-plot — the grammar. plot(spec) returns an immutable Chart you compose with geoms, coordinate systems (coordCartesian / coordPolar / coordRadial), themes, faceting, annotations, color palettes, and stats helpers (bin, kde, linearFit, …). This is the one you want 90% of the time.
  • insomni-plot/core — the imperative primitives the grammar is built on: scales (linearScale, bandScale, timeScale, logScale, …), axis builders (bottomAxis, leftAxis, …), mark builders (pointMark, lineMark, barMark, areaMark, stacking), legends, color scales, formatters, and the data viewport / navigator. Drop down here when you want to hand-wire a render without the Chart builder, or pull a single primitive out on its own.