@leebhin/pixigraph
v0.3.2
Published
Pixi.js 기반 그래프 렌더링 라이브러리 — node/edge/highlight/preview/history/viewport (cytoscape-like API)
Maintainers
Readme
pixigraph
A Pixi.js v8 graph rendering engine with a Cytoscape.js-like API (graph.nodes(), graph.add(), graph.style(), graph.on()), built for WebGL performance on large graphs — tens of thousands of nodes and edges.
Unlike Cytoscape, pixigraph is a pure scene-graph library: it renders into a Pixi Container you mount yourself and knows nothing about the DOM. You forward pointer events into it; it stays renderer-agnostic, embeddable, and fast.
- Declarative selector styling —
node,edge,.class,:selected,#idwith AND-combination and CSS-like specificity cascade - Rich node shapes — rectangles, circles, arbitrary polygons, and image/SVG-textured nodes (with automatic shape masking and a per-graph texture cache)
- Smart edges — surface clipping to node geometry, parallel-edge curvature, target arrowheads, round caps, and animated dashed flow
- Event delegation — Cytoscape-style
graph.on(type, selector, handler)driven byfeed(), with automaticmouseover/mouseoutfrom a singlemousemove - Selection & transform handles — resize (corner/edge handles), rotate (dedicated handle or Figma-style corner ring), move, multi-select union boxes, aspect-lock, and center-resize — all per-element overridable
- Built-in viewport — pan/zoom camera with wheel & drag handlers, animated
fit/center/panToElement, and screen-stable handle/hit sizing - Highlight groups — overlay multiple named style groups on the same elements, prefix-batch removal, auto-dim of non-highlighted elements, and focus-color locking
- Undo / redo — automatic history tracking for add/remove/move/resize/rotate/data edits, with batching for compound operations
- Clipboard — copy / cut / paste / duplicate of selections, plus property-only copy/paste
- Graph editing primitives — connect nodes, inline-insert a node into an edge, split an edge with a new node, merge an edge–node–edge chain, with live preview overlays
- Hover tooltips — opt-in, property-ordered entry extraction for your own tooltip component
- Zero runtime dependencies — only
pixi.jsas a peer dependency. Ships ESM + CJS + type declarations.
Installation
npm install @leebhin/pixigraph pixi.jspixi.js (^8.0.0) is a peer dependency — install it alongside.
Quick Start
pixigraph renders into a Pixi Container (graph.view). You mount that container in your own Pixi Application, then forward pointer events into the graph using feed().
import { Application, Point } from 'pixi.js';
import { PixiGraph } from '@leebhin/pixigraph';
// 1. A Pixi application is the host renderer.
const app = new Application();
await app.init({ background: '#ffffff', resizeTo: window, antialias: true });
document.body.appendChild(app.canvas);
// 2. Create the graph. It is a pure scene graph — no DOM container here.
const graph = new PixiGraph({
selectionHandles: true, // single-node resize handles on select
viewport: { wheel: true, drag: true }, // built-in pan/zoom camera
});
app.stage.addChild(graph.view);
// The viewport needs the host canvas to attach wheel/drag listeners.
graph.viewport?.attach(app.canvas);
// 3. Add nodes and edges. Coordinates are "graph-local" (your own world space).
graph.add({
nodes: [
{ id: 'a', bbox: { x: 0, y: 0, w: 80, h: 40 } },
{ id: 'b', bbox: { x: 260, y: 0, w: 80, h: 40 } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
});
// 4. Style declaratively with selectors.
graph.style([
{ selector: 'node', style: { fill: 0x2563eb, alpha: 0.85 } },
{ selector: 'node:selected', style: { fill: 0xf59e0b } },
{ selector: 'edge', style: { stroke: 0x94a3b8, width: 2, arrowShape: 'triangle' } },
]);
// 5. Forward DOM pointer events into the graph (event delegation).
const world = () => graph.viewport?.world ?? graph.view; // graph-local space
function toLocal(e: PointerEvent) {
const r = app.canvas.getBoundingClientRect();
return world().toLocal(new Point(e.clientX - r.left, e.clientY - r.top));
}
app.canvas.addEventListener('pointertap', (e) => {
const { x, y } = toLocal(e);
graph.feed('tap', x, y, e);
});
app.canvas.addEventListener('pointermove', (e) => {
const { x, y } = toLocal(e);
graph.feed('mousemove', x, y, e); // auto-derives mouseover / mouseout
});
// 6. Subscribe with Cytoscape-style selectors.
graph.on('tap', 'node', ({ target }) => console.log('node tapped:', target?.id()));
graph.on('mouseover', 'node', ({ target }) => target?.addClass('hovered'));
graph.on('mouseout', 'node', ({ target }) => target?.removeClass('hovered'));Core Concepts
A few ideas explain the whole API:
graph.viewis a PixiContainer. You add it to your stage (or to your own world container). The graph never touches the DOM, so it embeds anywhere Pixi runs.- Coordinates are graph-local. Every position,
bbox, and hit-test uses your world space (for example, image-pixel coordinates of a diagram). Converting screen pixels ⇄ graph-local is the caller's job — typicallycontainer.toLocal(...). When the built-in viewport is active, that space isgraph.viewport.world. - Events are delegated, not listened. The graph doesn't register DOM listeners. You call
graph.feed('tap' | 'cxttap' | 'mousemove', x, y, nativeEvent); the graph hit-tests and dispatches to handlers whose selector matches. A singlemousemovefeed automatically producesmouseover/mouseout. - State changes are data; rendering follows.
ele.addClass(),ele.select(),ele.data(k, v)mutate the element and trigger a targeted re-render through the active style rules. You declare what things look like withgraph.style(); the graph decides when to repaint.
Adding Elements
graph.add(input) takes { nodes?, edges? }. Nodes are always processed before edges so edges can resolve their endpoints. Duplicate ids are ignored; an edge whose source or target node is absent is skipped.
graph.add({
nodes: [
// Rectangle (default shape).
{ id: 'n1', bbox: { x: 0, y: 0, w: 100, h: 60 }, data: { label: 'Pump' } },
// Circle.
{ id: 'n2', bbox: { x: 200, y: 0, w: 60, h: 60 }, shape: 'circle' },
// Polygon — vertices are normalized [0,1] within the bbox, flat [x0,y0,x1,y1,...].
{ id: 'n3', bbox: { x: 320, y: 0, w: 80, h: 80 },
polygonPoints: [0.5, 0, 1, 1, 0, 1] },
// Image / SVG node — stretched to fit the bbox, masked to the shape.
{ id: 'n4', bbox: { x: 440, y: 0, w: 64, h: 64 },
image: '/icons/valve.svg' },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2', data: { kind: 'flow' } },
],
});Node input fields
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique id. Referenced by edge.source / edge.target. |
| bbox | { x, y, w, h } | Graph-local axis-aligned box the node occupies. |
| data | Record<string, unknown> | Optional metadata, read back via ele.data('key'). |
| shape | 'rect' \| 'circle' \| 'polygon' | Optional. Inferred from polygonPoints when omitted (else rect). |
| polygonPoints | number[] | Normalized [0,1] flat vertices relative to the bbox. |
| image | string | Image / SVG / data-URL / blob-URL used as the node texture. Same URL is fetched & decoded once per graph and cached; a fallback fill shows until it loads. |
Edge input fields
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique id. |
| source | string | Source node id. |
| target | string | Target node id. |
| data | Record<string, unknown> | Optional metadata. |
Edges automatically clip to node surfaces (polygon outline intersection for polygon nodes, bbox boundary otherwise) and bow apart when multiple edges share the same node pair.
Styling
Styles are declarative selector rules, replacing the entire ruleset on each graph.style() call (like cy.style()).
graph.style([
{ selector: 'node', style: { fill: 0x1e293b, alpha: 0.9 } },
{ selector: 'node.critical', style: { fill: 0xdc2626 } },
{ selector: 'node:selected', style: { fill: 0xf59e0b } },
{ selector: 'edge', style: { stroke: 0x64748b, width: 2 } },
{ selector: 'edge:selected', style: { stroke: 0xf59e0b, width: 4, arrowShape: 'triangle' } },
{ selector: '#special-node', style: { fill: 0x9333ea } },
]);Selectors
A selector is one or more tokens AND-combined with no whitespace:
| Token | Matches |
|-------|---------|
| node / edge | Element group |
| .className | Elements carrying the class |
| :selected | Selected elements |
| #id | The element with that id |
Examples: node, edge, node.foo, node:selected, node.foo:selected, #my-id.
Specificity = id × 100 + (classes + pseudos) × 10 + (group ? 1 : 0). The effective style of an element is computed by cascading, lowest specificity first, then declaration order (later wins on ties), starting from group defaults.
Not supported (by design): descendant selectors, attribute selectors, function-valued styles (
data(x)), and:hover(use events to toggle ahoveredclass instead).
Style properties
| Property | Applies to | Description |
|----------|-----------|-------------|
| fill | node | Fill color — hex int (0x2563eb) or '#rrggbb'. |
| alpha | node / edge | Node fill alpha or edge stroke alpha. |
| stroke | edge | Stroke color. |
| width | edge | Stroke width (graph-local units). |
| arrowShape | edge | 'triangle' or 'none' (default 'none'). |
| arrowSize | edge | Arrowhead size (default width × 3). |
| lineCap | edge | 'butt' (default) or 'round'. |
| lineDash | edge | Dash length (graph-local units). 0 = solid. |
| lineGap | edge | Gap length. Defaults to lineDash. |
| lineDashOffset | edge | Dash phase offset. Animate it for flow effects (see Dashed flow). |
Base style override
Pass style to the constructor to change the fallback colors used when no rule matches:
const graph = new PixiGraph({
style: {
node: { fill: 0x2563eb, alpha: 0.8 },
edge: { stroke: 0x999999, width: 2, alpha: 1 },
},
});Events
The graph hit-tests on feed() and dispatches to matching handlers. Register with an optional selector:
graph.on('tap', ({ target, x, y, native }) => { /* any hit + background (target may be null) */ });
graph.on('tap', 'node', ({ target }) => { /* node hits only */ });
graph.on('cxttap', '.deletable', ({ target, native }) => { native?.preventDefault(); });
graph.on('mouseover', 'node:selected', ({ target }) => { /* … */ });
graph.off('tap', handler); // remove one
graph.off('tap'); // remove all of a type
graph.off(); // remove everythingEvent payload: { type, target, x, y, native } — target is the hit element (or null for background taps), x/y are graph-local, native is the original DOM event.
| Event | Fired by | target |
|-------|----------|----------|
| tap | feed('tap', …) (left click) | element or null (background) |
| cxttap | feed('cxttap', …) (right click / contextmenu) | element or null |
| mouseover | auto, from feed('mousemove', …) | always non-null |
| mouseout | auto, when hover leaves an element | always non-null |
| select / unselect | selection state changes | the element |
| add / remove | element added / removed | the element |
| bbox / rotation / polygon | node geometry changes | the element |
| data | ele.data(key, value) | the element |
You only ever feed tap, cxttap, and mousemove; mouseover/mouseout are derived automatically. Use graph.clearHover(native?) to force a mouseout when the pointer leaves the host canvas.
Selection & Transform Handles
Enable handles via the constructor (selectionHandles: true | options). Handles are drawn at a screen-stable size regardless of zoom and can be customized globally and per element.
const graph = new PixiGraph({
selectionHandles: {
enabled: true, // show resize handles on single-node select
corners: true, // 4 corner handles
edges: true, // 4 edge-midpoint handles
move: true, // drag nodes to move
union: false, // per-node handles on multi-select (true = one union box)
rotate: true, // enable rotation
rotateMode: 'zone', // Figma-style ring outside corners ('handle' = a dedicated top handle)
keepAspect: false, // hold Shift to lock aspect ratio temporarily
centerResize: false, // hold Ctrl to resize about center temporarily
handle: { size: 10, shape: 'square', fill: 0xffffff, stroke: 0x000000, strokeWidth: 1 },
box: { enabled: true, stroke: 0x2563eb, width: 1, dash: 4, gap: 4 },
},
});
// Selection API
graph.select('n1'); // replace selection
graph.select(['n2', 'n3'], { additive: true });
graph.unselect('n2');
graph.unselectAll();
graph.selected(); // currently selected elements
// Per-element overrides
graph.element('n1')?.resizable(false).rotatable(false).movable(true);When the viewport is active, handle and hit sizing track zoom automatically. Without it, call graph.setViewScale(scale) and graph.setHitTolerance(px) when your camera changes so handles stay screen-stable.
Handle options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enabled | boolean | false | Show resize handles on single-node select. |
| selectable | boolean | true | Global selection enable. |
| corners | boolean | true | 4 corner handles. |
| edges | boolean | false | 4 edge-midpoint handles. |
| resizeCursor | boolean | true | Show resize cursor on handle hover. |
| centerResize | boolean | false | Resize about center (also via Ctrl). |
| keepAspect | boolean | false | Lock aspect ratio (also via Shift). |
| union | boolean | false | One union box for multi-select (else per-node). |
| move | boolean | true | Drag nodes to move (needs ele.movable()). |
| selectOnGrab | boolean | true | Dragging an unselected node auto-selects + moves it. |
| rotate | boolean | false | Enable rotation. |
| rotateMode | 'zone' \| 'handle' | 'handle' | 'zone' = Figma corner ring; 'handle' = dedicated top handle. |
| rotateZone | number | 16 | zone mode: ring thickness (px). |
| rotateGap | number | 24 | handle mode: handle distance above the top edge (px). |
| handle | object | — | Handle style: size, zoomFollow, shape, fill, stroke, strokeWidth. |
| box | object | — | Selection outline: enabled, stroke, width, alpha, dash, gap. |
Viewport (Pan / Zoom Camera)
Activate with viewport: true | config. When active, graph.view becomes the camera's outer container and graph content lives inside an inner world container (graph.viewport.world). The viewport keeps handle sizing and hit tolerance screen-stable as you zoom.
const graph = new PixiGraph({
viewport: {
wheel: true, // wheel-zoom (requires viewport.attach(canvas))
drag: true, // drag-pan
dragButton: 'middle', // 'left' | 'middle' | 'right'
minZoom: 0.05,
maxZoom: 12,
hitTolerancePx: 6, // screen-px hit padding, kept constant across zoom
onChange: () => syncMinimap(),
},
});
graph.viewport?.attach(app.canvas);
// Camera control (also exposed directly on graph for convenience)
graph.fit(); // fit all elements
graph.fit([graph.element('n1')!], { padding: 0.2, duration: 300 });
graph.center(); // center all (keep zoom)
graph.panToElement('n1', { ratio: 0.18 }); // pan + select, auto-zoom to fill ~18%
graph.panToElement({ path: ['n1', 'e1', 'n2'] }, { duration: 400 });
graph.panToBbox({ x: 0, y: 0, w: 1000, h: 800 });
graph.zoom; graph.pan; // current zoom / pan (null if viewport off)Pan options (PanOptions): duration, easing, padding, zoom, ratio, maxZoom, select, onUpdate, onComplete.
Highlight Groups
Highlights overlay inline styles on a named set of elements. They cascade above selector rules, in registration order, and several groups can target the same element at once — ideal for trace results, search matches, connectivity views, and collaborative selection.
graph.highlight({ id: 'trace-1', elements: tracePath, style: { fill: '#ef4444', alpha: 0.6 } });
graph.highlight({ id: 'search', elements: matches, style: { stroke: '#0ea5e9', width: 14 } });
graph.unhighlight('search');
graph.unhighlightByPrefix('trace-'); // remove every group starting with "trace-"
graph.clearHighlights();
graph.highlightIds(); // active group ids, in orderWhen any highlight group is active, non-highlighted elements are automatically dimmed (.dim). Pass noDim: true to a group to suppress that (a "show this, leave the rest alone" overlay).
Focus color locks attention on one color: only elements in a highlight group of that color stay bright; everything else gets a softer .focus-dim.
graph.setFocusColor('#ef4444'); // only red trace paths stay prominent
graph.setFocusColor(null); // release focusBoth .dim and .focus-dim are defined by the library's built-in system style rules — they work without you adding any CSS or style rule, and you can override them by declaring the same selector in graph.style().
Undo / Redo
graph.history automatically records add / remove / move / resize / rotate / data edits.
graph.history.undo();
graph.history.redo();
graph.history.canUndo();
graph.history.canRedo();
const off = graph.history.onChange(() => updateToolbar()); // subscribe; call off() to unsubscribeFor drag interactions, suspend automatic recording during the drag and record once on release:
graph.history.suspend();
// … live drag via setNodesBboxes(...) per frame …
graph.history.resume();
graph.history.recordBboxChanges(changes); // one undo step for the whole dragWrap compound operations in a batch so a single undo reverts them together:
graph.history.beginBatch();
graph.connect('a', 'b');
graph.remove('old-edge');
graph.history.endBatch();History keeps up to 100 steps (oldest dropped). The library's own compound methods (insertNodeIntoEdge, mergeChain, splitEdgeAt, paste, …) batch internally.
Clipboard
graph.copySelection(); // copy selected elements
graph.cutSelection(); // copy + remove
graph.paste({ x: 100, y: 100 }); // paste at a graph-local point → new ids
graph.duplicate(); // copy + paste in place
graph.copyProperties(); // copy data/properties only (no geometry)
graph.pasteProperties(); // apply copied properties to the selection
graph.hasClipboard();
graph.hasPropertyClipboard();A drag-to-copy flow is also available: beginCopyDrag() → updateCopyDrag(dx, dy) → commitCopyDrag(dx, dy) / cancelCopyDrag(), with isCopyDragActive().
Graph Editing Primitives
High-level structural edits, each undoable and batched:
// Connect two nodes — auto-generates the edge id.
const id = graph.connect('a', 'b', { kind: 'flow' });
// Inline-insert an existing node into an edge (e.g. Alt-drag a node onto a wire):
graph.insertNodeIntoEdge('node-x', 'edge-1'); // → { e1, e2 } new edges
// Split an edge with a brand-new node at a projected point:
graph.splitEdgeAt('edge-1', { x: 120, y: 80, w: 40, h: 40 }); // → { nodeId, e1, e2 }
// Detect and merge an edge–node–edge chain back into a single edge:
const chain = graph.detectChain(graph.element('mid')!);
if (chain) graph.mergeChain(chain); // → new edge id
// Geometry helpers for snapping / routing:
graph.findEdgeNear({ x, y }, 8); // nearest edge within threshold
graph.projectOnEdge(edge, { x, y }); // clamped projection onto an edgePreview overlays
Before committing an edit, render a live, non-interactive preview. Previews are signature-guarded, so they're cheap to call every frame:
graph.previewInsert('node-x', targetEdge); // dim target + show the two branch edges
graph.previewMerge(chain); // mark the two edges removed + show the merged edge
graph.hasActivePreview();
graph.clearPreviews();Hover Tooltips
Opt in with tooltip: true | options. The library doesn't render the tooltip — it gives you ordered, filtered key/value entries for your own component.
const graph = new PixiGraph({
tooltip: {
enabled: true,
propertyOrder: ['category', 'symbolName', 'tagNumber'],
hiddenKeyPattern: /(uuid|id)/i,
hiddenKeys: ['polygonPoints'],
},
});
graph.on('mouseover', 'node', ({ target }) => {
const entries = graph.tooltipEntries(target!); // [[key, value], …] in priority order
showTooltip(entries);
});Dashed Flow Animation
Give edges a dash, then animate the global dash offset for a directional "flow" effect. Only dashed edges re-render, so it's cheap.
graph.style([
{ selector: 'edge.flow', style: { stroke: 0x22c55e, width: 3, lineDash: 12, lineGap: 8 } },
]);
let offset = 0;
app.ticker.add(() => {
offset -= 1; // negative flows source → target
graph.setDashOffset(offset);
});Querying & Hit Testing
graph.element('n1'); // by id, or null
graph.$('#n1'); // selector lookup (#id form)
graph.elements(); // all, insertion order
graph.nodes();
graph.edges();
graph.size();
graph.byClass('critical'); // elements with a class
graph.elementsIn({ x, y, w, h }); // rubber-band box select (excludes hidden/eye-off)
graph.elementAt(x, y); // topmost element at a graph-local point
graph.nodeAt(x, y);
graph.edgeAt(x, y);Hit testing respects real geometry (polygon outline, ellipse, rotated rect), prefers nodes over edges, and prefers the smallest overlapping node.
Configuration Reference
new PixiGraph({
style?: Partial<PixiGraphBaseStyle>, // base node/edge colors
selectionHandles?: boolean | PixiGraphHandleOptions, // resize/rotate/move handles
tooltip?: boolean | PixiGraphTooltipOptions, // hover tooltip entries
viewport?: boolean | PixiGraphViewportConfig, // pan/zoom camera
});Viewport config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| wheel | boolean | false | Wheel-zoom handler (needs attach(canvas)). |
| wheelSensitivity | number | 0.0015 | Zoom factor per deltaY unit. |
| drag | boolean | false | Drag-pan handler (needs attach(canvas)). |
| dragButton | 'left' \| 'middle' \| 'right' | 'middle' | Pan mouse button. |
| dragModifier | 'shift' \| 'ctrl' \| 'alt' \| null | null | Modifier required to start a pan. |
| minZoom | number | 0.01 | Minimum zoom. |
| maxZoom | number | 20 | Maximum zoom. |
| hitTolerancePx | number | 6 | Screen-px hit padding, kept constant across zoom (0 = off). |
| onChange | () => void | — | Called on any transform change (minimap sync, etc.). |
API Reference
new PixiGraph(config?) exposes:
Elements & queries
add · remove · clear · element · $ · elements · nodes · edges · size · byClass · elementsIn · elementAt · nodeAt · edgeAt · setHitTolerance
Styling
style · setDashOffset
Events
on · off · feed · clearHover
Selection & handles
select · unselect · unselectAll · selected · setSelectionHandles · redrawHandles · setViewScale · selectionBbox · resizableSelected · rotatableSelected · movableSelected · handlePositions · handleAt · rotateZoneAt
Node geometry
setNodeBbox · setNodesBboxes · setNodePolygon · setNodesRotations · static computeResizedBbox · static scaleBboxAbout · static handleCursor
Viewport
fit · center · panToElement · panToElements · panToBbox · zoom · pan · viewport (the PixiGraphViewport instance, or null)
Highlights
highlight · unhighlight · unhighlightByPrefix · clearHighlights · hasHighlight · highlightIds · setFocusColor · getFocusColor
History
history (a PixiGraphHistory: undo · redo · canUndo · canRedo · clear · onChange · beginBatch · endBatch · suspend · resume · record*)
Clipboard
copySelection · cutSelection · paste · duplicate · copyProperties · pasteProperties · hasClipboard · hasPropertyClipboard · beginCopyDrag · updateCopyDrag · commitCopyDrag · cancelCopyDrag · isCopyDragActive
Editing & preview
connect · insertNodeIntoEdge · splitEdgeAt · mergeChain · detectChain · findEdgeNear · projectOnEdge · previewInsert · previewMerge · hasActivePreview · currentPreviewInsertTarget · clearPreviews
Misc
hidden · tooltipEntries · destroy · isDestroyed · view
PixiGraphElement
Returned from graph.element(), graph.nodes(), etc. — never constructed directly.
- Identity:
id·group·isNode·isEdge·data(key?, value?)·bbox·position·rotation·shape·polygonPoints·image - Capabilities:
selectable·resizable·rotatable·movable·handleMode - Classes & state:
addClass·removeClass·toggleClass·hasClass·classes·select·unselect·selected - Highlights:
highlights·hasHighlight - Graph traversal (edges):
source·target·sourcePoint·targetPoint·connectedEdges
Most setters are chainable: ele.addClass('on').data('label', 'x').select();
Exports
import {
PixiGraph, PixiGraphElement,
PixiGraphViewport, PixiGraphHistory, PixiGraphPreview,
PixiGraphEventBus, HighlightManager,
StyleEngine, parseSelector, matchesSelector,
ptSegDist, projectOnSeg,
DEFAULT_PIXIGRAPH_STYLE,
} from '@leebhin/pixigraph';
// Types
import type {
PixiGraphConfig, PixiGraphAddInput, PixiGraphNodeInput, PixiGraphEdgeInput,
PixiGraphBaseStyle, PixiGraphStyleProps, PixiGraphStyleRule, ParsedSelector,
PixiGraphEventType, PixiGraphFeedType, PixiGraphEventPayload, PixiGraphHandler,
PixiGraphHighlightInput, PixiGraphViewportConfig, PanOptions, ViewportTween,
ElementGroup, GraphBbox, GraphPoint,
} from '@leebhin/pixigraph';Cleanup
graph.destroy(); // detach handlers, cancel tweens, free image textures
graph.isDestroyed();How It Works
- The graph renders into a Pixi
Container(graph.view) split into stacked sublayers — edges below nodes below handles — so z-order is correct without per-element sorting. - With the viewport active,
graph.viewis the camera's outer container and content lives in an inner world container whose transform is the pan/zoom matrix. - All input is graph-local. You convert screen → graph-local (
container.toLocal) andfeed()it; the graph never registers DOM listeners itself, which keeps it embeddable and SSR-/test-friendly. feed('mousemove', …)tracks the hovered element and emitsmouseover/mouseoutautomatically when it changes.- Effective style = group defaults → matching selector rules (specificity, then declaration order) → highlight-group overrides → system classes (
.dim,.focus-dim,.hidden). Only the affected elements re-render on a state change. - Edges clip to node surfaces and bow apart when parallel; arrowheads are placed precisely even where geometry overlaps.
- Image nodes load through Pixi
Assets, cached per URL so N nodes sharing an image cost one fetch; a fallback fill shows until the texture resolves. - Handles and the selection box are drawn at a screen-stable size by inversely scaling with zoom (
setViewScale/ viewport), so they don't grow or shrink as you zoom.
Building From Source
npm run build # dist/ — ESM + CJS + .d.ts (via tsup)
npm run typecheck # tsc --noEmitLicense
MIT © LeeBhin
