A high-performance, canvas-first visualization engine for 1,000+ interactive elements,
implemented on d3 (layout/math) with a bespoke Canvas 2D renderer. Framework-
agnostic and WebGL-ready — the renderer is swappable behind an abstract DrawList
boundary, proven by a dual-backend gate. First domain: radial dendrograms with
expand/collapse, per-node drag, animation, and pan/zoom.
- No SVG. d3 is used for layout/spatial/scale math +
d3-shapedrawn into canvas. - Scales with node count — auto-radius layout so nodes never overlap; constant-size crisp labels at any zoom.
- Measured: 2,000 elements at ~2.6 ms/frame (p95) under 4× CPU throttle; 10k within
the 16.6 ms budget (
apps/bench).
Use @ge/graph — the framework-agnostic GraphController.
It mounts a graph into a DOM element with collapse/expand, drag, animation, and pan/zoom built in.
import { GraphController } from '@ge/graph';
const graph = new GraphController(
document.getElementById('app')!,
{ parentIndex: [-1, 0, 0, 0, 1, 1, 3] }, // flat hierarchy: parentIndex[i] = parent, -1 = root
{ nodeSpacing: 16 },
);
graph.on('collapse', ({ id }) => console.log('collapsed', id));
graph.toggle(0);
// … on unmount: graph.destroy()➡ Full API reference, events, and React/Vue/vanilla examples: packages/graph/README.md.
The engine is modular; each package is independently replaceable behind an interface.
@ge/graph composes them — most users only need that one.
| Package | Role | Public surface |
|---|---|---|
@ge/graph |
Public API — GraphController facade + events, GraphModel, EventEmitter |
new GraphController(el, data, opts) |
@ge/contracts |
Shared interfaces + flag bits (zero-dep DI surface) | LayoutEngine, SpatialIndex, Renderer-adjacent types, FLAG_* |
@ge/renderer |
The load-bearing boundary: abstract Renderer + declarative DrawList |
Renderer, DrawList, ShapeBatch/EdgeBatch/LabelBatch |
@ge/renderer-canvas |
Canvas 2D backend (sole owner of CanvasRenderingContext2D) |
CanvasRenderer |
@ge/renderer-webgl-stub |
POC backend proving the DrawList boundary is WebGL-ready |
WebGLRenderer (stub) |
@ge/geometry-store |
Struct-of-Arrays typed-array node/edge store + object pools | TypedGeometryStore, ObjectPool |
@ge/layout-d3 |
d3-hierarchy radial cluster layout with auto-radius (no overlap) | RadialClusterLayout, RadialLayoutOptions |
@ge/layout-worker |
Off-thread layout via comlink (zero-copy transfer) | LayoutSupervisor |
@ge/spatial-index |
O(log n) hit-testing | QuadtreeIndex (+ PolarIndex stub) |
@ge/viewport |
Camera ({tx,ty,scale}) + culling + native pointer/wheel input |
Camera, CartesianCuller, InputController |
@ge/interaction |
Hit-testing + behaviors | PointerInteractionEngine, Hover/Select/CollapseExpand/DragNode behaviors |
@ge/animation |
d3-interpolate position tweens |
TweenAnimationEngine |
@ge/core |
Engine loop, DI EngineContext, extension registry, scene-graph, frame scheduler |
Engine, register/get, SceneGraph, FrameScheduler |
@ge/domain-radial |
Radial dendrogram edge/label builders | buildEdgeBatch, buildLabelBatch |
Apps: apps/playground (Vite demo using @ge/graph) · apps/bench (CDP perf harness + the WebGL POC gate).
data ──▶ @ge/layout-d3 (RadialClusterLayout, auto-radius) ──▶ @ge/geometry-store (SoA typed arrays)
──▶ @ge/spatial-index (hit-test) · @ge/viewport (cull/camera) · @ge/animation (tweens)
──▶ @ge/renderer DrawList ──▶ @ge/renderer-canvas (Canvas2D) [swap-in: WebGL]
▲ │
└────────── @ge/graph GraphController (events + gestures) ─────┘
DrawListboundary (Rule 1): core/domain/interaction emit declarative typed-array batches;CanvasRenderingContext2Dlives only inrenderer-canvas. A WebGL backend drops in without touching core (gate-tested inapps/bench).- No per-frame allocation on the render path (object pools + reused typed arrays).
- Labels render in device space at constant size → always crisp, never blurry on zoom.
Design records: .omc/specs/ · build status: BUILD-STATUS.md.
bun install
turbo run check-types # strict TS across the workspace
turbo run build # tsup ESM + .d.ts per package; Vite for apps
bun run test # unit tests (bun test, scoped to our packages — vendor excluded)
bun run --filter @app/playground dev # open the interactive demo
bun run apps/bench/src/run-cdp.mjs # perf sweep (needs google-chrome) → results.jsonToolchain: TypeScript 5.9 (strict) · bun · turbo · Node ≥18.