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, andradialgraph 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',
eventHandlers: {
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({
eventHandlers: {
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. | |
| setAnimation(enabled) | Enable or disable force simulation animation. When disabled, pins all nodes in place. | |
| setPinOnDragEnd(pin) | Enable or disable pinning nodes after dragging. | |
| setLayout(layoutMode) | 'force' |
Switch layout mode: 'force' | 'tree' | 'radial'. |
| setLayoutOptions(options) | Update per-layout options (tree, radial, force). Triggers re-layout. | |
| setDebug(enabled) | false |
Enable or disable debug logging to console. All log messages are prefixed with [FalkorDBCanvas]. |
| refresh() | Trigger a repaint after in-place property mutations (visibility, color, size). Recomputes positions for deterministic layouts. | |
| 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. | |
| getCullingStats() | Get viewport culling statistics (bounds, visible vs total node/link counts). |
Configuration is passed to setConfig() as a ForceGraphConfig object. It is organized into sub-configs:
| Option | Default | Description |
|---|---|---|
width |
<window width> |
Canvas width in pixels |
height |
<window height> |
Canvas height in pixels |
backgroundColor |
'#FFFFFF' |
Background color (hex or CSS color) |
foregroundColor |
'#1A1A1A' |
Foreground color for borders and text |
layoutMode |
'force' |
Layout algorithm: 'force' | 'tree' | 'radial' |
layoutOptions |
{} |
Per-layout options (see Layout Modes) |
animation |
Enable/disable layout animation | |
captionsKeys |
[] |
Node property keys to display as labels |
showPropertyKeyPrefix |
false |
Show property key prefix in node labels |
pinOnDragEnd |
false |
Pin nodes after dragging |
isNodeSelected |
Function: (node: GraphNode) => boolean |
|
isLinkSelected |
Function: (link: GraphLink) => boolean |
|
linkLineDash |
Function: (link: GraphLink) => number[] |
|
node |
Custom node rendering (see Custom Rendering) | |
link |
Custom link rendering (see Custom Rendering) | |
largeGraph |
Large-graph optimizations (see Large-Graph Optimizations) |
| Option | Default | Description |
|---|---|---|
fontFamily |
'SofiaSans' |
Font family for node labels |
fontWeightUnselected |
400 |
Font weight when not selected |
fontWeightSelected |
700 |
Font weight when selected |
fontSize |
2 |
Fixed font size (world units). Used when textFillRatio is 0 or unset |
textFillRatio |
0.85 |
Auto-size: fraction of node radius that text fills (0–1). Takes precedence over fontSize |
strokeWidthSelected |
1 |
Border width when selected |
strokeWidthUnselected |
0.5 |
Border width when not selected |
glowDuration |
10000 |
Glow effect duration (ms) after expand/collapse |
glowSpread |
12 |
Glow spread radius (px) |
glowSteps |
16 |
Number of gradient rings in glow |
glowColor |
[59, 130, 246] |
Glow color as [r, g, b] |
glowMaxOpacity |
0.6 |
Maximum glow opacity (0–1) |
| Option | Default | Description |
|---|---|---|
fontFamily |
'SofiaSans' |
Font family for link labels |
fontWeightUnselected |
400 |
Font weight when not selected |
fontWeightSelected |
700 |
Font weight when selected |
arrowWidthRatio |
1.6 |
Arrow width-to-height ratio |
arrowNotchRatio |
0.2 |
Arrow notch depth ratio |
selfLoopCurveFactor |
11.67 |
Self-loop curve factor |
parallelEdgeCurveMultiplier |
0.4 |
Parallel edge curve multiplier |
labelBackgroundPadding |
0.3 |
Label background padding (world units) |
edgeGap |
2 |
Gap between edge tip and visible node border (px) |
| Option | Default | Description |
|---|---|---|
centerStrength |
0.03 |
Center force strength (X and Y) |
chargeStrength |
-400 |
Charge repulsion strength |
velocityDecay |
0.4 |
Velocity decay (damping) |
alphaMin |
0.05 |
Alpha min (convergence threshold) |
warmupTicks |
300 |
Warmup ticks for force simulation |
| Option | Default | Description |
|---|---|---|
tooltipFontSize |
12 |
Tooltip font size (px) |
tooltipPadding |
'4px 8px' |
Tooltip padding CSS value |
tooltipBorderRadius |
'4px' |
Tooltip border radius CSS value |
tooltipZIndex |
1000 |
Tooltip z-index |
zoomToFitPadding |
0.1 |
Zoom-to-fit padding as fraction of smallest dimension |
zoomToFitDelay |
50 |
Delay (ms) before zoom-to-fit after layout change |
linkHitWidth |
10 |
Link pointer hit-test width (screen px) |
contrastThreshold |
0.5 |
Luminance threshold for text color contrast |
| Option | Description |
|---|---|
onNodeClick |
(node: GraphNode, event: MouseEvent) => void |
onNodeRightClick |
(node: GraphNode, event: MouseEvent) => void |
onLinkClick |
(link: GraphLink, event: MouseEvent) => void |
onLinkRightClick |
(link: GraphLink, event: MouseEvent) => void |
onNodeHover |
(node: GraphNode | null) => void |
onNodeDragEnd |
(node: GraphNode) => void |
onPinChange |
(pinned: boolean) => void |
onLinkHover |
(link: GraphLink | null) => void |
onBackgroundClick |
(event: MouseEvent) => void |
onBackgroundRightClick |
(event: MouseEvent) => void |
onZoom |
(transform: Transform) => void |
onEngineStop |
() => void |
onLayoutChange |
(layout: LayoutMode) => void |
Use layoutMode in setConfig to choose the graph view style:
canvas.setConfig({
layoutMode: 'tree',
layoutOptions: {
tree: {
direction: 'lr', // 'lr' | 'rl' | 'td' | 'bu'
levelDistance: 180,
nodeSpacing: 110
}
}
});Radial example:
canvas.setConfig({
layoutMode: 'radial',
layoutOptions: {
radial: {
direction: 'out', // 'out' | 'in'
levelDistance: 130
}
}
});Notes:
forcekeeps simulation enabled with configurable physics (seesimulationconfig).tree:direction('td'|'bu'|'lr'|'rl'),levelDistance,nodeSpacing.radial:direction('out'|'in'),levelDistance,nodeSpacing.
| 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 |
9 |
Node radius (world units) |
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 |
width |
1 |
Line width in screen pixels (stays constant while zooming) |
fontSize |
6 |
Relationship caption font size (world units) |
arrowSize |
8 |
Arrowhead length (world units) |
data |
required | Link properties as key-value pairs |
width and arrowSize are doubled while the link is selected, so selected
links stay visually distinct.
Internal format with computed properties:
{
...Node;
size: number; // Always present (defaults to 9)
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: Disable animation (
setAnimation(false)) once the layout stabilizes - Static graphs: Use a deterministic layout (
setLayout('tree')) to avoid simulation overhead - 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 (true by default)
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({
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 enabled by default (enabled: true). The lowZoomThreshold defaults to 1 (always active) and viewport culling uses zero padding.
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