# @vhult/graph 0.2.0 > WebGPU graph rendering engine. Millions of nodes and edges Install: `npm install @vhult/graph`. Source: https://github.com/vhult/graph. Package: https://www.npmjs.com/package/@vhult/graph. ## Quick start ```ts import { Graph, packRgba } from "@vhult/graph"; const graph = await Graph.create(document.querySelector("canvas")!); graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 100, 0, 50, 80]), sizes: new Float32Array([10, 10, 10]), colors: new Uint32Array(3).fill(packRgba(1, 0.4, 0.2)), }); graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]) }); graph.camera.fit(); graph.on("click", (hit) => console.log(hit.node)); ``` # Guides ## Getting started Install the package, draw a first graph and clean up after it. ### Install ```sh npm install @vhult/graph ``` The package has no runtime dependency. It needs a browser with WebGPU: Chrome and Edge 113 or later, Firefox 141 or later on Windows, Safari 26 or later. ### A first graph `Graph.create` takes a canvas, moves it to a render worker and resolves once WebGPU is ready. Nodes and edges are typed arrays: one entry per node for sizes and colors, two for positions (x, y) and two per edge for indices (source, target). ```ts import { Graph, packRgba } from "@vhult/graph"; const canvas = document.querySelector("canvas")!; const graph = await Graph.create(canvas); graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 100, 0, 50, 80]), sizes: new Float32Array([10, 10, 10]), colors: new Uint32Array(3).fill(packRgba(1, 0.4, 0.2)), }); graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]) }); graph.camera.fit(); ``` Set the nodes before the edges: `nodes.set` starts over and removes every edge. The canvas follows its CSS size by itself, so give it a size in your layout. > **Note:** Arrays you pass are transferred to the worker, not copied, so they are empty after the call. Pass `{ copy: true }` as the last argument to keep using them. ### Clean up `graph.destroy()` releases the worker, the GPU device and every listener. It is safe to call twice. ```ts graph.destroy(); ``` ### Errors `Graph.create` rejects with an `UnsupportedError` when the browser or the GPU cannot run the engine, and with a `GraphError` for other failures. Both carry a `code`. After creation, failures arrive through the `error` event. ```ts import { Graph, UnsupportedError } from "@vhult/graph"; try { const graph = await Graph.create(canvas); graph.on("error", (e) => console.error(e.code, e.message)); } catch (e) { if (e instanceof UnsupportedError) showFallback(e.message); else throw e; } ``` ### With a bundler The engine loads its worker with `new URL("./worker.js", import.meta.url)`, which bundlers follow. With Vite, keep the package out of the dependency pre-bundling so the dev server serves the worker next to it. ```ts import { defineConfig } from "vite"; export default defineConfig({ optimizeDeps: { exclude: ["@vhult/graph"] }, worker: { format: "es" }, }); ``` ### With React A canvas can move to a worker only once. Create the canvas inside the effect, so the effect can run twice in strict mode without reusing it. ```tsx import { Graph } from "@vhult/graph"; import { useEffect, useRef } from "react"; export function GraphView() { const host = useRef(null); useEffect(() => { const canvas = document.createElement("canvas"); host.current!.append(canvas); let graph: Graph | null = null; let live = true; Graph.create(canvas).then((g) => { if (!live) return g.destroy(); graph = g; }); return () => { live = false; graph?.destroy(); canvas.remove(); }; }, []); return
; } ``` ## Nodes and edges Load, add, change and remove nodes and edges with typed arrays. ### Channels A node is a slot index. Each field of `NodeData` is one channel, a typed array with one or more values per node. Leave a channel out to keep its default. | Channel | Type | Values per node | | --- | --- | --- | | `positions` | `Float32Array` | 2: x, y in world units | | `sizes` | `Float32Array` | 1: diameter in world units | | `colors` | `Uint32Array` | 1: packed RGBA, see `packRgba` | | `shapes` | `Uint8Array` | 1: one of `NodeShape` | | `zIndex` | `Uint8Array` | 1: layer from 0 to 15, higher on top | | `icons` | `Uint16Array` | 1: icon id, or `NO_ICON` | | `iconColors` | `Uint32Array` | 1: packed RGBA icon tint | | `labels` | string array | 1: label text, or null | Edges work the same way with `EdgeData`: `indices` holds the source and target node of each edge, `colors` holds two packed colors per edge (at the source, then at the target), `styles` holds one word per edge from `packEdgeStyle`, and `labels` one text per edge. ### Load everything `nodes.set` and `edges.set` replace everything. They are the fastest way to load a graph. `nodes.set` also removes every edge, so call it first. ```ts import { NodeShape, packEdgeStyle, packRgba } from "@vhult/graph"; graph.nodes.set({ count: 4, positions: new Float32Array([0, 0, 120, 0, 120, 120, 0, 120]), sizes: new Float32Array([16, 10, 10, 10]), colors: new Uint32Array([packRgba(0.3, 0.6, 1), packRgba(0.7, 0.5, 1), packRgba(0.2, 0.8, 0.6), packRgba(1, 0.4, 0.2)]), shapes: new Uint8Array([NodeShape.hexagon, NodeShape.circle, NodeShape.square, NodeShape.circle]), labels: ["hub", "a", "b", "c"], }); graph.edges.set({ count: 3, indices: new Uint32Array([0, 1, 0, 2, 0, 3]), styles: new Uint32Array(3).fill(packEdgeStyle({ width: 2, directed: true })), }); ``` > **Note:** Arrays are transferred to the worker, so they are empty after the call. Pass `{ copy: true }` to keep them: `graph.nodes.set(data, { copy: true })`. ### Add and remove `nodes.add` and `edges.add` return the indices of the new slots. `nodes.remove` also removes the edges of those nodes and reports them in the `edgesRemoved` event. ```ts const [a, b] = graph.nodes.add({ count: 2, positions: new Float32Array([200, 0, 200, 80]) }); graph.edges.add({ count: 1, indices: new Uint32Array([a, b]) }); graph.nodes.remove([a]); ``` Removed slots are freed, not moved, so the other indices stay valid. `nodes.count` is the number of live nodes and `nodes.slots` the number of slots, live plus freed. `compact()` packs live nodes into the first slots and returns the table from old index to new index, with `NO_INDEX` for freed slots. ### Change channels `update` changes channels for a list of slots, `updateAll` for every slot. Only the channels you pass change. ```ts import { packRgba } from "@vhult/graph"; graph.nodes.update(new Uint32Array([1, 2]), { colors: new Uint32Array(2).fill(packRgba(1, 1, 1)) }); graph.nodes.updateAll({ labels: null }); ``` When positions change every frame, as in a simulation, use `nodes.stream`. It returns arrays you write into, then `commit()` sends them. ```ts const stream = graph.nodes.stream({ positions: true }); function tick() { simulate(stream.positions); stream.commit(); requestAnimationFrame(tick); } tick(); ``` ### Flags Flags change how nodes and edges are drawn without touching their channels: `Flag.selected`, `Flag.focused`, `Flag.dimmed` and `Flag.hidden`. Their looks are set in the style. ```ts import { Flag } from "@vhult/graph"; graph.nodes.flag("all", Flag.dimmed, true); graph.nodes.flag(new Uint32Array([0]), Flag.dimmed, false); graph.nodes.flag(new Uint32Array([0]), Flag.focused, true); ``` ## Styling Colors, shapes, labels, icons and the look of highlighted nodes. ### Colors Per-node and per-edge colors are packed RGBA words. `packRgba` takes red, green, blue and alpha from 0 to 1. Colors in the style, like the background, are `RGBA` arrays with the same four values. ```ts import { packRgba } from "@vhult/graph"; const orange = packRgba(1, 0.4, 0.2); const glass = packRgba(1, 1, 1, 0.3); ``` ### The graph style `style.set` changes part of the look; fields you leave out keep their value. `Graph.create` takes the same shape as the first look. ```ts graph.style.set({ background: [0.04, 0.04, 0.06, 1], nodeScale: 1.2, edge: { color: [0.6, 0.7, 0.9, 0.6], width: 1 }, label: { size: 12, color: [1, 1, 1, 0.9] }, dimmed: { alpha: 0.15 }, }); ``` A background alpha below 1 makes the canvas see-through, so the page shows behind the graph. ### Edges `packEdgeStyle` makes one style word per edge from a width in CSS px and whether the edge is directed. Edges without their own width or color use `style.edge`. Two colors per edge draw a gradient from the source to the target. ```ts import { packEdgeStyle, packRgba } from "@vhult/graph"; graph.edges.set({ count: 1, indices: new Uint32Array([0, 1]), styles: new Uint32Array([packEdgeStyle({ width: 3, directed: true })]), colors: new Uint32Array([packRgba(0.3, 0.6, 1), packRgba(0.2, 0.8, 0.6)]), }); ``` ### Highlights The hovered node, the selected nodes and the focused nodes each have a `HighlightLook`: an outline around the node, and a color and width for its edges. Set `hover` to false to turn the hover highlight off. ```ts graph.style.set({ hover: { outline: { color: [1, 1, 1, 1], scale: 0.15, minWidth: 1, maxWidth: 4 } }, selected: { outline: { color: [0.3, 0.6, 1, 1] }, edgeColor: [0.3, 0.6, 1, 1], edgeWidth: 2 }, }); ``` ### Icons Icons come from SVG path data or SVG markup. `icons.define` sets the whole icon set, and an icon's id is its position in the list. Nodes pick an icon with the `icons` channel and tint it with `iconColors`. ```ts import { NO_ICON, packRgba } from "@vhult/graph"; await graph.icons.define([ { path: "M12 2 L22 22 L2 22 Z", viewBox: [0, 0, 24, 24] }, { svg: '' }, ]); graph.nodes.update(new Uint32Array([0, 1, 2]), { icons: new Uint16Array([0, 1, NO_ICON]), iconColors: new Uint32Array(3).fill(packRgba(1, 1, 1)), }); ``` `style.icon.scale` sets the icon size inside the node, from 0.01 to 1. Icons are not drawn on nodes smaller than `style.icon.minPx` CSS px. ## Camera Frame the graph, move and turn the view, and convert between screen and world. ### Fit `camera.fit()` frames every node. It can also frame a list of nodes or a world rectangle, with padding in CSS px and an animation duration in ms. ```ts graph.camera.fit(); graph.camera.fit({ nodes: new Uint32Array([4, 8, 15]), padding: 48, duration: 400 }); ``` ### Move A view is a center in world units, a zoom in CSS px per world unit and a rotation in radians. `camera.set` changes part of it, animated when you give a duration. ```ts graph.camera.set({ x: 0, y: 0, zoom: 2 }, { duration: 600, easing: "ease" }); graph.camera.rotate(Math.PI / 8, { duration: 300 }); ``` `camera.get()` returns the last drawn view, at most one frame old. The `view` event fires when the camera moves, at most once per frame. ### Limits ```ts graph.camera.limits({ minZoom: 0.05, maxZoom: 20, bounds: { minX: -1000, minY: -1000, maxX: 1000, maxY: 1000 }, }); ``` `bounds` is the world rectangle the view center stays in. ### Screen and world `toWorld` converts a canvas point in CSS px to world units, and `toScreen` does the reverse. Both take an optional `out` object to write into. ```ts const out = { x: 0, y: 0 }; canvas.addEventListener("click", (e) => { graph.camera.toWorld(e.offsetX, e.offsetY, out); console.log(out.x, out.y); }); ``` ## Interaction Pan, zoom, drag, select, listen to events and ask what is under a point. ### Modes Each interaction has a `Mode`: `"auto"` means the engine does it, `"manual"` means it only reports it in an event, and `false` turns it off. Set them in `Graph.create` or later with `input.set`. | Setting | What it does | Default | | --- | --- | --- | | `pan` | Drag on empty space moves the camera | `"auto"` | | `zoom` | Wheel and pinch zoom around the pointer | `"auto"` | | `rotate` | Two-finger twist turns the view | `false` | | `drag` | Press and move on a node moves it, with every selected node | `"auto"` | | `select` | Click and key + drag select nodes | `"auto"` | ```ts graph.input.set({ zoom: false, drag: "manual", selectShape: "lasso", selectKey: "alt" }); ``` `pick` chooses what hover and click see (nodes, edges, groups), and `pickRadius` and `edgePickRadius` add reach around them in CSS px. ### Events `graph.on` subscribes to an event and returns the function that unsubscribes. ```ts const off = graph.on("click", (hit) => { if (hit.node !== null) console.log("node", hit.node); else if (hit.edge !== null) console.log("edge", hit.edge); }); graph.on("select", (e) => console.log(e.shape, e.nodes)); graph.on("drag", (e) => console.log(e.index, e.dx, e.dy)); off(); ``` | Event | When | | --- | --- | | `click`, `doubleClick`, `contextMenu` | A click on a node, an edge or empty space, as a `Hit` | | `hover` | What is under the pointer changed | | `select` | A click or a shape selected nodes | | `dragStart`, `drag`, `dragEnd` | Nodes are dragged | | `pan`, `zoom`, `rotate` | A gesture step | | `view` | The camera moved | | `edgesRemoved` | Edges went away with a removed node | | `error` | Something failed after creation | ### Selection With `select` on `"auto"`, a click selects a node, shift + click adds or removes it, and the select key + drag draws a box or a lasso. The engine sets `Flag.selected` itself and the `select` event lists the selected nodes. With `"manual"`, nothing is flagged and the event lists the nodes under the click or inside the shape. ### Queries `query.at` resolves with what is at a canvas point, and `query.inside` with the nodes inside a box or a polygon. Both use CSS px. ```ts const hit = await graph.query.at(120, 80); const nodes = await graph.query.inside({ x: 0, y: 0, width: 200, height: 100 }); ``` ## Performance Cross-origin isolation, frame numbers and the debug overlay. ### Cross-origin isolation The engine works on any page. When the page is cross-origin isolated, it uses a `SharedArrayBuffer` for pointer input and frame numbers. Otherwise input, frame numbers and node streams go through messages between the page and the worker instead. To turn it on, serve the page with these two headers: ```text Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` > **Warning:** With these headers, every resource from another origin, such as an image, a font or an embed, must allow it with a CORS or a `Cross-Origin-Resource-Policy` header, or it is blocked. `graph.caps.sharedMemory` tells you which path is running. ### Frame numbers `graph.stats()` returns the latest frame numbers: CPU and GPU time, visible nodes and edges, GPU memory and more. Pass the same object back to avoid a new one per call. ```ts const stats = graph.stats(); function tick() { graph.stats(stats); console.log(stats.cpuMs, stats.gpuMs, stats.visibleNodes); requestAnimationFrame(tick); } tick(); ``` `gpuMs` is NaN when the GPU has no timestamp queries, see `graph.caps.timestampQuery`. It is also NaN while the debug overlay is closed and no recording or benchmark runs, because only then does the engine time the GPU. ### Debug overlay `graph.debug.open()` shows the debug overlay with live frame timings. `graph.debug.record()` records the frames until `graph.debug.stop()` and resolves with the recording. ```ts graph.debug.open(); const recording = graph.debug.record(); setTimeout(() => graph.debug.stop(), 5000); console.log((await recording).summary); ``` ### See it run - **Storybook**: Large graphs up to 10 million nodes, every style, and the benchmark. # API reference ## Start `Graph.create` turns a canvas into a graph. The `Graph` it returns is the way in to everything else, and its errors tell you when the browser or the GPU cannot run the engine. ### `Graph` (class) A graph drawn with WebGPU on a canvas. The drawing runs in a worker, so the calls you make on the main thread only send work and return. Make one with `Graph.create`, then reach the parts through `graph.nodes`, `graph.edges`, `graph.camera` and the other namespaces. ```ts export declare class Graph ``` Example: ```ts import { Graph, UnsupportedError } from "@vhult/graph"; try { const graph = await Graph.create(canvas); graph.nodes.set({ count: 2, positions: new Float32Array([0, 0, 100, 0]) }); graph.edges.set({ count: 1, indices: new Uint32Array([0, 1]) }); graph.camera.fit(); } catch (e) { if (e instanceof UnsupportedError) showFallback(e.message); else throw e; } ``` #### `camera` (readonly) ```ts readonly camera: GraphCamera; ``` Moves and reads the view: pan, zoom, rotation, fit to the nodes, limits, and conversion between screen and world units. See `GraphCamera`. Example: ```ts graph.camera.fit({ duration: 300 }); ``` #### `canvas` (readonly) ```ts readonly canvas: GraphCanvas; ``` Resizes the canvas, draws a frame on demand and takes snapshots. See `GraphCanvas`. Example: ```ts const png = await graph.canvas.snapshot(); window.open(URL.createObjectURL(png)); ``` #### `caps` (readonly) ```ts readonly caps: GraphCaps; ``` What this GPU and page support, read once when the graph starts. See `GraphCaps`. Example: ```ts console.log(`${graph.caps.adapter}, up to ${graph.caps.maxIcons} icons`); ``` #### `create` (static) ```ts static create(canvas: HTMLCanvasElement, options?: GraphOptions): Promise; ``` Hands the canvas to a render worker, starts WebGPU and resolves with the graph. It rejects with an `UnsupportedError` when the browser or the GPU cannot run the engine. The canvas size is read at this moment, so give it its CSS size first. A canvas can be handed over only once. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { style: { background: [0.05, 0.05, 0.08, 1] }, input: { selectShape: "lasso" }, }); ``` #### `debug` (readonly) ```ts readonly debug: GraphDebug; ``` Opens the debug overlay, records timings and runs benchmarks. See `GraphDebug`. Example: ```ts window.addEventListener("keydown", (e) => { if (e.key === "F2") graph.debug.toggle(); }); ``` #### `destroy` ```ts destroy(): void; ``` Stops the graph and frees the worker, the GPU device and every listener. Promises still waiting reject, and calls that send work after it throw a `GraphError` with code `destroyed`. Calling it twice is safe. Example: ```ts window.addEventListener("pagehide", () => graph.destroy()); ``` #### `edges` (readonly) ```ts readonly edges: GraphEdges; ``` Loads, changes, removes and flags the edges. Edges point at nodes by index, so load the nodes first. See `GraphEdges`. Example: ```ts graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]) }); ``` #### `icons` (readonly) ```ts readonly icons: GraphIcons; ``` Loads the icons that nodes can show. See `GraphIcons`. Example: ```ts const [star] = await graph.icons.add([{ path: "M12 2l3 7h7l-6 5 2 7-6-4-6 4 2-7-6-5h7z", viewBox: [0, 0, 24, 24] }]); graph.nodes.update(new Uint32Array([0]), { icons: new Uint16Array([star]) }); ``` #### `input` (readonly) ```ts readonly input: GraphInputApi; ``` Sets how the graph reacts to the mouse and touch: pan, zoom, rotate, drag, select and picking. See `GraphInput`. Example: ```ts graph.input.set({ rotate: "auto", drag: false }); ``` #### `nodes` (readonly) ```ts readonly nodes: GraphNodes; ``` Loads, changes, removes and flags the nodes. See `GraphNodes`. Example: ```ts graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 80, 0, 40, 60]) }); ``` #### `on` ```ts on(event: K, fn: (payload: GraphEvents[K]) => void): () => void; ``` Listens to an event, like `click`, `hover`, `select` or `error`, and returns a function that stops listening. The worker only sends the events that have a listener. With no `error` listener, errors are logged with `console.error`. See `GraphEvents`. Example: ```ts const off = graph.on("click", (hit) => { if (hit.node === null) return; console.log(`clicked node ${hit.node}`); off(); }); ``` #### `query` (readonly) ```ts readonly query: GraphQuery; ``` Asks what is at a point or inside a shape on the canvas. See `GraphQuery`. Example: ```ts const hit = await graph.query.at(120, 80); console.log(hit.node); ``` #### `stats` ```ts stats(out?: GraphStats): GraphStats; ``` Returns the latest frame numbers right away: frame times in ms, node and edge counts, what is on screen and GPU memory. Pass an object as `out` to fill it again instead of making a new one on each call. See `GraphStats`. Example: ```ts const stats = graph.stats(); setInterval(() => { graph.stats(stats); console.log(`${stats.cpuMs.toFixed(2)} ms, ${stats.visibleNodes} nodes on screen`); }, 1000); ``` #### `style` (readonly) ```ts readonly style: GraphStyleApi; ``` Sets the look of the graph: background, edges, labels, icons and highlights. See `GraphStyle`. Example: ```ts graph.style.set({ background: [1, 1, 1, 1], nodeScale: 1.5 }); ``` ### `GraphOptions` (interface) The options you pass to `Graph.create`. Every field is optional. The style and input you set here are the first ones; change them later with `graph.style` and `graph.input`. ```ts export interface GraphOptions ``` Example: ```ts import { Graph, type GraphOptions } from "@vhult/graph"; const options: GraphOptions = { pixelRatio: 1, autoResize: true, nodeReserve: 1000 }; const graph = await Graph.create(canvas, options); ``` #### `autoResize` (optional) ```ts autoResize?: boolean; ``` Follows the CSS size of the canvas with a `ResizeObserver`. The default is true. Turn it off to set the size yourself with `graph.canvas.resize`. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { autoResize: false }); graph.canvas.resize(800, 600); ``` #### `input` (optional) ```ts input?: GraphInput; ``` The first interaction settings, in the same shape as `input.set`. See `GraphInput`. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { input: { select: "manual", selectKey: null } }); ``` #### `nodeReserve` (optional) ```ts nodeReserve?: number; ``` How many empty node slots the engine keeps ready past `nodes.slots`. The default is 100. Adds that fit in them are cheap; the add that takes the last one grows the buffers, which costs more. Raise it when you add many nodes one call at a time. It must be a whole number of 0 or more. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { nodeReserve: 10_000 }); ``` #### `pixelRatio` (optional) ```ts pixelRatio?: number; ``` How many canvas pixels one CSS px takes. The default is `devicePixelRatio`. A lower value draws fewer pixels, which is faster but less sharp. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { pixelRatio: 1 }); ``` #### `style` (optional) ```ts style?: GraphStyle; ``` The first look of the graph, in the same shape as `style.set`. See `GraphStyle`. Example: ```ts import { Graph } from "@vhult/graph"; const graph = await Graph.create(canvas, { style: { background: [0, 0, 0, 0], edge: { width: 2 } } }); ``` ### `graph.caps` (interface `GraphCaps`) What this GPU and page support, in `graph.caps`. Read it to know the limits of the machine, or to turn off a feature it cannot run. ```ts export interface GraphCaps ``` Example: ```ts if (!graph.caps.timestampQuery) console.log("GPU times are not available here"); ``` #### `adapter` ```ts adapter: string; ``` A short description of the GPU adapter, such as "nvidia ampere". #### `float32Filterable` ```ts float32Filterable: boolean; ``` Whether 32-bit float textures can be filtered. #### `indirectFirstInstance` ```ts indirectFirstInstance: boolean; ``` Whether indirect draws can set the first instance. #### `maxBufferSize` ```ts maxBufferSize: number; ``` The largest GPU buffer, in bytes. #### `maxIcons` ```ts maxIcons: number; ``` The most icons the icon set can hold. `icons.define` and `icons.add` throw past it. #### `maxStorageBufferBindingSize` ```ts maxStorageBufferBindingSize: number; ``` The largest storage buffer binding, in bytes. It limits how many edges the GPU can hold. #### `maxStorageBuffersPerShaderStage` ```ts maxStorageBuffersPerShaderStage: number; ``` How many storage buffers one shader stage can use. #### `maxTextureDimension2D` ```ts maxTextureDimension2D: number; ``` The largest side of a 2D texture, in px. #### `profilerSlots` ```ts profilerSlots: string[]; ``` The names of the GPU timings, in order. They are the keys of `passMs` in `graph.stats`. #### `shaderF16` ```ts shaderF16: boolean; ``` Whether 16-bit floats are available in shaders. #### `sharedMemory` ```ts sharedMemory: boolean; ``` Whether the page can share memory with the worker. It needs `SharedArrayBuffer` and a cross-origin isolated page. Without it, pointer input and node streams go through messages. #### `subgroups` ```ts subgroups: boolean; ``` Whether shader subgroups are available. #### `timestampQuery` ```ts timestampQuery: boolean; ``` Whether the GPU can measure its own time. Without it, `gpuMs` in `graph.stats` is `NaN`. ### `GraphError` (class) The error the engine throws, rejects with and reports. Read `code` to know what went wrong; the message says it in words. Errors that happen later in the worker come through the `error` event. ```ts export declare class GraphError extends Error ``` Example: ```ts import { GraphError } from "@vhult/graph"; try { graph.nodes.update(new Uint32Array([999999]), { sizes: new Float32Array([8]) }); } catch (e) { if (e instanceof GraphError && e.code === "invalid-argument") console.warn(e.message); else throw e; } ``` #### `constructor` ```ts constructor( code: GraphErrorCode, message: string); ``` Makes an error with a code and a message. The engine makes these for you, so you rarely need it. Example: ```ts import { GraphError } from "@vhult/graph"; const error = new GraphError("invalid-argument", "positions must hold x, y pairs"); ``` #### `code` (readonly) ```ts readonly code: GraphErrorCode; ``` What went wrong, as one of `GraphErrorCode`. #### `name` (readonly) ```ts readonly name: string; ``` The class name, `"GraphError"`. ### `GraphErrorCode` (type) The code of a `GraphError`. The first five, `webgpu-unavailable`, `offscreen-canvas-unavailable`, `no-adapter`, `compat-mode` and `insufficient-limits`, mean this browser or GPU cannot run the engine, and come as an `UnsupportedError`. `limits-exceeded` means the data is too large, `device-failed` and `device-lost` mean the GPU device could not start or was lost, `detached-array` means an array was already sent, and `destroyed` means the graph is gone. `invalid-argument` is bad input and `internal` is everything else. ```ts export type GraphErrorCode = "webgpu-unavailable" | "offscreen-canvas-unavailable" | "no-adapter" | "compat-mode" | "insufficient-limits" | "limits-exceeded" | "device-failed" | "device-lost" | "detached-array" | "invalid-argument" | "destroyed" | "internal"; ``` Example: ```ts graph.on("error", (e) => { if (e.code === "device-lost") showFallback(e.message); }); ``` ### `UnsupportedError` (class) A `GraphError` that says this browser or GPU cannot run the engine. `Graph.create` throws it when WebGPU or `OffscreenCanvas` is missing, when there is no GPU adapter, when the adapter only offers compatibility mode, or when the GPU limits are too low. Catch it to show a fallback. ```ts export declare class UnsupportedError extends GraphError ``` Example: ```ts import { Graph, UnsupportedError } from "@vhult/graph"; const graph = await Graph.create(canvas).catch((e: unknown) => { if (e instanceof UnsupportedError) showFallback(`${e.code}: ${e.message}`); return null; }); ``` #### `name` (readonly) ```ts readonly name = "UnsupportedError"; ``` The class name, `"UnsupportedError"`. ## Nodes Nodes are the dots of the graph. You reach them through `graph.nodes`, and you describe them with typed arrays: one entry per node for each channel, like its position, size or color. ### `graph.nodes` (interface `GraphNodes`) Everything you do with nodes goes through `graph.nodes`: load them, add more, change them, remove them and mark them with flags. A node is known by its index, a whole number the engine gives it when you add it. Edges point at nodes by these indices. ```ts export interface GraphNodes ``` Example: ```ts import { packRgba } from "@vhult/graph"; graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 100, 0, 50, 80]), colors: new Uint32Array(3).fill(packRgba(0.3, 0.6, 1)), }); console.log(graph.nodes.count); ``` #### `add` ```ts add(data: NodeData, opts?: CopyOption): Uint32Array; ``` Adds nodes and returns their indices. Freed indices are used again first, so the returned indices are not always at the end. Keep them: you need them to connect edges or to change the nodes later. Example: ```ts const [a, b] = graph.nodes.add({ count: 2, positions: new Float32Array([200, 0, 200, 80]) }); graph.edges.add({ count: 1, indices: new Uint32Array([a, b]) }); ``` #### `clear` ```ts clear(): void; ``` Removes every node and every edge. The graph is empty after it. Example: ```ts graph.nodes.clear(); ``` #### `compact` ```ts compact(): Uint32Array; ``` Moves the live nodes into the first indices, so there are no freed slots left. It returns a table from old index to new index, with `NO_INDEX` for nodes that were removed. Edges follow by themselves; update any index you keep on your side with the table. Example: ```ts import { NO_INDEX } from "@vhult/graph"; let selected = 42; const table = graph.nodes.compact(); if (table[selected] !== NO_INDEX) selected = table[selected]; ``` #### `count` (readonly) ```ts readonly count: number; ``` How many nodes are in the graph right now. Removed nodes do not count. Example: ```ts console.log(`${graph.nodes.count} nodes`); ``` #### `flag` ```ts flag(target: Uint32Array | "all", flags: number, on: boolean): void; ``` Turns flags on or off for the listed nodes, or for all of them with `"all"`. Flags change how a node is drawn without touching its channels: selected, focused, dimmed or hidden. Combine several with `|`. Example: ```ts import { Flag } from "@vhult/graph"; graph.nodes.flag("all", Flag.dimmed, true); graph.nodes.flag(new Uint32Array([0, 1]), Flag.dimmed, false); graph.nodes.flag(new Uint32Array([0]), Flag.focused | Flag.selected, true); ``` #### `remove` ```ts remove(indices: Uint32Array | number[]): void; ``` Removes nodes by index, with every edge that touches them. The other nodes keep their indices. The edges that went away are reported in the `edgesRemoved` event. Example: ```ts graph.on("edgesRemoved", (edges) => console.log(`${edges.length} edges went away`)); graph.nodes.remove([3, 4]); ``` #### `set` ```ts set(data: NodeData, opts?: CopyOption): void; ``` Replaces every node with the ones you pass. It also removes every edge, so call it before `edges.set`. This is the fastest way to load a whole graph. Channels you leave out get their default: size 4, the engine blue, a circle, layer 0, no icon and no label. Example: ```ts graph.nodes.set({ count: 4, positions: new Float32Array([0, 0, 100, 0, 100, 100, 0, 100]), sizes: new Float32Array([12, 8, 8, 8]), }); graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 0, 2]) }); ``` #### `slots` (readonly) ```ts readonly slots: number; ``` How many node indices are in use, live nodes plus the ones freed by `remove`. Arrays you pass to `updateAll` must have one entry per slot. `compact` brings `slots` back down to `count`. Example: ```ts const sizes = new Float32Array(graph.nodes.slots).fill(6); graph.nodes.updateAll({ sizes }); ``` #### `stream` ```ts stream(channels: NodeStreamChannels): NodeStream; ``` Gives you arrays that you fill again every frame, then send with `commit`. Use it when nodes move or change color all the time, as in a simulation or an animation. Taking a new stream is needed after the number of slots changes. Example: ```ts const stream = graph.nodes.stream({ positions: true }); function frame(time: number) { for (let i = 0; i < stream.positions.length / 2; i++) { stream.positions[i * 2] = Math.cos(time / 1000 + i) * 100; stream.positions[i * 2 + 1] = Math.sin(time / 1000 + i) * 100; } stream.commit(); requestAnimationFrame(frame); } requestAnimationFrame(frame); ``` #### `update` ```ts update(indices: Uint32Array, data: NodeUpdate, opts?: CopyOption): void; ``` Changes some channels of the listed nodes. Each array has one entry per listed index, in the same order. Channels you leave out do not change. Example: ```ts import { packRgba } from "@vhult/graph"; const picked = new Uint32Array([2, 5]); graph.nodes.update(picked, { colors: new Uint32Array(2).fill(packRgba(1, 0.4, 0.2)), sizes: new Float32Array([14, 14]), }); ``` #### `updateAll` ```ts updateAll(data: NodeUpdate, opts?: CopyOption): void; ``` Changes some channels of every node at once. Each array must have one entry per slot, see `slots`. It is faster than `update` when most nodes change, for example after a layout step. Pass `labels: null` to clear every label. Example: ```ts const positions = new Float32Array(graph.nodes.slots * 2); for (let i = 0; i < graph.nodes.slots; i++) { positions[i * 2] = Math.cos(i) * 200; positions[i * 2 + 1] = Math.sin(i) * 200; } graph.nodes.updateAll({ positions }); ``` ### `NodeData` (interface) The nodes you pass to `nodes.set` and `nodes.add`. `count` says how many nodes there are, and every other field is an optional channel: a typed array with one entry per node, or two for positions. ```ts export interface NodeData ``` Example: ```ts import { NodeShape, packRgba, type NodeData } from "@vhult/graph"; const data: NodeData = { count: 2, positions: new Float32Array([0, 0, 50, 0]), sizes: new Float32Array([10, 6]), colors: new Uint32Array([packRgba(1, 0.4, 0.2), packRgba(0.3, 0.6, 1)]), shapes: new Uint8Array([NodeShape.hexagon, NodeShape.circle]), labels: ["root", "leaf"], }; graph.nodes.set(data); ``` #### `colors` (optional) ```ts colors?: Uint32Array | Uint8Array; ``` One packed color per node, made with `packRgba`. A `Uint8Array` with four bytes per node (red, green, blue, alpha) works too. Example: ```ts import { packRgba } from "@vhult/graph"; graph.nodes.set({ count: 2, colors: new Uint32Array([packRgba(1, 0, 0), packRgba(0, 0, 1, 0.5)]) }); ``` #### `count` ```ts count: number; ``` How many nodes the data holds. Every channel array must match it. #### `iconColors` (optional) ```ts iconColors?: Uint32Array | Uint8Array; ``` The tint of each node's icon, as a packed color from `packRgba`. Example: ```ts import { packRgba } from "@vhult/graph"; graph.nodes.set({ count: 1, icons: new Uint16Array([0]), iconColors: new Uint32Array([packRgba(1, 1, 1)]) }); ``` #### `icons` (optional) ```ts icons?: Uint16Array; ``` The icon drawn inside each node, as an id from the icon set, or `NO_ICON` for none. See `graph.icons` to load icons. Example: ```ts import { NO_ICON } from "@vhult/graph"; graph.nodes.set({ count: 2, icons: new Uint16Array([0, NO_ICON]) }); ``` #### `labels` (optional) ```ts labels?: readonly (string | null | undefined)[]; ``` The text shown next to each node. Use null or leave a hole for a node without a label. The engine picks which labels fit on screen, so crowded labels hide until you zoom in. Example: ```ts graph.nodes.set({ count: 3, labels: ["Paris", null, "Berlin"] }); ``` #### `positions` (optional) ```ts positions?: Float32Array; ``` Where each node sits, as x then y, in world units. World units are yours: the camera decides how many screen pixels one unit takes. Example: ```ts graph.nodes.set({ count: 2, positions: new Float32Array([0, 0, 120, -40]) }); ``` #### `shapes` (optional) ```ts shapes?: Uint8Array; ``` The shape of each node: circle, square or hexagon, from `NodeShape`. The default is a circle. Example: ```ts import { NodeShape } from "@vhult/graph"; graph.nodes.set({ count: 2, shapes: new Uint8Array([NodeShape.square, NodeShape.hexagon]) }); ``` #### `sizes` (optional) ```ts sizes?: Float32Array; ``` The diameter of each node in world units, so nodes grow and shrink as you zoom. The default is 4. Example: ```ts graph.nodes.set({ count: 2, sizes: new Float32Array([4, 16]) }); ``` #### `zIndex` (optional) ```ts zIndex?: Uint8Array; ``` The layer of each node, from 0 to 15. Nodes on a higher layer are drawn on top of the others. A value above 15 throws. Example: ```ts graph.nodes.set({ count: 2, zIndex: new Uint8Array([0, 15]) }); ``` ### `NodeUpdate` (type) The channels you pass to `nodes.update` and `nodes.updateAll`. It has the same channels as `NodeData` but no `count`, since the indices or the slots decide the length. `labels: null` in `updateAll` clears every label. ```ts export type NodeUpdate = Omit & { labels?: readonly (string | null | undefined)[] | null; }; ``` Example: ```ts import { packRgba, type NodeUpdate } from "@vhult/graph"; const change: NodeUpdate = { colors: new Uint32Array([packRgba(0.2, 0.8, 0.6)]) }; graph.nodes.update(new Uint32Array([7]), change); ``` ### `NodeShape` (type) The shapes a node can take. Use `NodeShape.circle`, `NodeShape.square` or `NodeShape.hexagon` in the `shapes` channel. `NodeShape` is also the type of one of these values. ```ts export type NodeShape = (typeof NodeShape)[keyof typeof NodeShape]; ``` Example: ```ts import { NodeShape } from "@vhult/graph"; const shapes = new Uint8Array(100).fill(NodeShape.circle); shapes[0] = NodeShape.hexagon; graph.nodes.updateAll({ shapes }); ``` ### `NodeShape` (const) Node shapes. ```ts NodeShape: { readonly circle: 0; readonly square: 1; readonly hexagon: 2; } ``` ### `NodeStream` (interface) The arrays `nodes.stream` gives you. Write the new values in place, then call `commit` to send them. Only the channels you asked for have a length; the others are empty. ```ts export interface NodeStream ``` Example: ```ts const stream = graph.nodes.stream({ colors: true }); stream.colors.fill(0xff00ff00); stream.commit(); ``` #### `colors` (readonly) ```ts readonly colors: Uint32Array; ``` One packed color for every node slot. #### `commit` ```ts commit(): void; ``` Sends what you wrote to the engine. Call it once per frame after writing. #### `positions` (readonly) ```ts readonly positions: Float32Array; ``` x then y for every node slot, in world units. #### `zIndex` (readonly) ```ts readonly zIndex: Uint8Array; ``` The layer for every node slot. Values above 15 are sent as 15. ### `NodeStreamChannels` (interface) Which channels a node stream carries. Ask only for what changes every frame: each channel adds data to send. ```ts export interface NodeStreamChannels ``` Example: ```ts const stream = graph.nodes.stream({ positions: true, colors: true }); ``` #### `colors` (optional) ```ts colors?: boolean; ``` Streams node colors. #### `positions` (optional) ```ts positions?: boolean; ``` Streams node positions. #### `zIndex` (optional) ```ts zIndex?: boolean; ``` Streams node layers. ## Edges Edges are the lines between nodes. You reach them through `graph.edges`, and you describe them with typed arrays: one entry per edge for each channel, like its two ends, its style or its colors. ### `graph.edges` (interface `GraphEdges`) Everything you do with edges goes through `graph.edges`: load them, add more, change them, remove them and mark them with flags. An edge is known by its index, a whole number the engine gives it when you add it. Each edge joins two nodes by their node indices. ```ts export interface GraphEdges ``` Example: ```ts import { packRgba } from "@vhult/graph"; graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 100, 0, 50, 80]) }); graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]), colors: new Uint32Array(4).fill(packRgba(0.6, 0.6, 0.7)), }); console.log(graph.edges.count); ``` #### `add` ```ts add(data: EdgeData, opts?: CopyOption): Uint32Array; ``` Adds edges and returns their indices. Freed indices are used again first, so the returned indices are not always at the end. Both ends of each edge must be live nodes, or it throws. Example: ```ts const added = graph.edges.add({ count: 2, indices: new Uint32Array([0, 3, 3, 4]) }); console.log(`first new edge: ${added[0]}`); ``` #### `clear` ```ts clear(): void; ``` Removes every edge. The nodes stay. Example: ```ts graph.edges.clear(); ``` #### `compact` ```ts compact(): Uint32Array; ``` Moves the live edges into the first indices, so there are no freed slots left. It returns a table from old index to new index, with `NO_INDEX` for edges that were removed. Update any edge index you keep on your side with the table. Example: ```ts import { NO_INDEX } from "@vhult/graph"; let selected = 12; const table = graph.edges.compact(); if (table[selected] !== NO_INDEX) selected = table[selected]; ``` #### `count` (readonly) ```ts readonly count: number; ``` How many edges are in the graph right now. Removed edges do not count. Example: ```ts console.log(`${graph.edges.count} edges`); ``` #### `flag` ```ts flag(target: Uint32Array | "all", flags: number, on: boolean): void; ``` Turns flags on or off for the listed edges, or for all of them with `"all"`. Flags change how an edge is drawn without touching its channels: selected, focused, dimmed or hidden. Combine several with `|`. An edge is also hidden when one of its nodes is hidden. Example: ```ts import { Flag } from "@vhult/graph"; graph.edges.flag("all", Flag.dimmed, true); graph.edges.flag(new Uint32Array([3, 7]), Flag.dimmed, false); graph.edges.flag(new Uint32Array([3]), Flag.selected, true); ``` #### `remove` ```ts remove(indices: Uint32Array | number[]): void; ``` Removes edges by index. The other edges keep their indices, and the nodes stay where they are. Example: ```ts graph.edges.remove([0, 2]); ``` #### `set` ```ts set(data: EdgeData, opts?: CopyOption): void; ``` Replaces every edge with the ones you pass. This is the fastest way to load all the edges at once. Call it after `nodes.set`, since `nodes.set` removes every edge. Channels you leave out get their default: the width and color from `style.edge`, no arrow and no label. Example: ```ts graph.nodes.set({ count: 3, positions: new Float32Array([0, 0, 100, 0, 50, 80]) }); graph.edges.set({ count: 3, indices: new Uint32Array([0, 1, 1, 2, 2, 0]) }); ``` #### `slots` (readonly) ```ts readonly slots: number; ``` How many edge indices are in use, live edges plus the ones freed by `remove`. Arrays you pass to `updateAll` must have one entry per slot, or two for `indices` and `colors`. `compact` brings `slots` back down to `count`. Example: ```ts import { packEdgeStyle } from "@vhult/graph"; const styles = new Uint32Array(graph.edges.slots).fill(packEdgeStyle({ width: 2 })); graph.edges.updateAll({ styles }); ``` #### `update` ```ts update(indices: Uint32Array, data: EdgeUpdate, opts?: CopyOption): void; ``` Changes some channels of the listed edges. Each array has one entry per listed index, in the same order, or two for `indices` and `colors`. Channels you leave out do not change. Pass new `indices` to connect an edge to other nodes. Example: ```ts import { packEdgeStyle, packRgba } from "@vhult/graph"; const picked = new Uint32Array([4, 9]); graph.edges.update(picked, { styles: new Uint32Array(2).fill(packEdgeStyle({ width: 3, directed: true })), colors: new Uint32Array(4).fill(packRgba(1, 0.6, 0.2)), }); ``` #### `updateAll` ```ts updateAll(data: EdgeUpdate, opts?: CopyOption): void; ``` Changes some channels of every edge at once. Each array must have one entry per slot, see `slots`, or two for `indices` and `colors`. It is faster than `update` when most edges change. Pass `labels: null` to clear every label. Example: ```ts import { packRgba } from "@vhult/graph"; const colors = new Uint32Array(graph.edges.slots * 2); for (let i = 0; i < graph.edges.slots; i++) { colors[i * 2] = packRgba(1, 0.3, 0.3); colors[i * 2 + 1] = packRgba(0.3, 0.6, 1); } graph.edges.updateAll({ colors }); ``` ### `EdgeData` (interface) The edges you pass to `edges.set` and `edges.add`. `count` says how many edges there are, and `indices` says which nodes they join. The other fields are optional channels: a typed array with one entry per edge, or two for colors. ```ts export interface EdgeData ``` Example: ```ts import { packEdgeStyle, packRgba, type EdgeData } from "@vhult/graph"; const data: EdgeData = { count: 2, indices: new Uint32Array([0, 1, 1, 2]), styles: new Uint32Array([packEdgeStyle({ width: 2 }), packEdgeStyle({ directed: true })]), colors: new Uint32Array([packRgba(1, 0, 0), packRgba(0, 0, 1), 0, 0]), labels: ["knows", null], }; graph.edges.set(data); ``` #### `colors` (optional) ```ts colors?: Uint32Array; ``` Two packed colors per edge, made with `packRgba`: one at the source, then one at the target. The edge fades from the first to the second along its length. A 0 uses the color from `style.edge`. Example: ```ts import { packRgba } from "@vhult/graph"; graph.edges.set({ count: 1, indices: new Uint32Array([0, 1]), colors: new Uint32Array([packRgba(1, 0.2, 0.2), packRgba(0.2, 0.4, 1)]), }); ``` #### `count` ```ts count: number; ``` How many edges the data holds. Every channel array must match it, or the call throws. #### `indices` ```ts indices: Uint32Array; ``` The two ends of each edge, as a source node index then a target node index. The array holds two entries per edge. For a directed edge, the arrow points at the target. Example: ```ts graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 0, 2]) }); ``` #### `labels` (optional) ```ts labels?: readonly (string | null | undefined)[]; ``` The text shown on each edge, at its middle. Use null or leave a hole for an edge without a label. A label shows only when the edge is long enough on screen to fit it, so zoom in to see more. Example: ```ts graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]), labels: ["follows", null] }); ``` #### `styles` (optional) ```ts styles?: Uint32Array; ``` One style word per edge, made with `packEdgeStyle`. It holds the width in CSS px and whether the edge is directed. Edges without a style use the width from `style.edge` and have no arrow. Example: ```ts import { packEdgeStyle } from "@vhult/graph"; graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]), styles: new Uint32Array([packEdgeStyle({ width: 4 }), packEdgeStyle({ width: 1, directed: true })]), }); ``` ### `EdgeUpdate` (type) The channels you pass to `edges.update` and `edges.updateAll`. It has the same channels as `EdgeData`, all optional, but no `count`, since the indices or the slots decide the length. `labels: null` in `updateAll` clears every label. ```ts export type EdgeUpdate = Partial> & { labels?: readonly (string | null | undefined)[] | null; }; ``` Example: ```ts import { packRgba, type EdgeUpdate } from "@vhult/graph"; const change: EdgeUpdate = { colors: new Uint32Array([packRgba(1, 1, 1), packRgba(1, 1, 1)]) }; graph.edges.update(new Uint32Array([5]), change); ``` ## Shared by nodes and edges The pieces `graph.nodes` and `graph.edges` have in common: the flags that change how an item is drawn, the option to copy the arrays you send, and the marker for a removed index. ### `CopyOption` (interface) The last argument of `set`, `add`, `update` and `updateAll` on `graph.nodes` and `graph.edges`. By default the engine does not copy your arrays: it moves them to the worker, and they are empty after the call. Pass `{ copy: true }` when you want to keep using them. ```ts export interface CopyOption ``` Example: ```ts import type { CopyOption } from "@vhult/graph"; const keep: CopyOption = { copy: true }; const sizes = new Float32Array(graph.nodes.slots).fill(6); graph.nodes.updateAll({ sizes }, keep); console.log(sizes.length); ``` #### `copy` (optional) ```ts copy?: boolean; ``` Copies the arrays before sending them, so they stay usable after the call. The default is false, which moves them with no copy. Moving empties the whole buffer behind an array, with every other view on it. Sending an array that was already moved throws a `detached-array` error. Example: ```ts const positions = new Float32Array([0, 0, 50, 50]); graph.nodes.set({ count: 2, positions }, { copy: true }); positions[0] = 10; graph.nodes.updateAll({ positions }, { copy: true }); ``` ### `Flag` (const) The flags for `nodes.flag` and `edges.flag`, which you combine with `|`: `Flag.selected`, `Flag.focused`, `Flag.dimmed` and `Flag.hidden`. Selected and focused items take the looks set in `GraphStyle`, dimmed ones fade to its `dimmed.alpha`, and hidden ones are not drawn. A hidden node also hides its edges and is left out of `query.inside`. With `select` on `"auto"`, the engine sets `Flag.selected` on the nodes you click or select. ```ts Flag: { readonly selected: 2; readonly dimmed: 4; readonly hidden: 8; readonly focused: 64; } ``` Example: ```ts import { Flag } from "@vhult/graph"; graph.nodes.flag(new Uint32Array([4, 5]), Flag.hidden, true); graph.edges.flag("all", Flag.dimmed, true); graph.edges.flag(new Uint32Array([0]), Flag.focused, true); ``` ### `NO_INDEX` (const) The value `0xffffffff`. In the table that `nodes.compact` and `edges.compact` return, it marks an old index whose node or edge was removed, so it has no new index. ```ts NO_INDEX = 4294967295 ``` Example: ```ts import { NO_INDEX } from "@vhult/graph"; const table = graph.edges.compact(); const kept = [3, 8, 12].filter((i) => table[i] !== NO_INDEX).map((i) => table[i]); ``` ## Style The style is the look of the whole graph: background, default edge color and width, labels, icons and the highlights for hover, selection and focus. You change it through `graph.style`, and you pack per-node and per-edge colors with `packRgba`. ### `graph.style` (interface `GraphStyleApi`) Everything about the global look goes through `graph.style`. You pass only the fields you want to change, and the others keep their value. Per-node and per-edge values, like a node color, live in the data channels instead. ```ts export interface GraphStyleApi ``` Example: ```ts graph.style.set({ background: [1, 1, 1, 1], edge: { color: [0.5, 0.5, 0.55, 0.5] }, label: { color: [0.1, 0.1, 0.1, 1] }, }); ``` #### `set` ```ts set(style: GraphStyle): void; ``` Changes part of the look. Fields you leave out keep their value, also inside nested fields like `edge` or `hover`. `Graph.create` takes the same shape in its `style` option for the first look. Example: ```ts graph.style.set({ nodeScale: 1.5 }); graph.style.set({ label: { size: 14 } }); ``` ### `GraphStyle` (interface) The look you pass to `style.set` or to `Graph.create`. Every field is optional. Colors are `RGBA` tuples. Widths and text sizes are in CSS px, so they stay the same on screen when you zoom. ```ts export interface GraphStyle ``` Example: ```ts import { Graph, type GraphStyle } from "@vhult/graph"; const style: GraphStyle = { background: [0.02, 0.02, 0.04, 1], edge: { color: [0.4, 0.45, 0.5, 0.4], width: 1 }, hover: { outline: { color: [1, 0.8, 0.2, 1] } }, }; const g = await Graph.create(canvas, { style }); ``` #### `background` (optional) ```ts background?: RGBA; ``` The color the canvas is cleared to before each frame. An alpha below 1 lets the page show through the canvas. The default is a near black. Example: ```ts graph.style.set({ background: [0, 0, 0, 0] }); ``` #### `dimmed` (optional) ```ts dimmed?: { alpha?: number; }; ``` The opacity of nodes and edges that have `Flag.dimmed`, from 0 to 1 (default 0.25). An edge also fades when one of its nodes is dimmed. Example: ```ts graph.style.set({ dimmed: { alpha: 0.1 } }); ``` #### `edge` (optional) ```ts edge?: { color?: RGBA; width?: number; }; ``` The color and the width in CSS px of edges that have none of their own. An edge uses this width when it has no style or a style width of 0, and this color when it has no colors or a color of 0. The defaults are a dark gray at alpha 0.4 and 1 px. Example: ```ts graph.style.set({ edge: { color: [0.6, 0.6, 0.7, 0.3], width: 1.5 } }); ``` #### `focused` (optional) ```ts focused?: HighlightLook; ``` The look of nodes and edges that have `Flag.focused`, as a `HighlightLook`. Example: ```ts graph.style.set({ focused: { outline: { color: [1, 0.4, 0.2, 1], minWidth: 4 } } }); ``` #### `hover` (optional) ```ts hover?: HighlightLook | false; ``` The look of the node or edge under the pointer, as a `HighlightLook`. Pass `false` to turn the hover highlight off. The default is a white outline and white edges. Example: ```ts graph.style.set({ hover: { outline: { color: [1, 0.8, 0.2, 1] }, edgeColor: [1, 0.8, 0.2, 1] } }); graph.style.set({ hover: false }); ``` #### `icon` (optional) ```ts icon?: { scale?: number; minPx?: number; }; ``` `scale` is the icon size as a fraction of the node, from 0.01 to 1 (default 0.6). `minPx` is the node size in CSS px below which icons are not drawn (default 6). Example: ```ts graph.style.set({ icon: { scale: 0.7, minPx: 10 } }); ``` #### `label` (optional) ```ts label?: { size?: number; font?: string; color?: RGBA; padding?: number; }; ``` The look of every label: `size` in CSS px (default 12), `font` as a CSS font family (default the system font), `color`, and `padding` in CSS px (default 2) kept around each label. Example: ```ts graph.style.set({ label: { size: 14, font: "Inter, sans-serif", color: [1, 1, 1, 1], padding: 4 } }); ``` #### `nodeScale` (optional) ```ts nodeScale?: number; ``` Multiplies the size of every node. The default is 1. Use it to grow or shrink all nodes without sending new `sizes`. Example: ```ts graph.style.set({ nodeScale: 2 }); ``` #### `selected` (optional) ```ts selected?: HighlightLook; ``` The look of nodes and edges that have `Flag.selected`, as a `HighlightLook`. Example: ```ts graph.style.set({ selected: { outline: { color: [0.3, 0.6, 1, 1] }, edgeColor: [0.3, 0.6, 1, 1] } }); ``` #### `selection` (optional) ```ts selection?: { fill?: RGBA; stroke?: RGBA; }; ``` The fill and stroke colors of the box or lasso while the user draws it. The defaults are a light blue fill and a blue stroke. Example: ```ts graph.style.set({ selection: { fill: [1, 1, 1, 0.1], stroke: [1, 1, 1, 0.8] } }); ``` ### `HighlightLook` (interface) How a highlighted node and its edges look, used by `hover`, `selected` and `focused` in `GraphStyle`. Fields you leave out keep their value. ```ts export interface HighlightLook ``` Example: ```ts import type { HighlightLook } from "@vhult/graph"; const look: HighlightLook = { outline: { color: [0.3, 0.6, 1, 1], scale: 0.1 }, edgeColor: [0.3, 0.6, 1, 1], edgeWidth: 3, }; graph.style.set({ selected: look }); ``` #### `edgeColor` (optional) ```ts edgeColor?: RGBA; ``` The color of the highlighted edges. The default is white. Example: ```ts graph.style.set({ selected: { edgeColor: [1, 0.6, 0.2, 1] } }); ``` #### `edgeWidth` (optional) ```ts edgeWidth?: number; ``` How many times wider the highlighted edges are than their normal width. The default is 2. Example: ```ts graph.style.set({ focused: { edgeWidth: 3 } }); ``` #### `outline` (optional) ```ts outline?: OutlineLook; ``` The ring drawn around the highlighted node, as an `OutlineLook`. Example: ```ts graph.style.set({ hover: { outline: { color: [1, 1, 1, 1], maxWidth: 6 } } }); ``` ### `OutlineLook` (interface) The ring drawn around a highlighted node. Its width follows the node size on screen, and it is kept between `minWidth` and `maxWidth`. ```ts export interface OutlineLook ``` Example: ```ts import type { OutlineLook } from "@vhult/graph"; const outline: OutlineLook = { color: [1, 0.8, 0.2, 1], scale: 0.1, minWidth: 2, maxWidth: 8 }; graph.style.set({ hover: { outline } }); ``` #### `color` (optional) ```ts color?: RGBA; ``` The ring color. The default is white. Example: ```ts graph.style.set({ selected: { outline: { color: [0.3, 0.6, 1, 1] } } }); ``` #### `maxWidth` (optional) ```ts maxWidth?: number; ``` The largest ring width, in CSS px. The default is 12. Example: ```ts graph.style.set({ hover: { outline: { maxWidth: 8 } } }); ``` #### `minWidth` (optional) ```ts minWidth?: number; ``` The smallest ring width, in CSS px. The default is 3. Example: ```ts graph.style.set({ hover: { outline: { minWidth: 2 } } }); ``` #### `scale` (optional) ```ts scale?: number; ``` The ring width as a fraction of the node radius on screen. The default is 0.08. Example: ```ts graph.style.set({ hover: { outline: { scale: 0.15 } } }); ``` ### `RGBA` (type) A color as red, green, blue and alpha, each from 0 to 1. The style uses it for every color. All four values are needed, so write `[1, 0, 0, 1]` for opaque red. For node and edge channels, pack a color into one number with `packRgba` instead. ```ts export type RGBA = readonly [r: number, g: number, b: number, a: number]; ``` Example: ```ts import type { RGBA } from "@vhult/graph"; const accent: RGBA = [0.3, 0.6, 1, 1]; graph.style.set({ selected: { edgeColor: accent, outline: { color: accent } } }); ``` ### `packRgba` (function) Packs red, green, blue and alpha, each from 0 to 1, into one number for the `colors` and `iconColors` channels. Alpha is optional and defaults to 1. Values outside 0 to 1 are clamped. ```ts export declare function packRgba(r: number, g: number, b: number, a?: number): number; ``` Example: ```ts import { packRgba } from "@vhult/graph"; const colors = new Uint32Array(graph.nodes.slots).fill(packRgba(0.3, 0.6, 1)); colors[0] = packRgba(1, 0.4, 0.2, 0.8); graph.nodes.updateAll({ colors }); ``` ### `packEdgeStyle` (function) Packs an edge width and a direction into one number for the edge `styles` channel. `width` is in CSS px, in steps of 1/8 px, up to 31.875 px. A width of 0 or left out uses `style.edge.width`. `directed: true` draws an arrow at the target end. ```ts export declare function packEdgeStyle(style: { width?: number; directed?: boolean; }): number; ``` Example: ```ts import { packEdgeStyle } from "@vhult/graph"; graph.edges.set({ count: 2, indices: new Uint32Array([0, 1, 1, 2]), styles: new Uint32Array([packEdgeStyle({ width: 2, directed: true }), packEdgeStyle({})]), }); ``` ## Icons Icons are small shapes drawn inside nodes. You load them once through `graph.icons`, from SVG path data or SVG markup, and each node picks one by its id. ### `graph.icons` (interface `GraphIcons`) Everything you do with icons goes through `graph.icons`: load the set, add more, swap one or remove some. An icon is known by its id, a whole number you put in the `icons` channel of `NodeData`. The GPU holds at most `graph.caps.maxIcons` icons. By default an icon takes 0.6 of the node and hides on nodes under 6 CSS px; the `icon` option of `graph.style` changes both. ```ts export interface GraphIcons ``` Example: ```ts import { packRgba } from "@vhult/graph"; const [home, user] = await graph.icons.add([ { path: "M12 3 2 12h3v8h6v-6h2v6h6v-8h3z" }, { svg: '' }, ]); graph.nodes.update(new Uint32Array([0, 1]), { icons: new Uint16Array([home, user]), iconColors: new Uint32Array(2).fill(packRgba(1, 1, 1)), }); ``` #### `add` ```ts add(sources: readonly IconSource[]): Promise; ``` Adds icons to the set and resolves with their ids. Ids freed by `remove` are used again first, so keep the ids it returns. It throws when the set would go over `graph.caps.maxIcons`. Example: ```ts const [star] = await graph.icons.add([{ path: "M12 2l3 7h7l-5.5 4.5L18.5 21 12 16.5 5.5 21l2-7.5L2 9h7z" }]); graph.nodes.update(new Uint32Array([4]), { icons: new Uint16Array([star]) }); ``` #### `define` ```ts define(sources: readonly IconSource[]): Promise; ``` Replaces the whole icon set with the list you pass. An icon's id is its position in the list, so the first one is 0. The promise resolves once the icons are ready to draw. A bad icon is drawn blank and reported by the `error` event; the others still load. Example: ```ts await graph.icons.define([ { path: "M12 2 22 22H2z" }, { path: "M4 4h16v16H4z" }, ]); graph.nodes.updateAll({ icons: new Uint16Array(graph.nodes.slots).fill(0) }); ``` #### `remove` ```ts remove(ids: readonly number[] | Uint16Array): void; ``` Removes icons by id. Nodes that used them show no icon, as if they had `NO_ICON`. The freed ids can come back from a later `add`. Example: ```ts graph.icons.remove([2, 3]); ``` #### `replace` ```ts replace(id: number, source: IconSource): Promise; ``` Swaps the shape behind an icon id. Every node that uses the id shows the new shape, with no change to the nodes. It throws when the id is not in use. A bad shape is drawn blank and reported by the `error` event. Example: ```ts await graph.icons.replace(0, { svg: '' }); ``` ### `IconSource` (type) One icon shape, as you pass it to `icons.define`, `icons.add` or `icons.replace`. It is either an `IconPath`, with SVG path data, or an `IconSvg`, with SVG markup. Only the shape counts: the icon takes its color from the node's `iconColors` channel. ```ts export type IconSource = IconPath | IconSvg; ``` Example: ```ts import type { IconSource } from "@vhult/graph"; const sources: IconSource[] = [ { path: "M12 2 22 22H2z" }, { svg: '' }, ]; const ids = await graph.icons.add(sources); ``` ### `IconPath` (interface) An icon made from SVG path data, the text you find in the `d` attribute of a ``. Use it when you have the path strings of an icon set. The shape is scaled to fit the node and kept centered. ```ts export interface IconPath ``` Example: ```ts import type { IconPath } from "@vhult/graph"; const ring: IconPath = { path: ["M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20z", "M12 7a5 5 0 1 0 0 10a5 5 0 1 0 0-10z"], fillRule: "evenodd", }; await graph.icons.add([ring]); ``` #### `fillRule` (optional) ```ts fillRule?: "nonzero" | "evenodd"; ``` How overlapping parts are filled, like the SVG `fill-rule` attribute. With `"evenodd"`, a shape inside another one cuts a hole. The default is `"nonzero"`. Example: ```ts await graph.icons.add([{ path: ["M2 2h20v20H2z", "M8 8h8v8H8z"], fillRule: "evenodd" }]); ``` #### `path` ```ts path: string | readonly string[]; ``` The SVG path data, as one string or a list of strings. A list is drawn as one shape, with one fill rule. Every path command works, arcs included. Example: ```ts await graph.icons.add([{ path: ["M3 3h8v8H3z", "M13 13h8v8h-8z"] }]); ``` #### `viewBox` (optional) ```ts viewBox?: readonly [x: number, y: number, width: number, height: number]; ``` The area the path is drawn in, as x, y, width and height, like the SVG `viewBox` attribute. The default is `[0, 0, 24, 24]`, the size most icon sets use. Width and height must be above 0. Example: ```ts await graph.icons.add([{ path: "M50 5 95 95H5z", viewBox: [0, 0, 100, 100] }]); ``` ### `IconSvg` (interface) An icon made from SVG markup. Use it when you have whole `.svg` files. Only filled shapes are drawn: strokes, colors, text and images are not. An SVG where everything has `fill="none"`, as in many outline icon sets, has nothing to draw and fails. ```ts export interface IconSvg ``` Example: ```ts import type { IconSvg } from "@vhult/graph"; const pin: IconSvg = { svg: '', }; await graph.icons.add([pin]); ``` #### `svg` ```ts svg: string; ``` The SVG markup as a string. The parser reads `path`, `circle`, `ellipse`, `rect`, `polygon` and `polyline`, inside `g` and `a` groups, with their `transform`, `fill-rule`, `display` and `visibility`. It skips `defs`, `symbol`, `clipPath`, `mask`, `style` and gradients. The size comes from the `viewBox` on the root `svg`, then from its `width` and `height`, then from the shapes themselves. Example: ```ts const svg = await (await fetch("/icons/cloud.svg")).text(); const [cloud] = await graph.icons.add([{ svg }]); ``` ### `NO_ICON` (const) The icon id that means "no icon". It is the default for every node. Put it in the `icons` channel to take the icon off a node. ```ts NO_ICON: number ``` Example: ```ts import { NO_ICON } from "@vhult/graph"; graph.nodes.update(new Uint32Array([0, 1]), { icons: new Uint16Array([NO_ICON, NO_ICON]) }); ``` ## Camera The camera decides which part of the world is on the canvas. You move it through `graph.camera`: frame some nodes, set a view, turn it, limit how far it can go and convert points between the canvas and the world. ### `graph.camera` (interface `GraphCamera`) Everything you do with the camera goes through `graph.camera`. The view is a center in world units, a zoom in CSS px per world unit and a rotation in radians. World y points down, as on the screen. A new `set`, `fit` or `rotate`, or a move by the user, stops a camera animation that is running. ```ts export interface GraphCamera ``` Example: ```ts graph.camera.fit({ duration: 400 }); graph.on("view", (v) => console.log(`zoom ${v.zoom.toFixed(2)}`)); ``` #### `fit` ```ts fit(opts?: CameraFitOptions): void; ``` Moves and zooms the camera so every node is on the canvas. Pass `nodes` to frame some of them, or `bounds` to frame a world rectangle, not both. The rotation does not change. Node frames leave room for the largest node size, so nodes at the edge are not cut off. Example: ```ts graph.camera.fit({ nodes: new Uint32Array([3, 8, 13]), padding: 48, duration: 500 }); ``` #### `get` ```ts get(): CameraView; ``` Returns the last drawn view. It can be one frame old, so right after `set` it may still show the old view. To follow every move, listen to `graph.on("view")`. Example: ```ts const { x, y, zoom } = graph.camera.get(); graph.camera.set({ x, y, zoom: zoom * 2 }, { duration: 300 }); ``` #### `limits` ```ts limits(l: CameraLimits): void; ``` Sets the zoom range and the world rectangle the view center must stay in. Each call replaces the last one: a field you leave out goes back to no limit. The limits apply to the view at once, and to every later move by code or by the user. Example: ```ts graph.camera.limits({ minZoom: 0.05, maxZoom: 40, bounds: { minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 } }); graph.camera.limits({}); ``` #### `rotate` ```ts rotate(angle: number, opts?: CameraRotateOptions): void; ``` Turns the view by an angle in radians, added to the current rotation. It turns around a canvas point, by default the canvas center, and that point stays in place. Pass a `duration` to animate it. Example: ```ts graph.camera.rotate(Math.PI / 4, { duration: 300 }); graph.camera.rotate(-0.1, { x: 120, y: 80 }); ``` #### `set` ```ts set(view: Partial, anim?: CameraAnimOptions): void; ``` Moves the camera to a view. Fields you leave out keep their value. Without a second argument the move is at once; with a duration it is animated, and a rotation takes the shortest way round. Example: ```ts graph.camera.set({ x: 0, y: 0, zoom: 2 }); graph.camera.set({ rotation: Math.PI / 2 }, { duration: 600, easing: "linear" }); ``` #### `toScreen` ```ts toScreen(x: number, y: number, out?: { x: number; y: number; }): { x: number; y: number; }; ``` Converts a world point to canvas CSS px, from the top left corner of the canvas. Use it to place your own HTML over a node. Like `toWorld`, it uses the last drawn view and takes an `out` object. Example: ```ts const tip = document.getElementById("tip")!; graph.on("view", () => { const p = graph.camera.toScreen(100, 40); tip.style.transform = `translate(${p.x}px, ${p.y}px)`; }); ``` #### `toWorld` ```ts toWorld(x: number, y: number, out?: { x: number; y: number; }): { x: number; y: number; }; ``` Converts a canvas point in CSS px, from the top left corner of the canvas, to world units. It uses the last drawn view. Pass an `out` object to reuse it and avoid a new object on each call. Example: ```ts const point = { x: 0, y: 0 }; canvas.addEventListener("pointermove", (e) => { graph.camera.toWorld(e.offsetX, e.offsetY, point); console.log(point.x, point.y); }); ``` ### `CameraView` (interface) A camera view: where the camera looks, how close and at what angle. `camera.get` and the `view` event give you one, and `camera.set` takes one, in part or whole. ```ts export interface CameraView ``` Example: ```ts import type { CameraView } from "@vhult/graph"; const home: CameraView = { x: 0, y: 0, zoom: 1, rotation: 0 }; graph.camera.set(home, { duration: 500 }); ``` #### `rotation` ```ts rotation: number; ``` The angle of the view, in radians. Example: ```ts graph.camera.set({ rotation: Math.PI }); ``` #### `x` ```ts x: number; ``` The x of the point at the center of the canvas, in world units. Example: ```ts graph.camera.set({ x: 250 }); ``` #### `y` ```ts y: number; ``` The y of the point at the center of the canvas, in world units. World y points down. Example: ```ts graph.camera.set({ y: -120 }); ``` #### `zoom` ```ts zoom: number; ``` How many CSS px one world unit takes on the canvas. 2 means everything looks twice as big as at 1. It stays inside the range set by `camera.limits`. Example: ```ts graph.camera.set({ zoom: 0.5 }, { duration: 300 }); ``` ### `CameraFitOptions` (interface) The options of `camera.fit`. With no options it frames every node at once, with 24 CSS px of space around them. ```ts export interface CameraFitOptions ``` Example: ```ts import type { CameraFitOptions } from "@vhult/graph"; const opts: CameraFitOptions = { padding: 40, duration: 600 }; graph.camera.fit(opts); ``` #### `bounds` (optional) ```ts bounds?: WorldBounds; ``` A world rectangle to frame instead of nodes. It cannot be used with `nodes`. Example: ```ts graph.camera.fit({ bounds: { minX: 0, minY: 0, maxX: 800, maxY: 600 } }); ``` #### `duration` (optional) ```ts duration?: number; ``` How long the move takes, in ms. The default is 0, a move at once. An animated fit uses the `"ease"` easing. Example: ```ts graph.camera.fit({ duration: 800 }); ``` #### `nodes` (optional) ```ts nodes?: Uint32Array; ``` The nodes to frame, by index. The default is every node. An empty list does nothing. It cannot be used with `bounds`. Example: ```ts graph.camera.fit({ nodes: new Uint32Array([0, 1, 2]) }); ``` #### `padding` (optional) ```ts padding?: number; ``` The space left around the framed area, in CSS px. The default is 24. Example: ```ts graph.camera.fit({ padding: 64 }); ``` ### `CameraAnimOptions` (interface) How `camera.set` animates. Give a duration to animate the move; leave the whole object out to move at once. ```ts export interface CameraAnimOptions ``` Example: ```ts import type { CameraAnimOptions } from "@vhult/graph"; const slow: CameraAnimOptions = { duration: 1200, easing: "linear" }; graph.camera.set({ x: 500, y: 200 }, slow); ``` #### `duration` ```ts duration: number; ``` How long the move takes, in ms. It must be 0 or more. Example: ```ts graph.camera.set({ zoom: 4 }, { duration: 400 }); ``` #### `easing` (optional) ```ts easing?: CameraEasing; ``` How the speed changes during the move. The default is `"ease"`. Example: ```ts graph.camera.set({ x: 0 }, { duration: 400, easing: "linear" }); ``` ### `CameraEasing` (type) The speed curve of a camera animation. `"linear"` keeps the same speed from start to end. `"ease"` starts slow, speeds up and slows down at the end. ```ts export type CameraEasing = "linear" | "ease"; ``` Example: ```ts import type { CameraEasing } from "@vhult/graph"; const easing: CameraEasing = "ease"; graph.camera.set({ zoom: 3 }, { duration: 500, easing }); ``` ### `CameraRotateOptions` (interface) The options of `camera.rotate`: the point to turn around and how long the turn takes. ```ts export interface CameraRotateOptions ``` Example: ```ts import type { CameraRotateOptions } from "@vhult/graph"; const opts: CameraRotateOptions = { x: 200, y: 150, duration: 250 }; graph.camera.rotate(Math.PI / 2, opts); ``` #### `duration` (optional) ```ts duration?: number; ``` How long the turn takes, in ms. The default is 0, a turn at once. Example: ```ts graph.camera.rotate(Math.PI, { duration: 700 }); ``` #### `x` (optional) ```ts x?: number; ``` The x of the point to turn around, in canvas CSS px. The default is the canvas center. Example: ```ts graph.camera.rotate(0.5, { x: 100, y: 100 }); ``` #### `y` (optional) ```ts y?: number; ``` The y of the point to turn around, in canvas CSS px. The default is the canvas center. Example: ```ts graph.camera.rotate(0.5, { x: 300, y: 40 }); ``` ### `CameraLimits` (interface) The limits you pass to `camera.limits`. Use them to stop the user from zooming too far or from getting lost away from the graph. `minZoom` must not be above `maxZoom`, or the call throws. ```ts export interface CameraLimits ``` Example: ```ts import type { CameraLimits } from "@vhult/graph"; const limits: CameraLimits = { minZoom: 0.1, maxZoom: 20 }; graph.camera.limits(limits); ``` #### `bounds` (optional) ```ts bounds?: WorldBounds; ``` A world rectangle the view center stays in. The edges of the view can still show what is outside it. The default is no limit. Example: ```ts graph.camera.limits({ bounds: { minX: -1000, minY: -1000, maxX: 1000, maxY: 1000 } }); ``` #### `maxZoom` (optional) ```ts maxZoom?: number; ``` The largest zoom, in CSS px per world unit. It must be above 0. The default is no limit. Example: ```ts graph.camera.limits({ maxZoom: 50 }); ``` #### `minZoom` (optional) ```ts minZoom?: number; ``` The smallest zoom, in CSS px per world unit. The default is 0, no limit. Example: ```ts graph.camera.limits({ minZoom: 0.2 }); ``` ### `WorldBounds` (interface) A rectangle in world units, used by `camera.fit` and `camera.limits`. Since world y points down, `minY` is the top edge. Every value must be a finite number, with each min at or below its max, or the call throws. ```ts export interface WorldBounds ``` Example: ```ts import type { WorldBounds } from "@vhult/graph"; const area: WorldBounds = { minX: 0, minY: 0, maxX: 1200, maxY: 800 }; graph.camera.fit({ bounds: area, duration: 400 }); ``` #### `maxX` ```ts maxX: number; ``` The right edge, in world units. Example: ```ts graph.camera.limits({ bounds: { minX: 0, minY: 0, maxX: 5000, maxY: 5000 } }); ``` #### `maxY` ```ts maxY: number; ``` The bottom edge, in world units. Example: ```ts graph.camera.limits({ bounds: { minX: 0, minY: 0, maxX: 5000, maxY: 2000 } }); ``` #### `minX` ```ts minX: number; ``` The left edge, in world units. Example: ```ts graph.camera.fit({ bounds: { minX: -300, minY: 0, maxX: 300, maxY: 200 } }); ``` #### `minY` ```ts minY: number; ``` The top edge, in world units. Example: ```ts graph.camera.fit({ bounds: { minX: 0, minY: -300, maxX: 200, maxY: 300 } }); ``` ## Input Input sets how the graph reacts to the mouse, touch and wheel: pan, zoom, rotate, drag and select. You reach it through `graph.input`, and it also sets what the pointer can pick. ### `graph.input` (interface `GraphInputApi`) Everything about interaction goes through `graph.input`. Each interaction can run by itself, only send its events so your code acts, or be turned off. You can also set the first values when you call `Graph.create`, with the `input` option. ```ts export interface GraphInputApi ``` Example: ```ts graph.input.set({ rotate: "auto", selectShape: "lasso" }); ``` #### `set` ```ts set(partial: GraphInput): void; ``` Changes some of the settings. Fields you leave out keep their value. A wrong value throws a `GraphError` with the code `"invalid-argument"`. Example: ```ts graph.input.set({ drag: false }); graph.input.set({ pick: { edges: false } }); ``` ### `GraphInput` (interface) The settings you pass to `input.set` or to the `input` option of `Graph.create`. Every field is optional. Pan, zoom, drag and select are on by default, rotate is off. ```ts export interface GraphInput ``` Example: ```ts import { Graph, type GraphInput } from "@vhult/graph"; const input: GraphInput = { rotate: "auto", selectKey: "alt", pickRadius: 4 }; const view = await Graph.create(canvas, { input }); ``` #### `drag` (optional) ```ts drag?: Mode; ``` A press on a node and a move of more than 3 CSS px drags it. When the node is selected, every selected node moves with it. In `"manual"` the nodes stay in place and you get the `dragStart`, `drag` and `dragEnd` events to move them. With `false`, a press on a node pans the camera. Example: ```ts graph.input.set({ drag: "manual" }); graph.on("dragEnd", (e) => console.log(`node ${e.index} moved by ${e.dx}, ${e.dy}`)); ``` #### `edgePickRadius` (optional) ```ts edgePickRadius?: number; ``` How far from an edge the pointer still hits it, in CSS px. The default is 4. Example: ```ts graph.input.set({ edgePickRadius: 8 }); ``` #### `pan` (optional) ```ts pan?: Mode; ``` A press and move on empty space moves the camera. In `"manual"` the camera does not move and you get `pan` events to move it yourself. The default is `"auto"`. Example: ```ts graph.input.set({ pan: "manual" }); graph.on("pan", (e) => { const view = graph.camera.get(); graph.camera.set({ x: view.x - e.dx / view.zoom, y: view.y - e.dy / view.zoom }); }); ``` #### `pick` (optional) ```ts pick?: { nodes?: boolean; edges?: boolean; groups?: boolean; }; ``` What hover, click and the hover highlight can find: nodes, edges and groups. By default nodes and edges are on and groups are off. Fields you leave out keep their value. Example: ```ts graph.input.set({ pick: { nodes: true, edges: false } }); ``` #### `pickRadius` (optional) ```ts pickRadius?: number; ``` Extra reach around each node for the pointer, in CSS px. It makes small nodes easier to hit. The default is 0. Example: ```ts graph.input.set({ pickRadius: 6 }); ``` #### `rotate` (optional) ```ts rotate?: Mode; ``` A two-finger twist on a touch screen turns the view. In `"manual"` the view does not turn and you get `rotate` events. The default is `false`. Example: ```ts graph.input.set({ rotate: "auto" }); ``` #### `select` (optional) ```ts select?: Mode; ``` A click and a drawn shape select nodes. In `"auto"` the engine sets `Flag.selected` on them. In `"manual"` no flag changes and the `select` event tells you which nodes were picked. The default is `"auto"`. Example: ```ts import { Flag } from "@vhult/graph"; graph.input.set({ select: "manual" }); graph.on("select", (e) => graph.nodes.flag(e.nodes, Flag.focused, true)); ``` #### `selectKey` (optional) ```ts selectKey?: SelectKey | null; ``` The key to hold while you press on empty space to draw the select shape. The default is `"shift"`. With null the shape is drawn on every press on empty space, so a drag there no longer pans. Touch never draws the shape. Example: ```ts graph.input.set({ selectKey: null, selectShape: "lasso" }); ``` #### `selectShape` (optional) ```ts selectShape?: SelectShape; ``` The shape you draw to select nodes: `"box"` for a rectangle or `"lasso"` for a free line. The default is `"box"`. Example: ```ts graph.input.set({ selectShape: "lasso" }); ``` #### `zoom` (optional) ```ts zoom?: Mode; ``` The wheel and a two-finger pinch zoom around the pointer. In `"manual"` the camera does not zoom and you get `zoom` events. With `false` the wheel scrolls the page as usual. The default is `"auto"`. Example: ```ts graph.input.set({ zoom: false }); ``` ### `Mode` (type) How an interaction runs. With `"auto"` the engine does it and sends its events. With `"manual"` the engine only sends the events, and your code decides what happens. With `false` the interaction is off and sends nothing. ```ts export type Mode = "auto" | "manual" | false; ``` Example: ```ts graph.input.set({ pan: "auto", drag: "manual", rotate: false }); ``` ### `SelectKey` (type) The key that draws the select shape: `"shift"`, `"alt"`, `"ctrl"` or `"meta"`. Use it in the `selectKey` setting. ```ts export type SelectKey = "shift" | "alt" | "ctrl" | "meta"; ``` Example: ```ts graph.input.set({ selectKey: "alt" }); ``` ### `SelectShape` (type) The shape drawn to select nodes, `"box"` or `"lasso"`. Use it in the `selectShape` setting. The `select` event reports it in `shape`. ```ts export type SelectShape = "box" | "lasso"; ``` Example: ```ts graph.input.set({ selectShape: "box" }); ``` ## Events Events tell you what the user did and what changed in the engine. Listen with `graph.on`, and stop with the function it returns. ### `graph.on` (interface `GraphEvents`) `graph.on(name, fn)` calls `fn` with the payload each time the event fires. It returns a function that stops listening. The engine only sends an event while something listens to it, so events you do not use cost nothing. ```ts export interface GraphEvents ``` Example: ```ts const stop = graph.on("click", (hit) => console.log(hit.node)); stop(); ``` #### `click` ```ts click: Hit; ``` Fires on a left click on a node, an edge or empty space. A press that moves more than 3 CSS px is not a click. The payload is a `Hit`. Example: ```ts graph.on("click", (hit) => { if (hit.node !== null) console.log(`node ${hit.node}`); else if (hit.edge !== null) console.log(`edge ${hit.edge}`); }); ``` #### `contextMenu` ```ts contextMenu: Hit; ``` Fires on a right click or a long press. The payload is a `Hit`. While you listen to it, the browser menu does not open. Example: ```ts graph.on("contextMenu", (hit) => { if (hit.node !== null) console.log(`menu for node ${hit.node} at ${hit.screenX}, ${hit.screenY}`); }); ``` #### `doubleClick` ```ts doubleClick: Hit; ``` Fires on a left double click. The payload is a `Hit`. The two clicks before it still fire `click`. Example: ```ts graph.on("doubleClick", (hit) => { if (hit.node !== null) graph.camera.fit({ nodes: Uint32Array.of(hit.node) }); }); ``` #### `drag` ```ts drag: DragMoveEvent; ``` Fires when the dragged nodes move. The payload is a `DragMoveEvent` with the offset from the start, in world units. With `drag: "manual"` you move the nodes yourself from it. Example: ```ts let start = { x: 0, y: 0 }; graph.on("dragStart", (e) => (start = { x: e.x, y: e.y })); graph.on("drag", (e) => { graph.nodes.update(Uint32Array.of(e.index), { positions: new Float32Array([start.x + e.dx, start.y + e.dy]) }); }); ``` #### `dragEnd` ```ts dragEnd: DragMoveEvent; ``` Fires when a drag ends: on release, or when a right click, a pinch or `drag: false` stops it. The payload is a `DragMoveEvent` with the last offset. Example: ```ts graph.on("dragEnd", (e) => console.log(`node ${e.index} moved by ${e.dx}, ${e.dy}`)); ``` #### `dragStart` ```ts dragStart: DragStartEvent; ``` Fires when a drag on a node starts, after the pointer moves more than 3 CSS px. The payload is a `DragStartEvent` with the grabbed node and every node that moves with it. Example: ```ts graph.on("dragStart", (e) => console.log(`dragging ${e.nodes.length} nodes`)); ``` #### `edgesRemoved` ```ts edgesRemoved: Uint32Array; ``` Fires after `nodes.remove` with the indices of the edges that went away with the removed nodes. Use it to drop any data you keep for those edges. Example: ```ts const edgeNames = new Map(); graph.on("edgesRemoved", (edges) => { for (const e of edges) edgeNames.delete(e); }); ``` #### `error` ```ts error: GraphError; ``` Fires when something fails after `Graph.create`, for example when the GPU device is lost. The payload is a `GraphError` with a `code` and a `message`. With no listener, the error goes to `console.error`. Example: ```ts graph.on("error", (err) => { if (err.code === "device-lost") showFallback(err.message); }); ``` #### `hover` ```ts hover: Hit; ``` Fires when the node or edge under the pointer changes. The payload is a `Hit`, with null fields when the pointer leaves them, leaves the canvas or the camera moves. What it can find is set by `pick` in `graph.input`. Example: ```ts graph.on("hover", (hit) => { canvas.style.cursor = hit.node !== null || hit.edge !== null ? "pointer" : "default"; }); ``` #### `pan` ```ts pan: PanEvent; ``` Fires while the user pans, at most once per frame. The payload is a `PanEvent`. It fires in `"auto"` and `"manual"` mode. Example: ```ts graph.on("pan", (e) => { if (e.phase === "end") console.log("pan done"); }); ``` #### `rotate` ```ts rotate: RotateEvent; ``` Fires while the user turns the view with two fingers, at most once per frame. The payload is a `RotateEvent`. It needs `rotate` on in `graph.input`, since it is off by default. Example: ```ts graph.input.set({ rotate: "auto" }); graph.on("rotate", (e) => console.log(`turned ${e.angle} rad`)); ``` #### `select` ```ts select: SelectEvent; ``` Fires after each click and each drawn shape while `select` is not `false`. The payload is a `SelectEvent`. In `"auto"` it holds the whole selection; in `"manual"` it holds the nodes you picked, and you decide what to do with them. Example: ```ts graph.on("select", (e) => console.log(`${e.nodes.length} nodes selected with a ${e.shape}`)); ``` #### `view` ```ts view: CameraView; ``` Fires when the camera moves, for any reason, at most once per frame. The payload is a `CameraView`: the centre in world units, the zoom in CSS px per world unit and the rotation in radians. Example: ```ts graph.on("view", (view) => console.log(`zoom ${view.zoom.toFixed(2)}`)); ``` #### `zoom` ```ts zoom: ZoomEvent; ``` Fires while the user zooms with the wheel or a pinch, at most once per frame. The payload is a `ZoomEvent`. A wheel zoom ends 150 ms after the last wheel step. Example: ```ts graph.on("zoom", (e) => { if (e.phase === "move") console.log(`zoom step ${e.factor.toFixed(2)}`); }); ``` ### `Hit` (interface) What is under the pointer or under a point you ask about. The `hover`, `click`, `doubleClick` and `contextMenu` events send it, and `query.at` returns it. Each of `node`, `edge` and `group` is null when nothing of that kind is there. ```ts export interface Hit ``` Example: ```ts const hit = await graph.query.at(120, 80); if (hit.node !== null) console.log(`node ${hit.node} at ${hit.x}, ${hit.y}`); ``` #### `alt` ```ts alt: boolean; ``` True when the Alt key was held. #### `button` ```ts button: number; ``` The mouse button of the press, as in DOM mouse events. It is -1 for `hover` and `query.at`. #### `ctrl` ```ts ctrl: boolean; ``` True when the Control key was held. #### `edge` ```ts edge: number | null; ``` The index of the edge under the point, or null. #### `group` ```ts group: number | null; ``` The group id under the point, or null. #### `meta` ```ts meta: boolean; ``` True when the Meta key was held. #### `node` ```ts node: number | null; ``` The index of the node under the point, or null. #### `screenX` ```ts screenX: number; ``` The x of the point on the canvas, in CSS px. #### `screenY` ```ts screenY: number; ``` The y of the point on the canvas, in CSS px. #### `shift` ```ts shift: boolean; ``` True when the Shift key was held. #### `x` ```ts x: number; ``` The x of the point, in world units. #### `y` ```ts y: number; ``` The y of the point, in world units. ### `SelectEvent` (interface) The payload of the `select` event. It says which nodes, the gesture that picked them and which keys were held. ```ts export interface SelectEvent ``` Example: ```ts import type { SelectEvent } from "@vhult/graph"; function onSelect(e: SelectEvent) { if (e.shape === "click" && e.nodes.length === 0) console.log("clicked empty space"); } graph.on("select", onSelect); ``` #### `alt` ```ts alt: boolean; ``` True when the Alt key was held. #### `ctrl` ```ts ctrl: boolean; ``` True when the Control key was held. #### `meta` ```ts meta: boolean; ``` True when the Meta key was held. #### `nodes` ```ts nodes: Uint32Array; ``` In `"auto"`, every selected node, in the order they were selected. In `"manual"`, the nodes inside the shape or the clicked node, and none for a click on empty space. #### `shape` ```ts shape: "click" | SelectShape; ``` The gesture that made the selection: `"click"`, `"box"` or `"lasso"`. #### `shift` ```ts shift: boolean; ``` True when the Shift key was held. ### `DragStartEvent` (interface) The payload of the `dragStart` event. It holds the grabbed node, every node that moves with it and where the grabbed node was. ```ts export interface DragStartEvent ``` Example: ```ts import type { DragStartEvent } from "@vhult/graph"; graph.on("dragStart", (e: DragStartEvent) => console.log(`node ${e.index} from ${e.x}, ${e.y}`)); ``` #### `index` ```ts index: number; ``` The node under the pointer when the drag started. #### `nodes` ```ts nodes: Uint32Array; ``` Every dragged node. When the grabbed node is selected, it holds every selected node; otherwise only the grabbed one. #### `x` ```ts x: number; ``` The x of the grabbed node at the start, in world units. #### `y` ```ts y: number; ``` The y of the grabbed node at the start, in world units. ### `DragMoveEvent` (interface) The payload of the `drag` and `dragEnd` events. It holds the grabbed node and how far it moved since the drag started. ```ts export interface DragMoveEvent ``` Example: ```ts import type { DragMoveEvent } from "@vhult/graph"; const log = (e: DragMoveEvent) => console.log(`node ${e.index}: ${e.dx}, ${e.dy}`); graph.on("drag", log); graph.on("dragEnd", log); ``` #### `dx` ```ts dx: number; ``` The horizontal offset since the start of the drag, in world units. #### `dy` ```ts dy: number; ``` The vertical offset since the start of the drag, in world units. #### `index` ```ts index: number; ``` The grabbed node. ### `PanEvent` (interface) The payload of the `pan` event. It holds the move since the last event, in CSS px, and where the pointer is. ```ts export interface PanEvent ``` Example: ```ts import type { PanEvent } from "@vhult/graph"; let total = 0; graph.on("pan", (e: PanEvent) => (total += Math.hypot(e.dx, e.dy))); ``` #### `alt` ```ts alt: boolean; ``` True when the Alt key was held. #### `ctrl` ```ts ctrl: boolean; ``` True when the Control key was held. #### `dx` ```ts dx: number; ``` The horizontal move since the last event, in CSS px. #### `dy` ```ts dy: number; ``` The vertical move since the last event, in CSS px. #### `meta` ```ts meta: boolean; ``` True when the Meta key was held. #### `phase` ```ts phase: GesturePhase; ``` Where the gesture is: `"start"`, `"move"` or `"end"`. #### `shift` ```ts shift: boolean; ``` True when the Shift key was held. #### `x` ```ts x: number; ``` The pointer x on the canvas, in CSS px. #### `y` ```ts y: number; ``` The pointer y on the canvas, in CSS px. ### `ZoomEvent` (interface) The payload of the `zoom` event. It holds the zoom factor of this step and the point the zoom is centred on. ```ts export interface ZoomEvent ``` Example: ```ts import type { ZoomEvent } from "@vhult/graph"; graph.on("zoom", (e: ZoomEvent) => console.log(e.factor > 1 ? "in" : "out")); ``` #### `alt` ```ts alt: boolean; ``` True when the Alt key was held. #### `ctrl` ```ts ctrl: boolean; ``` True when the Control key was held. A trackpad pinch comes as a wheel with Control held. #### `factor` ```ts factor: number; ``` The zoom factor of this step. Above 1 zooms in, below 1 zooms out. #### `meta` ```ts meta: boolean; ``` True when the Meta key was held. #### `phase` ```ts phase: GesturePhase; ``` Where the gesture is: `"start"`, `"move"` or `"end"`. #### `shift` ```ts shift: boolean; ``` True when the Shift key was held. #### `x` ```ts x: number; ``` The x of the zoom centre on the canvas, in CSS px. #### `y` ```ts y: number; ``` The y of the zoom centre on the canvas, in CSS px. ### `RotateEvent` (interface) The payload of the `rotate` event. It holds the turn of this step, in radians, and the point the turn is centred on. ```ts export interface RotateEvent ``` Example: ```ts import type { RotateEvent } from "@vhult/graph"; let turned = 0; graph.on("rotate", (e: RotateEvent) => (turned += e.angle)); ``` #### `alt` ```ts alt: boolean; ``` True when the Alt key was held. #### `angle` ```ts angle: number; ``` The turn of this step, in radians. #### `ctrl` ```ts ctrl: boolean; ``` True when the Control key was held. #### `meta` ```ts meta: boolean; ``` True when the Meta key was held. #### `phase` ```ts phase: GesturePhase; ``` Where the gesture is: `"start"`, `"move"` or `"end"`. #### `shift` ```ts shift: boolean; ``` True when the Shift key was held. #### `x` ```ts x: number; ``` The x of the turn centre on the canvas, in CSS px. #### `y` ```ts y: number; ``` The y of the turn centre on the canvas, in CSS px. ### `GesturePhase` (type) Where a pan, zoom or rotate gesture is. The first event is `"start"`, the next ones are `"move"`, and the last one is `"end"`. An `"end"` event carries no movement: 0 for `dx`, `dy` and `angle`, and 1 for `factor`. ```ts export type GesturePhase = "start" | "move" | "end"; ``` Example: ```ts import type { GesturePhase } from "@vhult/graph"; graph.on("zoom", (e) => { const phase: GesturePhase = e.phase; if (phase === "end") console.log("zoom done"); }); ``` ## Query Ask the engine what is at a point or inside a shape on the canvas. You reach it through `graph.query`, with canvas coordinates in CSS px. ### `graph.query` (interface `GraphQuery`) Finds nodes and edges from canvas coordinates, in CSS px from the top left of the canvas. The answer comes back from the render worker, so every call returns a promise. Use it for your own tools, like a custom menu or a selection you draw yourself. ```ts export interface GraphQuery ``` Example: ```ts const hit = await graph.query.at(200, 150); if (hit.node !== null) console.log(`node ${hit.node}`); const nodes = await graph.query.inside({ x: 0, y: 0, width: 300, height: 200 }); console.log(`${nodes.length} nodes in the box`); ``` #### `at` ```ts at(x: number, y: number): Promise; ``` Resolves with what is at a canvas point, as a `Hit`: the node and the edge there, or null, and the point in world units. `button` is -1 and the key fields are false, since no pointer made the query. It throws when `x` or `y` is not a finite number. Example: ```ts canvas.addEventListener("dblclick", async (e) => { const hit = await graph.query.at(e.offsetX, e.offsetY); if (hit.edge !== null) console.log(`edge ${hit.edge} at ${hit.x}, ${hit.y}`); }); ``` #### `inside` ```ts inside(shape: Rect | Polygon): Promise; ``` Resolves with the indices of the nodes inside a box or a polygon on the canvas. A node counts when its center is inside. Hidden nodes are left out, and the order of the indices is not fixed. Example: ```ts import { Flag } from "@vhult/graph"; const nodes = await graph.query.inside({ points: [100, 20, 180, 160, 20, 160] }); graph.nodes.flag(nodes, Flag.selected, true); ``` ### `Rect` (interface) A box on the canvas for `query.inside`, in CSS px. `x` and `y` are its top left corner. Width and height must be 0 or more, or the query throws. ```ts export interface Rect ``` Example: ```ts import type { Rect } from "@vhult/graph"; const box: Rect = { x: 40, y: 40, width: canvas.clientWidth - 80, height: canvas.clientHeight - 80 }; const nodes = await graph.query.inside(box); ``` #### `height` ```ts height: number; ``` The height of the box, in CSS px. Example: ```ts const nodes = await graph.query.inside({ x: 0, y: 0, width: 40, height: canvas.clientHeight }); ``` #### `width` ```ts width: number; ``` The width of the box, in CSS px. Example: ```ts const nodes = await graph.query.inside({ x: 0, y: 0, width: canvas.clientWidth, height: 40 }); ``` #### `x` ```ts x: number; ``` The left edge of the box, in CSS px from the left of the canvas. Example: ```ts const nodes = await graph.query.inside({ x: 100, y: 0, width: 50, height: 50 }); ``` #### `y` ```ts y: number; ``` The top edge of the box, in CSS px from the top of the canvas. Example: ```ts const nodes = await graph.query.inside({ x: 0, y: 100, width: 50, height: 50 }); ``` ### `Polygon` (interface) A closed shape on the canvas for `query.inside`, such as a lasso. It takes 3 to 1024 corners, in CSS px. The last corner joins the first one. ```ts export interface Polygon ``` Example: ```ts import type { Polygon } from "@vhult/graph"; const triangle: Polygon = { points: [100, 20, 180, 160, 20, 160] }; const nodes = await graph.query.inside(triangle); ``` #### `points` ```ts points: readonly number[]; ``` The corners as x then y, in CSS px from the top left of the canvas. The array must hold an even count of finite numbers. Example: ```ts const nodes = await graph.query.inside({ points: [0, 0, 300, 0, 300, 120, 150, 260, 0, 120] }); ``` ## Canvas and stats The canvas the graph draws on. You reach it through `graph.canvas` to set its size, ask for a frame or take an image, and you read the latest frame numbers with `graph.stats`. ### `graph.canvas` (interface `GraphCanvas`) Everything about the canvas itself goes through `graph.canvas`: its size, drawing a frame and taking an image of it. The engine draws only when something changed, and every API call that changes the graph asks for a frame by itself. ```ts export interface GraphCanvas ``` Example: ```ts graph.canvas.resize(800, 600); const image = await graph.canvas.snapshot(); console.log(`${image.size} bytes`); ``` #### `render` ```ts render(): void; ``` Asks the engine to draw one frame, even when nothing changed. Changes you make through the API already draw a new frame, so you rarely need it. Example: ```ts graph.canvas.render(); ``` #### `resize` ```ts resize(width: number, height: number): void; ``` Sets the size of the canvas in CSS px. The engine multiplies it by the pixel ratio to size the drawing buffer. By default the engine follows the canvas CSS size by itself, so you need this when you pass `autoResize: false` to `Graph.create`. Example: ```ts import { Graph } from "@vhult/graph"; const fixed = await Graph.create(canvas, { autoResize: false }); fixed.canvas.resize(1280, 720); ``` #### `snapshot` ```ts snapshot(type?: string): Promise; ``` Draws a frame and resolves with an image of it, as a `Blob`. The type is an image MIME type and defaults to `"image/png"`. The image has the size of the drawing buffer, in device px. Example: ```ts const blob = await graph.canvas.snapshot("image/webp"); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "graph.webp"; link.click(); ``` ### `GraphStats` (interface) The latest frame numbers, returned by `graph.stats`. Reading them is cheap, so you can call it every frame. Pass the same object each time: the engine writes into it and does not allocate a new one. ```ts export interface GraphStats ``` Example: ```ts import type { GraphStats } from "@vhult/graph"; const stats = {} as GraphStats; function frame() { graph.stats(stats); console.log(`${stats.cpuMsAvg.toFixed(2)} ms, ${stats.visibleNodes} nodes drawn`); requestAnimationFrame(frame); } requestAnimationFrame(frame); ``` #### `cpuMs` ```ts cpuMs: number; ``` The time the worker spent on the CPU for the last frame it drew, in ms. #### `cpuMsAvg` ```ts cpuMsAvg: number; ``` The same worker CPU time as `cpuMs`, as a moving average, in ms. It moves less from frame to frame. #### `droppedSamples` ```ts droppedSamples: number; ``` How many GPU timing samples the profiler dropped since start. #### `edgeCount` ```ts edgeCount: number; ``` How many edges are in the engine. Removed edges do not count. #### `frameIndex` ```ts frameIndex: number; ``` The index of the last frame. #### `gpuBytes` ```ts gpuBytes: number; ``` How many bytes the engine holds in GPU buffers right now. #### `gpuMs` ```ts gpuMs: number; ``` The GPU time of a frame, as a rolling mean, in ms. It is NaN when the GPU has no timestamp queries, see `GraphCaps.timestampQuery`. It is also NaN while `graph.debug` is not open, recording or running a benchmark, because only then does the engine time every GPU pass. #### `labelsAdded` ```ts labelsAdded: number; ``` How many labels started to fade in since start. #### `labelSolves` ```ts labelSolves: number; ``` How many label placements the engine finished since start. The engine places labels again when the camera, the nodes or the labels change. #### `labelsRemoved` ```ts labelsRemoved: number; ``` How many labels started to fade out since start. #### `labelsShown` ```ts labelsShown: number; ``` How many labels are on screen. Labels that are fading out do not count. #### `nodeCount` ```ts nodeCount: number; ``` How many node slots the engine holds, the same number as `graph.nodes.slots`. Removed nodes still count until `nodes.compact`. #### `passMs` ```ts passMs: Record; ``` The GPU time of each pass, as a rolling mean, in ms. The keys are the names in `GraphCaps.profilerSlots`. A pass that was not timed is NaN. #### `peakGpuBytes` ```ts peakGpuBytes: number; ``` The largest `gpuBytes` since start. #### `pixelRatio` ```ts pixelRatio: number; ``` How many device px make one CSS px on the canvas. It comes from the `pixelRatio` option of `Graph.create`, and defaults to `devicePixelRatio`. #### `renderedFrames` ```ts renderedFrames: number; ``` How many frames the worker drew since start. It does not grow while the graph is idle. #### `uploadBytes` ```ts uploadBytes: number; ``` How many bytes the last frame sent to the GPU. #### `viewportHeight` ```ts viewportHeight: number; ``` The height of the drawing buffer, in device px. #### `viewportWidth` ```ts viewportWidth: number; ``` The width of the drawing buffer, in device px. #### `visibleEdges` ```ts visibleEdges: number; ``` How many edges the last profiled frame sent to be drawn. #### `visibleNodes` ```ts visibleNodes: number; ``` How many nodes the last profiled frame drew. ## Debug Tools to see and measure what the engine does. You reach them through `graph.debug`: an overlay on the canvas, frame recordings, benchmarks along a camera path and a look inside label placement. ### `graph.debug` (interface `GraphDebug`) Everything for measuring the engine goes through `graph.debug`. The overlay shows frame times over the canvas, `record` captures every frame for a while, and `benchmark` plays a camera path and times each frame. You do not need any of it to draw a graph. ```ts export interface GraphDebug ``` Example: ```ts graph.debug.open(); graph.debug.expand(); const recording = await graph.debug.record(); console.log(`${recording.frames} frames in ${recording.durationMs} ms`); ``` #### `benchmark` ```ts benchmark(options: BenchmarkOptions): Promise; ``` Plays a camera path, one step per frame, and resolves with the timings of every frame. Frame N always shows the same view, however fast the GPU is, so runs compare across machines and versions. The engine draws every frame while it runs. It rejects when a benchmark is already running, when the path is empty or when `frames` is not above 0. Example: ```ts const result = await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }, { x: 0.3, y: 0.6, zoom: 8 }], frames: 300, }); const cpu = Array.from(result.cpuMs).sort((a, b) => a - b); console.log(`cpu p95 ${cpu[Math.floor(cpu.length * 0.95)]} ms`); ``` #### `close` ```ts close(): void; ``` Removes the debug overlay. Unless a recording or a benchmark runs, the engine stops timing every GPU pass. Example: ```ts graph.debug.close(); ``` #### `expand` ```ts expand(expanded?: boolean): void; ``` Expands the overlay, or collapses it with `false`. Expanded, it shows GPU and CPU charts and the **Record** and **Export JSON** buttons, and the engine also times each CPU step of the frame. It does not open the overlay; clicking the collapsed overlay also expands it. Example: ```ts graph.debug.open(); graph.debug.expand(true); ``` #### `isOpen` ```ts isOpen(): boolean; ``` Returns true when the debug overlay is open. Example: ```ts if (!graph.debug.isOpen()) graph.debug.open(); ``` #### `labelSnapshot` ```ts labelSnapshot(): Promise; ``` Resolves with the candidates of the next label placement and what the placement decided for each, as a `LabelSnapshot`. When there is nothing to place, the snapshot is empty. Example: ```ts const snap = await graph.debug.labelSnapshot(); const shown = snap.decision.filter((d) => d === 1).length; console.log(`${shown} of ${snap.found} candidates shown`); ``` #### `open` ```ts open(): void; ``` Shows the debug overlay in the top left corner of the canvas. While it is open, the engine times every GPU pass, so `graph.stats` reports `gpuMs`. It starts collapsed. Example: ```ts graph.debug.open(); ``` #### `record` ```ts record(): Promise; ``` Starts recording every frame and resolves with a `DebugRecording` once it stops. It stops on `stop`, on the overlay **Stop** button, or by itself after 10 seconds or 20,000 frames. The overlay does not need to be open. It rejects when a recording is already running. Example: ```ts const done = graph.debug.record(); setTimeout(() => graph.debug.stop(), 3000); const recording = await done; console.log(`gpu p95 ${recording.summary["gpu"]?.p95} ms`); ``` #### `stop` ```ts stop(): void; ``` Stops the recording started by `record`, which then resolves. It does nothing when no recording is running. Example: ```ts graph.debug.stop(); ``` #### `toggle` ```ts toggle(): void; ``` Opens the overlay when it is closed, and closes it when it is open, for example behind a key. Example: ```ts window.addEventListener("keydown", (e) => { if (e.key === "F2") graph.debug.toggle(); }); ``` #### `tune` ```ts tune(tune: DebugTune): void; ``` Changes engine settings, see `DebugTune`. It is not stable API. Fields you leave out keep their value. It throws when a number is not finite or is below its minimum, or when `edgeMode` is unknown. Example: ```ts graph.debug.tune({ edgeMode: "length", edgeMinLengthPx: 10 }); ``` ### `DebugRecording` (interface) What `debug.record` resolves with: every recorded frame, statistics per series and facts about the machine. The overlay **Export JSON** button saves the same data as a file. ```ts export interface DebugRecording ``` Example: ```ts import type { DebugRecording } from "@vhult/graph"; const recording: DebugRecording = await graph.debug.record(); for (const [name, s] of Object.entries(recording.summary)) { console.log(`${name}: mean ${s.mean.toFixed(2)}, p95 ${s.p95.toFixed(2)}`); } ``` #### `adapter` ```ts adapter: string; ``` A description of the GPU adapter, such as "nvidia ampere". #### `caps` ```ts caps: GraphCaps; ``` What the GPU and the page support, as a `GraphCaps`. #### `date` ```ts date: string; ``` When the recording finished, as an ISO 8601 string. #### `durationMs` ```ts durationMs: number; ``` How long the recording ran, in ms. #### `edgeCount` ```ts edgeCount: number; ``` How many edges the engine held when the recording finished. #### `frames` ```ts frames: number; ``` How many frames were recorded. Every array in `series` has this length. #### `gpuGroups` ```ts gpuGroups: Record; ``` For each GPU pass series, the group the overlay charts it in. #### `mainThread` ```ts mainThread: Record; ``` A `DebugTotals` for each API call made on the main thread during the recording. #### `nodeCount` ```ts nodeCount: number; ``` How many node slots the engine held when the recording finished, as in `GraphStats.nodeCount`. #### `pixelRatio` ```ts pixelRatio: number; ``` How many device px make one CSS px on the canvas. #### `schema` ```ts schema: 1; ``` The version of the recording format. It is 1. #### `series` ```ts series: Record; ``` The value of each frame, by series name, such as `gpu`, `interval` or `cpu.frame`. A frame without a value holds NaN. #### `summary` ```ts summary: Record; ``` A `DebugSummary` for each series, by the same names as `series`. It counts only the frames that have a value. #### `userAgent` ```ts userAgent: string; ``` The user agent string of the browser. #### `viewport` ```ts viewport: [width: number, height: number]; ``` The width and height of the drawing buffer, in device px. #### `workerMessages` ```ts workerMessages: Record; ``` A `DebugTotals` for each type of message the worker handled during the recording. ### `DebugSummary` (interface) The statistics of one series of a `DebugRecording`. Frames without a value are left out. ```ts export interface DebugSummary ``` Example: ```ts import type { DebugSummary } from "@vhult/graph"; const recording = await graph.debug.record(); const gpu: DebugSummary | undefined = recording.summary["gpu"]; if (gpu) console.log(`gpu p50 ${gpu.p50} ms, p99 ${gpu.p99} ms`); ``` #### `max` ```ts max: number; ``` The largest value. #### `mean` ```ts mean: number; ``` The mean of the values. #### `n` ```ts n: number; ``` How many frames have a value. #### `p50` ```ts p50: number; ``` The median of the values. #### `p95` ```ts p95: number; ``` The 95th percentile of the values. #### `p99` ```ts p99: number; ``` The 99th percentile of the values. #### `ran` ```ts ran: number; ``` The share of frames that have a value, from 0 to 1. ### `DebugTotals` (interface) How often one message type or API call ran during a `DebugRecording`, and how long it took. ```ts export interface DebugTotals ``` Example: ```ts import type { DebugTotals } from "@vhult/graph"; const recording = await graph.debug.record(); const calls: Record = recording.mainThread; for (const [name, t] of Object.entries(calls)) console.log(`${name}: ${t.count} calls, ${t.totalMs} ms`); ``` #### `count` ```ts count: number; ``` How many times it ran. #### `maxMs` ```ts maxMs: number; ``` The longest single run, in ms. #### `totalMs` ```ts totalMs: number; ``` The time of all runs added up, in ms. ### `DebugTune` (interface) Engine settings you change with `debug.tune`. It is **not stable API**: it is made for the Storybook and the bench. Edge and LOD settings take effect once the engine has rebuilt the shaders that use them. ```ts export interface DebugTune ``` Example: ```ts import type { DebugTune } from "@vhult/graph"; const tune: DebugTune = { lodTargetPx: 0, edgeMaxOverdraw: 0, edgeMinLengthPx: 0 }; graph.debug.tune(tune); ``` #### `edgeMaxOverdraw` (optional) ```ts edgeMaxOverdraw?: number; ``` How far crowded edges are thinned out. A lower value draws fewer edges, and 0 draws every edge. The default is 1.5. Example: ```ts graph.debug.tune({ edgeMaxOverdraw: 0 }); ``` #### `edgeMinLengthPx` (optional) ```ts edgeMinLengthPx?: number; ``` The length on screen, in CSS px, at or below which an edge is not drawn. Edges up to 1.5 times this length fade in. The default is 6. Example: ```ts graph.debug.tune({ edgeMinLengthPx: 2 }); ``` #### `edgeMode` (optional) ```ts edgeMode?: EdgeDebugMode; ``` Colors edges to show what the edge cull decided, see `EdgeDebugMode`. The default is `"off"`. Example: ```ts graph.debug.tune({ edgeMode: "thinning" }); ``` #### `lodTargetPx` (optional) ```ts lodTargetPx?: number; ``` The spacing between nodes, in device px, below which nodes merge into clusters. 0 turns this off. The default is 2.5. Example: ```ts graph.debug.tune({ lodTargetPx: 4 }); ``` #### `pickRate` (optional) ```ts pickRate?: number; ``` How many times per second the engine looks for what is under the pointer for hover. It must be at least 1. The default is 60. Example: ```ts graph.debug.tune({ pickRate: 30 }); ``` ### `EdgeDebugMode` (type) How `debug.tune` colors edges to show what the edge cull decided. `"off"` draws edges as usual. `"length"` draws red for edges too short to draw (shown anyway), orange for edges fading in and green for the rest. `"thinning"` goes from blue, where every edge of a chunk is drawn, to red, where 1 in 1024 is. `"chunk"` gives each chunk of 1024 edges its own color. ```ts export type EdgeDebugMode = "off" | "length" | "thinning" | "chunk"; ``` Example: ```ts import type { EdgeDebugMode } from "@vhult/graph"; const mode: EdgeDebugMode = "chunk"; graph.debug.tune({ edgeMode: mode }); ``` ### `BenchmarkOptions` (interface) The options of `debug.benchmark`: the camera path to play, how many frames to record along it, and how much to time. ```ts export interface BenchmarkOptions ``` Example: ```ts import type { BenchmarkOptions } from "@vhult/graph"; const options: BenchmarkOptions = { path: [{ x: 0.5, y: 0.5, zoom: 1 }, { x: 0.5, y: 0.5, zoom: 16 }], frames: 600, warmup: 30, timing: "passes", }; const result = await graph.debug.benchmark(options); ``` #### `frames` ```ts frames: number; ``` How many frames to record along the path. It must be above 0. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }], frames: 120 }); ``` #### `path` ```ts path: readonly CameraPathKey[]; ``` The camera keys to play, in order, as `CameraPathKey` values. It needs at least one key. The frames are spread evenly over the path, with an ease between keys. Zoom is blended on a log scale. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0, y: 0, zoom: 4 }, { x: 1, y: 0, zoom: 4 }, { x: 1, y: 1, zoom: 4 }], frames: 300, }); ``` #### `timing` (optional) ```ts timing?: "off" | "passes" | "full"; ``` How much GPU timing to take. `"off"` does not time the GPU passes, so `gpuMs` is NaN. `"passes"` times every GPU pass. `"full"` also times each CPU step of the frame. The default is `"passes"`. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }], frames: 120, timing: "off" }); ``` #### `warmup` (optional) ```ts warmup?: number; ``` How many frames to draw at the first key before recording starts. They are not in the result. The default is 10. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }], frames: 120, warmup: 60 }); ``` ### `BenchmarkResult` (interface) What `debug.benchmark` resolves with. Each series is a `Float64Array` with one entry per recorded frame, and NaN where a frame has no value. ```ts export interface BenchmarkResult ``` Example: ```ts import type { BenchmarkResult } from "@vhult/graph"; const result: BenchmarkResult = await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }], frames: 300 }); const gpu = Array.from(result.gpuMs).filter((v) => !Number.isNaN(v)); console.log(`gpu mean ${gpu.reduce((a, b) => a + b, 0) / gpu.length} ms`); ``` #### `adapter` ```ts adapter: string; ``` A description of the GPU adapter. #### `cpuMs` ```ts cpuMs: Float64Array; ``` The time the worker spent on the CPU for each frame, in ms. #### `frames` ```ts frames: number; ``` How many frames were recorded. #### `gpuMs` ```ts gpuMs: Float64Array; ``` The GPU time of each frame, in ms. #### `intervalMs` ```ts intervalMs: Float64Array; ``` The time between a frame and the one before it, in ms. The first entry is NaN. #### `nodeCount` ```ts nodeCount: number; ``` How many node slots the engine held, as in `GraphStats.nodeCount`. #### `passMs` ```ts passMs: Record; ``` The GPU time of each pass for each frame, in ms, by pass name as in `GraphCaps.profilerSlots`. #### `timestampQuery` ```ts timestampQuery: boolean; ``` True when GPU timestamp queries were available. When false, `gpuMs` and `passMs` hold NaN. #### `viewport` ```ts viewport: [width: number, height: number]; ``` The width and height of the drawing buffer, in device px. #### `visibleEdges` ```ts visibleEdges: Float64Array; ``` How many edges each frame sent to be drawn. #### `visibleNodes` ```ts visibleNodes: Float64Array; ``` How many nodes each frame drew. #### `wallMs` ```ts wallMs: number; ``` The time from the first recorded frame until the GPU finished its work, in ms. #### `warmup` ```ts warmup: number; ``` How many frames were drawn before recording started. ### `CameraPathKey` (interface) One key of a benchmark camera path. It is relative to the nodes, not to world units, so the same path works on any dataset. The camera is not rotated along the path. ```ts export interface CameraPathKey ``` Example: ```ts import type { CameraPathKey } from "@vhult/graph"; const path: CameraPathKey[] = [ { x: 0.5, y: 0.5, zoom: 1 }, { x: 0.25, y: 0.75, zoom: 10 }, ]; await graph.debug.benchmark({ path, frames: 300 }); ``` #### `x` ```ts x: number; ``` The x of the view center, as a share of the node bounds. 0 is the smallest node x, 0.5 the middle and 1 the largest. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0, y: 0.5, zoom: 2 }, { x: 1, y: 0.5, zoom: 2 }], frames: 200 }); ``` #### `y` ```ts y: number; ``` The y of the view center, as a share of the node bounds. 0.5 is the middle. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0.5, y: 0, zoom: 2 }, { x: 0.5, y: 1, zoom: 2 }], frames: 200 }); ``` #### `zoom` ```ts zoom: number; ``` A multiple of the zoom that fits every node on screen. 1 shows the whole graph, and 10 zooms in ten times closer. Example: ```ts await graph.debug.benchmark({ path: [{ x: 0.5, y: 0.5, zoom: 1 }, { x: 0.5, y: 0.5, zoom: 10 }], frames: 200 }); ``` ### `LabelSnapshot` (interface) What `debug.labelSnapshot` resolves with: the label candidates of one placement and what it decided for each. Each array has one entry per candidate, up to `capacity`, and `center` has two. ```ts export interface LabelSnapshot ``` Example: ```ts import type { LabelSnapshot } from "@vhult/graph"; const snap: LabelSnapshot = await graph.debug.labelSnapshot(); if (snap.found > snap.capacity) console.log(`${snap.found - snap.capacity} candidates did not fit`); ``` #### `capacity` ```ts capacity: number; ``` How many candidates the placement can hold. #### `center` ```ts center: Float32Array; ``` The center of each label box, in device px, as x then y. #### `decision` ```ts decision: Uint8Array; ``` What the placement decided for each candidate: 0 undecided, 1 shown, 2 hidden. #### `found` ```ts found: number; ``` How many candidates the placement found, including the ones beyond `capacity`. #### `halfHeight` ```ts halfHeight: Float32Array; ``` Half the height of each label box, in device px, padding included. #### `halfWidth` ```ts halfWidth: Float32Array; ``` Half the width of each label box, in device px, padding included. #### `index` ```ts index: Uint32Array; ``` For a node label, the engine node index. For an edge label, the sorted edge index with the top bit set. #### `rank` ```ts rank: Float32Array; ``` The priority of each candidate. A higher rank wins over a lower one. #### `size` ```ts size: Float32Array; ``` For a node label, the node size in world units. For an edge label, the length of the edge on screen, in device px.