graph-explorer v0.7.0

Browser graph exploration: a Rust/WebAssembly engine rendering through WebGL2, force-directed layout, and a typed TypeScript client. Styling is declarative — you describe rules, not draw calls.

Demos

Explorer
The full engine: a generated world you navigate by keyboard or pointer, with paged neighbour loading, aggregates, and a stress harness that scales to thousands of nodes.
vim keys · pointer · previews · stress
Style configurator
Four themes over one fixed dataset, each leaning on a different part of the spec. Edit the JSON live and the picture follows.
StyleSpec · rules · scales · background
React component
The same engine as <GraphExplorer>, with a node-details pane and a responsive layout. Physics on, drag a node and the neighbourhood follows.
/react · node details · responsive

Quick start

// npm install @benjamin-small/graph-explorer
import { mountGraph } from "@benjamin-small/graph-explorer/mount";
import { attachPointer } from "@benjamin-small/graph-explorer/pointer";

// The canvas must have a non-zero size before mounting.
const view = await mountGraph("canvas-id");

view.setStyle({
  node_base: { color: "#4dd0e1", radius: 10, shape: "circle", label_visible: true },
  node_rules: [
    { when: { attr: "kind", equals: "database" }, set: { shape: "square" } },
    { scale: { by: "rps", property: "radius", domain: [0, 9800], range: [7, 24] } },
  ],
  edge_base: { color: "#4b858f", width: 1.3 },
});

view.load(JSON.stringify({ nodes: [...], edges: [...] }));
view.fitView();
attachPointer(view, canvas);   // click, drag, wheel-zoom
view.start();                   // render loop; parks itself when idle

API

Entry points

Six subpaths, so a consumer pays only for what it imports. The wasm binary is pulled in by /mount, not by the root import.

ImportProvides
@benjamin-small/graph-explorerTypes only — StyleSpec, AnimationSpec, SimulationSpec, NodeDetails, GraphSnapshot.
…/mountmountGraph(canvasId) → a GraphClient. Loads and initialises the wasm engine.
…/pointerattachPointer(client, canvas) — click, drag, double-click, wheel zoom.
…/vimattachVim(client) — modal keyboard navigation.
…/react<GraphExplorer> and useGraphSnapshot. React is an optional peer dependency.
…/testingFakeEngine — drive a GraphClient in tests with no GPU.

GraphClient

The typed wrapper returned by mountGraph. Every method below is safe to call after dispose() only in the sense that it throws a clear error rather than reading freed wasm memory.

GroupMethods
Data load(json), loadRemote(seedJson), setFetchFn(fn), retryFailed()
Appearance setStyle(spec), setBackground(hex), setAnimation(spec), setSimulation(spec), setNodeStyler(fn), setEdgeStyler(fn), setHaloFn(fn), setIdleFn(fn)
Camera fitView(), pan(dx, dy), zoom(factor), zoomIn(), zoomOut(), resize(w, h), setPixelRatio(r)
Navigation focus(id), selectId(id), select(delta), sibling(delta), leap(dir), descend(), back(), overview(), toggleView(), setViewMode(mode)
Interaction nodeAt(x, y), nodeDetails(id), visibleNodes(), graphNodes(), storeNodes(), dragStart(id), dragTo(x, y), dragEnd(), unpinNode(id), unpinAll()
Layout setPositions(map), setLayout(value), relayout()
Loop start(), stop(), render(now), running, scheduled, dispose()
State on(event, cb), subscribe(cb), getSnapshot(), selection(), preview, mode(), clearError()

Coordinates

nodeAt, dragTo and the node-list accessors all speak physical pixels — the canvas backing store, not CSS pixels. On a HiDPI display those differ by devicePixelRatio. One convention across the whole pointer surface, so a host applies the conversion once.

Specs

Three plain JSON documents. All fields are optional; anything omitted keeps its current value rather than resetting to a default.

SpecGovernsNotable
StyleSpec Colour, radius, shape, opacity, labels, background Rules apply in order, last write wins — put role rules last or navigation stops being legible
AnimationSpec Transitions, halo pulse, reduced motion camera_follow re-frames on every navigation; fit_on_settle only after a drag
SimulationSpec Force layout — link distance, charge, centring, drag release enabled: false still runs one synchronous settle on load

Styling by rule

{
  node_base: { color: "#7c8a99", radius: 10, shape: "circle" },
  node_rules: [
    // Data rules first…
    { when: { attr: "kind", one_of: ["database", "queue"] }, set: { shape: "square" } },
    { scale: { by: "rps", property: "radius", domain: [3, 9800], range: [7, 26] } },
    { when: { attr: "status", equals: "down" }, set: { color: "#ff1744" } },
    // …role rules LAST, so the current node always reads as current.
    { when: { role: "focus" }, set: { color: "#ffffff" } },
  ],
}

Notes

Putting this in a real page? The browser guide covers the canvas contract, DPI and resizing, serving the wasm through a bundler, CSP, SSR, context loss, and a symptom-to-cause table for the failures each of those produces when it is wrong.

Multiple explorers on one page

Supported. The ceiling is the browser's WebGL context cap, not this library — Chrome allows 16 live contexts and force-loses the oldest beyond that, measured as exactly N − 16 losses at N = 24. Concurrent mountGraph calls are serialised internally, so mounting several in one tick is safe.

Idle cost

The render loop parks when nothing is moving and any mutating call wakes it. A pulsing halo keeps a cheap halo-only frame going; reduced motion or no halos lets it stop entirely. At 2000 nodes on a 2400×1600 backing store an idle frame costs about 1.3ms GPU-synced, a halo-only frame about 0.9ms, and a fully idle graph nothing at all.

Idle animation

The opposite trade, off by default: AnimationSpec.idle plays ambient motion — drift, breathe, or shimmer — after ~2s of quiet, on a reduced frame tier that skips styling, fetches and the simulation. Any real activity suppresses it instantly; reduced_motion disables it. Drift is draw-time by default (mode: "visual", node lists keep reporting rest positions) or applied to real positions ("physical"). setIdleFn computes custom motion in JS — one call per node per frame, so prefer the presets on large graphs. Try it in the explorer demo's idle motion controls.

Pre-1.0. The API is additive so far, but not frozen — minor versions may still change behaviour. The changelog calls out anything that alters existing output, including the sRGB correction in v0.4.0 that changes how previously-tuned palettes render.