A standalone web component for visualizing FalkorDB graphs using force-directed layouts.
- 🎨 Force-directed graph layout - Automatic positioning using D3 force simulation with smart collision detection
- 🧭 Multiple layout modes - Switch between
force,tree,flow,radial-tree,concentric,components, andarcgraph views - 🎯 Interactive - Click, hover, right-click interactions on nodes, links, and background
- 🌓 Theme support - Light and dark mode compatible with customizable colors
- ⚡ Performance - Optimized rendering with HTML5 canvas, including viewport culling and low-zoom draw skipping for large graphs
- 💫 Loading states - Built-in skeleton loading with pulse animation
- 🎨 Customizable - Colors, sizes, behaviors, and custom rendering functions
- 📦 TypeScript support - Full type definitions included
- 🔧 Web Component - Works with any framework or vanilla JavaScript
- 🎮 Viewport control - Zoom, pan, and auto-fit functionality
- 🔄 Smart layout - Adaptive force algorithm based on node connectivity
npm install @falkordb/canvas<!DOCTYPE html>
<html>
<head>
<title>FalkorDB Canvas Example</title>
</head>
<body>
<falkordb-canvas id="graph" style="width: 100%; height: 600px;"></falkordb-canvas>
<script type="module">
import '@falkordb/canvas';
const canvas = document.getElementById('graph');
// Set data
canvas.setData({
nodes: [
{ id: 1, labels: ['Person'], color: '#FF6B6B', visible: true, data: { name: 'Alice' } },
{ id: 2, labels: ['Person'], color: '#4ECDC4', visible: true, data: { name: 'Bob' } }
],
links: [
{ id: 1, relationship: 'KNOWS', color: '#999', source: 1, target: 2, visible: true, data: {} }
]
});
// Configure
canvas.setConfig({
width: 800,
height: 600,
backgroundColor: '#FFFFFF',
foregroundColor: '#1A1A1A',
onNodeClick: (node) => console.log('Clicked:', node)
});
</script>
</body>
</html>import { useEffect, useRef } from 'react';
import '@falkordb/canvas';
import type { FalkorDBCanvas, Data, GraphNode } from '@falkordb/canvas';
function GraphVisualization() {
const canvasRef = useRef<FalkorDBCanvas>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const data: Data = {
nodes: [
{ id: 1, labels: ['Person'], color: '#FF6B6B', visible: true, data: { name: 'Alice' } },
{ id: 2, labels: ['Person'], color: '#4ECDC4', visible: true, data: { name: 'Bob' } }
],
links: [
{ id: 1, relationship: 'KNOWS', color: '#999', source: 1, target: 2, visible: true, data: {} }
]
};
canvas.setData(data);
canvas.setConfig({
onNodeClick: (node: GraphNode) => {
console.log('Clicked node:', node);
}
});
}, []);
return (
<falkordb-canvas
ref={canvasRef}
style={{ width: '100%', height: '600px' }}
/>
);
}| Method | Default | Description |
|---|---|---|
| setData(data) | Set the graph data (nodes and links). Automatically triggers layout simulation and loading states. | |
| getData() | Get the current graph data in the simplified format. | |
| setGraphData(data) | Set graph data in the internal format (with computed properties). Use this for better performance when you already have GraphData format. | |
| getGraphData() | Get the current graph data in the internal format with all computed properties (x, y, vx, vy, etc.). | |
| setConfig(config) | Configure the graph visualization and behavior. Accepts a ForceGraphConfig object with styling, callbacks, and rendering options. |
|
| setWidth(width) | Set canvas width in pixels. | |
| setHeight(height) | Set canvas height in pixels. | |
| setBackgroundColor(color) | Set background color (hex or CSS color). | |
| setForegroundColor(color) | Set foreground color for text and borders. | |
| setIsLoading(isLoading) | Show/hide loading skeleton. | |
| setCooldownTicks(ticks) | Set simulation ticks before stopping (undefined = infinite). | |
| setDebug(enabled) | false |
Enable or disable debug logging to console. All log messages are prefixed with [FalkorDBCanvas]. |
| getViewport() | Get current zoom and center position as ViewportState. |
|
| setViewport(viewport) | Restore a previously saved viewport state. | |
| getZoom() | Get current zoom level. | |
| zoom(zoomLevel) | Set zoom level. | |
| zoomToFit(paddingMultiplier, filter) | 1.0, undefined |
Auto-fit all visible nodes in view. Optional padding multiplier and node filter function. |
| getGraph() | Get the underlying force-graph instance for advanced control. |
| Option | Default | Description |
|---|---|---|
width |
<window width> |
Canvas width in pixels |
height |
<window height> |
Canvas height in pixels |
backgroundColor |
Background color (hex or CSS color) | |
foregroundColor |
Foreground color for borders and text | |
layoutMode |
force |
Layout algorithm to use: force | tree | flow | radial-tree | concentric | components | arc |
layoutOptions |
{} |
Per-layout options for each mode: tree, flow, radialTree, concentric, components, and arc |
cooldownTicks |
undefined |
Number of simulation ticks before stopping (undefined = infinite) |
cooldownTime |
1000 |
Time in ms for each simulation tick |
autoStopOnSettle |
true |
Automatically stop simulation when settled |
isLoading |
false |
Show/hide loading skeleton |
onNodeClick |
Callback when a node is clicked. Signature: (node: GraphNode, event: MouseEvent) => void |
|
onNodeRightClick |
Callback when a node is right-clicked. Signature: (node: GraphNode, event: MouseEvent) => void |
|
onLinkClick |
Callback when a link is clicked. Signature: (link: GraphLink, event: MouseEvent) => void |
|
onLinkRightClick |
Callback when a link is right-clicked. Signature: (link: GraphLink, event: MouseEvent) => void |
|
onNodeHover |
Callback when hovering over a node. Signature: (node: GraphNode | null) => void |
|
onLinkHover |
Callback when hovering over a link. Signature: (link: GraphLink | null) => void |
|
onBackgroundClick |
Callback when clicking the background. Signature: (event: MouseEvent) => void |
|
onBackgroundRightClick |
Callback when right-clicking the background. Signature: (event: MouseEvent) => void |
|
onZoom |
Callback when zoom/pan changes. Signature: (transform: Transform) => void |
|
onEngineStop |
Callback when the force simulation stops. Signature: () => void |
|
onLoadingChange |
Callback when loading state changes. Signature: (loading: boolean) => void |
|
isNodeSelected |
Function to determine if a node is selected. Signature: (node: GraphNode) => boolean |
|
isLinkSelected |
Function to determine if a link is selected. Signature: (link: GraphLink) => boolean |
|
node |
Custom node rendering functions (see Custom Rendering) | |
link |
Custom link rendering functions (see Custom Rendering) | |
largeGraph |
Large-graph rendering optimizations (see Large-Graph Optimizations) |
Use layoutMode in setConfig to choose the graph view style:
canvas.setConfig({
layoutMode: 'flow',
layoutOptions: {
flow: {
direction: 'LR', // 'LR' | 'RL' | 'TB' | 'BT'
layerSpacing: 180,
nodeSpacing: 110
}
}
});Tree / radial tree example:
canvas.setConfig({
layoutMode: 'radial-tree',
layoutOptions: {
radialTree: {
rootNodeId: 1,
direction: 'TB',
radiusStep: 130
}
}
});Concentric / components / arc examples:
canvas.setConfig({
layoutMode: 'concentric',
layoutOptions: {
concentric: {
metric: 'degree', // 'degree' | 'inDegree' | 'outDegree' | 'bfsDepth'
ringSpacing: 130
},
components: {
innerLayout: 'concentric', // 'concentric' | 'tree' | 'flow' | 'radial-tree'
maxColumns: 3
},
arc: {
direction: 'LR', // 'LR' | 'RL'
orderBy: 'degree', // 'id' | 'label' | 'degree'
curveScale: 0.22
}
}
});Notes:
forcekeeps simulation enabled.- All non-
forcelayouts compute deterministic target positions, animate layout transitions, and support drag interactions while preserving layout structure. componentsuses its owninnerLayoutstrategy for each disconnected subgraph and arranges components in a tiled view.tree:rootNodeId,direction,levelSpacing,nodeSpacing,componentSpacing.flow:direction,layerSpacing,nodeSpacing,componentSpacing.radialTree:rootNodeId,direction,startAngle,endAngle,radiusStep,componentSpacing.concentric:metric,rootNodeId,ringSpacing,minRingNodeSpacing,sortWithinRing.components:innerLayout,componentGap,maxColumns,sortComponentsBy.arc:orderBy,direction,nodeSpacing,curveScale.
| Property | Default | Description |
|---|---|---|
id |
required | Unique identifier for the node |
labels |
required | Array of label names for the node |
color |
required | Node color (hex or CSS color) |
visible |
required | Whether the node is visible |
size |
6 |
Node radius |
caption |
'id' |
Property key to use from the data for display text |
data |
required | Node properties as key-value pairs |
| Property | Default | Description |
|---|---|---|
id |
required | Unique identifier for the link |
relationship |
required | Label displayed on the link |
color |
required | Link color (hex or CSS color) |
source |
required | Source node ID |
target |
required | Target node ID |
visible |
required | Whether the link is visible |
data |
required | Link properties as key-value pairs |
Internal format with computed properties:
{
...Node;
size: number; // Always present (defaults to 6)
displayName: [string, string]; // Computed text lines
x?: number; // Position from simulation
y?: number;
vx?: number; // Velocity
vy?: number;
fx?: number; // Fixed position
fy?: number;
}Internal format with resolved node references:
{
...Link;
source: GraphNode; // Resolved node object
target: GraphNode; // Resolved node object
curve: number; // Computed curvature for rendering
}{
zoom: number;
centerX: number;
centerY: number;
} | undefined{
k: number; // zoom scale
x: number; // pan x
y: number; // pan y
}You can provide custom rendering functions for nodes and links:
canvas.setConfig({
node: {
nodeCanvasObject: (node: GraphNode, ctx: CanvasRenderingContext2D) => {
// Custom node drawing logic
ctx.fillStyle = node.color;
ctx.fillRect(node.x! - 5, node.y! - 5, 10, 10);
},
nodePointerAreaPaint: (node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => {
// Define clickable area
ctx.fillStyle = color;
ctx.fillRect(node.x! - 5, node.y! - 5, 10, 10);
}
},
link: {
linkCanvasObject: (link: GraphLink, ctx: CanvasRenderingContext2D) => {
// Custom link drawing logic
},
linkPointerAreaPaint: (link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => {
// Define clickable area for link
}
}
});The package exports utility functions for data manipulation:
import {
dataToGraphData,
graphDataToData,
getNodeDisplayText,
getNodeDisplayKey,
wrapTextForCircularNode
} from '@falkordb/canvas';
// Convert between formats
const graphData = dataToGraphData(data);
const data = graphDataToData(graphData);
// Get display text for a node
const text = getNodeDisplayText(node); // Returns node.data[caption] or defaults to id
// Wrap text for circular nodes
const [line1, line2] = wrapTextForCircularNode(ctx, text, radius);# Install dependencies
npm install
# Build (TypeScript compilation)
npm run build
# Watch mode (auto-rebuild on changes)
npm run dev
# Run example server
npm run example
# Then open http://localhost:8080/examples/falkordb-canvas.example.html
# Lint code
npm run lint
# Clean build artifacts
npm run cleanThe component supports HTML attributes for render modes:
<falkordb-canvas
node-mode="replace" <!-- 'before' | 'after' | 'replace' -->
link-mode="after"> <!-- 'before' | 'after' | 'replace' -->
</falkordb-canvas>replace(default for nodes): Uses custom rendering exclusivelybefore: Renders custom content before default renderingafter(default for links): Renders custom content after default rendering
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
Requires support for:
- Web Components (Custom Elements)
- ES Modules
- Shadow DOM
- HTML5 Canvas
The canvas element inside the web component's shadow DOM exposes a data-engine-status attribute that indicates whether the force simulation is currently running or stopped. This is useful for automated testing to wait for the canvas to finish animating.
Values:
"running"- Force simulation is actively running"stopped"- Force simulation has stopped (animation complete)
Example usage with Playwright:
// Wait for canvas to be ready
const canvasElement = page.locator("falkordb-canvas").locator("canvas").first();
await canvasElement.waitFor({ state: "attached" });
// Poll until animation completes
while (true) {
const status = await canvasElement.getAttribute("data-engine-status");
if (status === "stopped") break;
await page.waitForTimeout(500);
}Note: The attribute is set on the <canvas> element within the shadow DOM, not on the <falkordb-canvas> web component itself.
- Large graphs: Use
cooldownTicksto limit simulation iterations - Static graphs: Set
cooldownTicks: 0after initial layout - Custom rendering: Optimize your custom
nodeCanvasObjectandlinkCanvasObjectfunctions - Viewport: Use
getViewport()andsetViewport()to preserve user's view when updating data - Very large graphs: Enable viewport culling via the
largeGraphoption (see below)
For graphs with thousands of nodes and links, enable the built-in viewport culling and low-zoom draw-skipping optimizations via the largeGraph configuration option.
Note: These optimizations are applied by the default renderer. If you provide custom
nodeCanvasObject/linkCanvasObjectcallbacks, the library will not cull or skip drawing for those elements automatically. Implement equivalent viewport culling and low-zoom checks in your custom renderer if needed.
canvas.setConfig({
largeGraph: {
enabled: true, // master switch (false by default – no behavioural change unless true)
viewportPadding: 50, // world-unit padding around the visible viewport (default: 0)
lowZoomThreshold: 0.5, // zoom level below which expensive details are skipped
skipLabelsAtLowZoom: true, // skip node labels at low zoom (default: true)
skipArrowsAtLowZoom: true, // skip link arrowheads at low zoom (default: true)
skipLinkLabelsAtLowZoom: true, // skip link relationship labels at low zoom (default: true)
}
});Viewport culling – before each node or link is drawn, the renderer checks whether its world-space bounding box overlaps the currently visible area (with optional extra viewportPadding). Elements that are entirely offscreen are skipped without any canvas work.
- For nodes: the bounding box is a circle of radius
node.size + 2. - For links: the bounding box is the convex-hull axis-aligned rectangle of the Bezier curve's control points (source, control point, target). This is a conservative bound – it never produces false negatives.
- For self-loops: the bounding box is a square centered on the node with a side length derived from the loop curvature.
Low-zoom draw skipping – when the current zoom level drops below lowZoomThreshold, expensive per-element details that would be too small to read are skipped:
- Node labels (text inside nodes) – controlled by
skipLabelsAtLowZoom - Link arrowheads – controlled by
skipArrowsAtLowZoom - Link relationship labels – controlled by
skipLinkLabelsAtLowZoom
Node and link shapes are always drawn so the overall graph structure remains visible.
// Good starting point for graphs with 1,000 – 10,000+ elements
canvas.setConfig({
cooldownTicks: 300, // limit physics simulation ticks
largeGraph: {
enabled: true,
viewportPadding: 100, // pre-render elements slightly off-screen to avoid pop-in
lowZoomThreshold: 0.4, // tune to match your typical minimum zoom
}
});The feature is disabled by default (enabled: false / property absent). Existing code that does not set largeGraph is completely unaffected.
The canvas component includes a debug logging system that can be enabled to help troubleshoot issues or understand the internal behavior of the graph visualization.
const canvas = document.getElementById('graph');
// Enable debug logging
canvas.setDebug(true);
// Disable debug logging
canvas.setDebug(false);When debug mode is enabled, the canvas will log detailed information to the console including:
- Component lifecycle events (connected, disconnected)
- Data updates (setData, setGraphData)
- Configuration changes (colors, dimensions, render modes)
- Force simulation state (initialization, setup, engine stop)
- Viewport operations (zoom, pan, fit)
- Loading state transitions
- Node degree calculations
- Canvas resizing
All debug messages are prefixed with [FalkorDBCanvas] for easy filtering.
const canvas = document.getElementById('graph');
canvas.setDebug(true);
canvas.setData({
nodes: [{ id: 1, labels: ['Person'], color: '#FF6B6B', visible: true, data: { name: 'Alice' } }],
links: []
});
// Console output:
// [FalkorDBCanvas] Debug mode enabled
// [FalkorDBCanvas] setData called with 1 nodes and 0 links
// [FalkorDBCanvas] Loading state: true
// [FalkorDBCanvas] Initializing graph
// [FalkorDBCanvas] Calculating node degrees for 1 nodes
// [FalkorDBCanvas] Setting up force simulation
// ... and more- Enable debug mode early in development to understand component behavior
- Use browser console filtering (
[FalkorDBCanvas]) to focus on canvas logs - Debug mode has minimal performance impact but should be disabled in production
- Combine with browser DevTools to inspect graph state and performance
See the examples directory for complete working examples including:
- Basic usage
- Custom node/link rendering
- Event handling
- Dynamic data updates
- Theme switching
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request