@loradb/lora-graph-canvas
v0.11.4
Published
React graph canvas for LoraDB. Unified 2D/3D force-directed component with a built-in tool palette for adding, connecting, selecting, and removing nodes.
Maintainers
Readme
@loradb/lora-graph-canvas
React graph canvas for LoraDB. One component, <LoraGraphCanvas />, that
wraps force-graph (2D HTML5 canvas) and 3d-force-graph (WebGL via
Three.js). Switch between 2D and 3D at runtime without re-mounting your
data, and use the built-in tool palette to build, edit, select, and
delete nodes interactively.
import { LoraGraphCanvas } from "@loradb/lora-graph-canvas";
import "@loradb/lora-graph-canvas/styles.css";
<LoraGraphCanvas
defaultData={{
nodes: [{ id: "a" }, { id: "b" }],
links: [{ source: "a", target: "b" }],
}}
nodeLabel="id"
nodeAutoColorBy="group"
/>;Install
yarn add @loradb/lora-graph-canvas three
# or
npm install @loradb/lora-graph-canvas threethree is a required peer dependency — the package itself does not bundle
Three.js, so 2D-only consumers can dedupe with the rest of the app's
Three usage, and 3D consumers can pin a specific version.
Built-in tools
The toolbar ships with twelve tools, controllable individually:
| Tool | Shortcut | What it does |
| ------------ | -------- | -------------------------------------------------- |
| Select | V | Click / shift-click nodes to select them. |
| Pan | H | Pan-only cursor. |
| Add node | N | Click on the canvas to drop a node. |
| Add link | L | Click two nodes to connect them. |
| Delete | ⌫ / ⌦ | Delete the current selection (cascades links). |
| Fit | F | zoomToFit to the current graph bbox. |
| Zoom in/out | + - | Step the zoom level. |
| Pause/Resume | — | Stop / start the d3 simulation. |
| Screenshot | — | Download a PNG of the canvas. |
| Toggle 2D/3D | 3 | Swap engines, preserving data and selection. |
// Pick a subset…
<LoraGraphCanvas tools={["select", "add-node", "delete", "toggle-mode"]} />
// …or hide the whole bar and drive everything from the ref:
<LoraGraphCanvas tools={false} ref={ref} />Ref handle
import { useRef } from "react";
import {
LoraGraphCanvas,
type LoraGraphCanvasHandle,
} from "@loradb/lora-graph-canvas";
const ref = useRef<LoraGraphCanvasHandle>(null);
ref.current?.addNode({ id: "x", label: "hello" });
ref.current?.addLink({ source: "x", target: "y" });
ref.current?.removeNode("x"); // also removes attached links
ref.current?.fit(400, 40);
ref.current?.setMode("3d");
const blob = await ref.current?.screenshot();Available methods: data (getData, setData, addNode, addNodes,
updateNode, removeNode, removeNodes, addLink, addLinks,
removeLink, clear); selection (getSelection, setSelection,
selectAll, clearSelection); view (getMode, setMode, fit,
centerAt, zoom, zoomIn, zoomOut); engine (pause, resume,
reheat, screenshot); escape hatches (engine2D, engine3D).
Controlled vs uncontrolled
// Uncontrolled — internal state owns the graph, host gets a notification:
<LoraGraphCanvas defaultData={initialData} onDataChange={save} />
// Controlled — host owns the graph:
<LoraGraphCanvas data={dataFromState} onDataChange={setDataState} />The same dichotomy applies to mode (defaultMode vs mode).
Theming
The chrome (toolbar, context menu, tooltips) is driven by --lgc-* CSS
variables. Override the ones you want either through the theme prop or
by attaching CSS to the .lora-graph-canvas container. The engine's own
canvas reads backgroundColor from a regular prop, independent of the
theme.
import { LoraGraphCanvas, darkTheme } from "@loradb/lora-graph-canvas";
<LoraGraphCanvas
backgroundColor="#0e1014"
theme={{ ...darkTheme, accent: "#ff6699" }}
/>;Two presets are exported: lightTheme and darkTheme.
Confirm-before-delete
Gate every node / link removal — keyboard, toolbar, context menu,
selection panel, cut, and the imperative removeNode / removeLink
handle methods — through an async guard:
<LoraGraphCanvas
onBeforeNodeDelete={(nodes, { source }) =>
source === "imperative"
? true // trust your own code
: openMyConfirmDialog(nodes) // returns Promise<boolean>
}
onBeforeLinkDelete={(links) => openMyConfirmDialog([], links)}
onNodeDeleted={(nodes, { source }) => analytics.track("nodes.removed", {
n: nodes.length,
source,
})}
/>The guard receives every item in the batch (one selection-wide call,
not per-item), the originating source ("keyboard" | "toolbar" |
"contextMenu" | "selectionPanel" | "cut" | "imperative"), and may
return either a boolean or a Promise<boolean>. A thrown error is
treated as a cancel — your host won't silently destroy data.
When no guard is wired, deletion happens immediately as before; the
imperative methods become Promise<boolean> but resolve on the same
tick.
Performance knobs
For large graphs, cap cooldownTicks (default ∞) and increase
warmupTicks to spend more time computing layout off-screen before the
first paint:
<LoraGraphCanvas cooldownTicks={50} warmupTicks={20} />For graphs above ~10k nodes, switch the layout engine in 3D mode:
<LoraGraphCanvas mode="3d" forceEngine="ngraph" />Opt-in UX flags
Each is a single boolean prop:
| Prop | What it does |
| --- | --- |
| focusOnClick | Click a node → animated camera focus; click again to restore. |
| highlightNeighborsOnHover | Hover a node → it + its neighbours light up in the accent color. Pair with autoIndexNeighbors so the component builds the neighbour index for you, or stash _neighbors / _links yourself. |
| autoIndexNeighbors | After every data change, build _neighbors and _links arrays on each node — required by the highlight flag if you don't provide them. |
| collideNodes | Inject d3-force-3d's forceCollide so circles don't overlap. Pass a number to override the radius. |
| showGrid | Faint background grid that adapts to the zoom level. 2D only. Pass { spacing, color } to customise. |
| showLegend | Bottom-left widget enumerating nodeAutoColorBy groups with a colour swatch; click a group to toggle its visibility (drives nodeVisibility automatically). |
| enableRename | (Default true.) Double-click a node to rename it inline. Set false to suppress. |
| enableClipboard | (Default true.) ⌘C / ⌘X / ⌘V keybindings + matching ref methods. |
<LoraGraphCanvas
data={graph}
nodeAutoColorBy="group"
focusOnClick
highlightNeighborsOnHover
autoIndexNeighbors
collideNodes
showGrid
showLegend
/>Animated particles for events
The kapsule emits an animated particle along a link via emitParticle.
Wire it through the ref to visualise events (data flow, message sent,
etc.):
const ref = useRef<LoraGraphCanvasHandle>(null);
useEffect(() => {
socket.on("message", (msg) => {
const link = graph.links.find((l) => l.id === msg.linkId);
if (link) ref.current?.emitParticle(link);
});
}, []);Custom forces
Inject d3-force primitives through d3Force(name, fn):
import { forceRadial } from "d3-force-3d";
ref.current?.d3Force("radial", forceRadial(200));
ref.current?.reheat();License
BUSL-1.1. Third-party attributions for force-graph and 3d-force-graph
(MIT, © Vasco Asturiano) live in THIRD_PARTY.md.
