@tomaszatoo/graph-viewer
v3.1.0
Published
An Angular standalone component for rendering interactive, animated graphs using Pixi.js and Graphology.
Downloads
196
Maintainers
Readme
GraphViewerComponent
An Angular standalone component for rendering interactive, animated graphs using Pixi.js and Graphology.
Designed for modularity, performance, and customizability—bring your own renderers, drive your own layout, hack the internals.
🐣 Installation
npm i @tomaszatoo/graph-viewer🧩 TL;DR
- Framework: Angular (standalone component, signal inputs)
- Rendering: Pixi.js +
pixi-viewport, render-on-demand (the GPU sleeps when nothing moves) - Layout: ForceAtlas2 worker (
graphology-layout-forceatlas2) - Graph engine: Graphology
- Customization: Pluggable node/edge renderers
🧠 Core Concepts
State goes in through inputs, actions are methods you call on the component instance, and everything that happens comes back out through a small set of outputs.
All rendering is done via Pixi.js on a WebGL/WebGPU canvas. Interaction (hovering, dragging,
selection) is mapped using Pixi's FederatedPointerEvent system. Graph logic
(nodes/edges/attributes/layout) is delegated to Graphology,
allowing separation of state and rendering.
The viewer renders on demand: the ticker suspends itself shortly after the layout worker stops and nothing is being dragged, hovered, or panned.
🧪 Inputs (state)
| Input | Type | Default | Description |
| --- | --- | --- | --- |
| graphData | GraphData \| Graph \| null | null | Raw {nodes, edges} data or a ready Graphology Graph (it is copied). Can arrive or change at any time — the scene rebuilds automatically. |
| graphOptions | GraphInstantiationOptions | {multi: false, allowSelfLoops: true, type: 'directed'} | Forwarded to the Graphology constructor when building from raw data. |
| nodeRenderer | NodeRendererFn | built-in | Returns your custom NodeWrapper for a node. |
| edgeRenderer | EdgeRendererFn | built-in | Returns your custom EdgeWrapper for an edge. |
| animate | boolean | false | Runs the ForceAtlas2 worker layout. Toggling it on/off starts/stops the worker. |
| interactive | boolean | true | Enables/disables pointer interaction with nodes and edges (live). |
| fullscreen | boolean | false | true requests fullscreen, false exits it. The wheel-zoom plugin is active in fullscreen only. |
| layoutSettings | GraphLayoutSettings | inferred | ForceAtlas2 tuning (gravity, scalingRatio, slowDown, …). Changing it restarts a running layout. |
| options | GraphVisualizerOptions | black, opaque | backgroundColor / backgroundAlpha, applied live. |
Notes:
- If every node in
graphDataalready has numericx/y, the automatic initial layout (circlepack + FA2 warm-up) is skipped and your positions are respected. - A user-supplied
radiusnode attribute is respected; otherwise radii are auto-scaled from node degree.
🛠 Methods (actions)
Grab the component instance (@ViewChild(GraphViewerComponent) or a template
reference — the component exports itself as graphViewer) and call:
Graph mutations
viewer.addNodes({ a: { label: 'Alpha' }, b: { x: 10, y: 10, label: 'Beta' } });
const keys = viewer.addEdges([{ source: 'a', target: 'b', attributes: { label: 'a → b' } }]);
viewer.dropNodes(['a']); // connected edges are dropped automatically
viewer.dropEdges(keys); // by Graphology edge key
viewer.getGraph(); // the internal Graphology graphMutations use Graphology's merge* semantics — re-adding an existing node/edge
updates it instead of throwing. Nodes added without x/y get a random initial
position and are then placed by the layout (when animate is on).
Selection & highlight
viewer.selectNode('a'); // or selectNode('a', false)
viewer.toggleNodeSelection('a'); // returns the new state
viewer.highlightNode('a'); // or highlightNode('a', false)
viewer.toggleNodeHighlight('a');
viewer.selectEdge(key); viewer.toggleEdgeSelection(key);
viewer.highlightEdge(key); viewer.toggleEdgeHighlight(key);
viewer.clearSelection(); // 'nodes' | 'edges' | 'all' (default)
viewer.clearHighlight();
viewer.selectedNodes(); // string[]
viewer.selectedEdges(); // string[]Programmatic changes emit the same selectionChange / hoverChange events as pointer
interaction, just without the event property. clearSelection / clearHighlight
are silent by design.
Viewport
viewer.snapToNode('a');
viewer.snapToCenter();
viewer.setZoom(1.5); // setZoom(scale, center = true)🧯 Outputs
selectionChange: GraphSelectionEvent
{
type: 'node' | 'edge',
id: string,
attributes: GraphNodeAttributes | GraphEdgeAttributes,
position?: Point, // nodes
source?: string, // edges
target?: string, // edges
event?: FederatedPointerEvent, // absent for programmatic changes
selected: boolean
}hoverChange: GraphHoverEvent
Same shape with highlighted: boolean instead of selected.
graphInitialised: Graph
Fires after a full scene (re)build — also when graphData changes later.
graphUpdated: Graph
Fires after incremental mutations (addNodes, addEdges, dropNodes, dropEdges).
destroyed: void
Fires when the component is destroyed.
🛠 Usage
import { GraphViewerComponent } from '@tomaszatoo/graph-viewer';<graph-viewer #viewer="graphViewer"
[graphData]="myGraphData"
[nodeRenderer]="myCustomNodeRenderer"
[edgeRenderer]="myCustomEdgeRenderer"
[animate]="true"
[layoutSettings]="myLayoutSettings"
(graphInitialised)="computeGraphTheory($event)"
(selectionChange)="handleSelection($event)"
(hoverChange)="handleHover($event)">
</graph-viewer>
<button (click)="viewer.snapToCenter()">center</button>@ViewChild('viewer') viewer!: GraphViewerComponent;
addSomething(): void {
this.viewer.addNodes({ a: { label: 'Alpha' }, b: { label: 'Beta' } });
this.viewer.addEdges([{ source: 'a', target: 'b', attributes: { weight: 1, label: 'a → b' } }]);
}
handleSelection(e: GraphSelectionEvent): void {
if (e.type === 'node') console.log('node', e.id, e.selected);
else console.log('edge', e.id, e.source, '→', e.target, e.selected);
}🚚 Migrating from v2
v3 replaces the command-style inputs with methods and unifies the events:
| v2 | v3 |
| --- | --- |
| [addNodes], [addEdges], [dropNodes], [dropEdges] inputs | addNodes(), addEdges(), dropNodes(), dropEdges() methods |
| [selectNode], [toggleNodeSelection], [highlightNode], [toggleNodeHighlight] | selectNode(), toggleNodeSelection(), highlightNode(), toggleNodeHighlight() |
| [selectEdge], [toggleEdgeSelection], [highlightEdge], [toggleEdgeHighlight] | selectEdge(), toggleEdgeSelection(), highlightEdge(), toggleEdgeHighlight() |
| [clearNodesSelection]="{clear: true}" & friends | clearSelection(), clearHighlight() |
| [snapToNode], [snapCenter], [zoom] | snapToNode(), snapToCenter(), setZoom() |
| (onNodeSelectChange), (onEdgeSelectChange) | (selectionChange) — discriminate on event.type |
| (onNodeHighlightChange), (onEdgeHighlightChange) | (hoverChange) |
| (onDestroy) | (destroyed) |
| NodePointerEvent / EdgePointerEvent | GraphSelectionEvent / GraphHoverEvent |
Behavioural fixes worth knowing about:
graphDatamay now be set/changed at any time (async data just works).- Dropped nodes/edges are actually removed from the canvas.
- Multiple
<graph-viewer>instances on one page no longer share layout state. - Clicking a node no longer disables viewport panning.
fullscreen = falseexits fullscreen; the CSS class follows the real fullscreen state (ESC included).
📚 Internals (aka "You can hack this")
NodeWrapperandEdgeWrapperare customContainers (extendable). Label text/style is initialised once; per-frame updates only touch geometry and transforms. OverrideinitNodeGraphics/initEdgeLine/initEdgeArrow/initLabelGraphics/positionLabel/updateLabelStyleto customize.- Node radii are auto-scaled using degree-based heuristics (unless you set
radius). - The layout engine is a per-component service wrapping the
graphology-layout-forceatlas2worker; positions are synced to Pixi in the animation loop while the worker runs. - Rendering suspends itself ~half a second after the last activity and wakes on layout/drag/hover/pan/zoom/selection changes.
🛣️ Roadmap Ideas
- [ ] Demo / Examples
- [x] Selecting edges
- [x] Error handling (duplicate-safe merges, missing-node guards)
- [x] Arrowheads and directed edges
- [ ] Cluster folding / node collapsing
- [ ] Tooltip system with hover delay
- [ ] Export to PNG / SVG
- [ ] Mini-map viewport tracker
📎 Dependencies
pixi.jspixi-viewportgraphologygraphology-layout-forceatlas2
🐛 Known Quirks
- Changing
graphDatadestroys/rebuilds the scene entirely. - The initial FA2 warm-up (100 iterations) runs on the main thread — can spike with very large graphs; the animated layout itself runs in a worker.
🧬 Author Notes
Built for high-performance graph rendering in web dashboards, internal devtools, and exploratory graph hacking. Designed to be extended, broken, refactored, and shaped by the needs of weird data.
🕳️ License
MIT – because locking pixels behind walls is boring.
