@falkordb/canvas
v0.3.3
Published
A standalone web component for visualizing FalkorDB graphs using force-directed layouts
Readme
FalkorDB Canvas
A standalone web component for visualizing FalkorDB graphs using force-directed layouts.
Features
- 🎨 Force-directed graph layout - Automatic positioning using D3 force simulation with smart collision detection
- 🧭 Multiple layout modes - Switch between
force,tree, andradialgraph views - 🧊 2D or 3D - Show the graph flat on a canvas or in 3D with WebGL; three.js is only loaded when 3D is first used
- 🎯 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
Installation
npm install @falkordb/canvasQuick Start
Vanilla JavaScript
<!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>React / TypeScript
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' }}
/>
);
}API
Methods
| 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. |
| setDimension(dimension) | '2d' | Show the graph in '2d' or '3d'. Returns a promise that settles once the switch is done (see 2D and 3D). |
| getDimension() | | The dimension the graph is shown in, or is being switched to. |
| 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. After the element is disconnected, returns the viewport it was left at, so a host can save it while unmounting. 2D only: in 3D it returns undefined, connected or not. |
| 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 Options
Configuration is passed to setConfig() as a ForceGraphConfig object. It is organized into sub-configs:
Top-Level Options
| 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 |
| dimension | '2d' | Render in '2d' or '3d' (see 2D and 3D) |
| layoutMode | 'force' | Layout algorithm: 'force' | 'tree' | 'radial' |
| layoutOptions | {} | Per-layout options (see Layout Modes) |
| animation | | Enable/disable layout animation |
| captionsKeys | [] | Ordered node property keys to display as labels. Each entry is a plain string (fuzzy, case-insensitive match) or a [key, exactMatch] tuple — e.g. ['name', ['Title', true]]. Falls back to the node ID if none match |
| 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) |
nodeStyle — Node Visual Style
| 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) |
linkStyle — Link/Edge Visual Style
| 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) |
simulation — Force Simulation Tuning
| 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 |
interaction — Interaction / UX Parameters
| 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 |
eventHandlers — Event Callbacks
| 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 |
| onDimensionChange | (dimension: CanvasDimension) => void — fires once the canvas shows the new dimension |
Layout Modes
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.
2D and 3D
The graph is drawn flat on an HTML5 canvas by default. Switch to 3D to explore it in space, rendered with WebGL by 3d-force-graph:
await canvas.setDimension('3d'); // or canvas.setConfig({ dimension: '3d' })
await canvas.setDimension('2d');The 3D renderer and three.js are loaded the first time 3D is used, so pages that stay in 2D never download them. Until the switch completes the current view stays up; onDimensionChange fires when it is done. Called before the element is connected, setDimension only records the dimension and its promise settles at once: the graph is shown in it when the element renders. For '3d', onDimensionChange fires then; '2d' is how the canvas starts anyway, so no event fires. If 3D cannot be shown (the bundle will not load, or WebGL will not start), the promise rejects, the canvas stays in or falls back to 2D, and onDimensionChange('2d') fires.
Switching lays the graph out afresh in the new dimension and fits it to the view. The data, selection, dimming, colors, captions and event handlers carry over. In 3D:
- Drag to rotate, scroll to zoom, right-drag to pan.
zoom()andgetZoom()express the camera's distance as a zoom factor, sozoom(getZoom() * 1.1)zooms in as it does in 2D. treeandradialkeep their 2D positions, so face-on they read exactly as in 2D with no node over another, and each level sits one level distance further back than the one above it, the root nearest. Seen side-on the levels step back like stairs: a tree's levels each line up as one point, and a radial ring as one line. Both are pinned, so they hold still; picking a layout turns the view face-on again.- Pausing the animation or pinning stops a
forcelayout's simulation. - Nodes are spheres (cubes for
shape: 'square') with the caption underneath; selected nodes get an outline in their border color and selected links are drawn thicker. Captions are left to the hover tooltip above 1,000 nodes or links. - A dragged node stays where it is dropped when
pinOnDragEndis on. getViewport()returnsundefinedandsetViewport()does nothing, since a 2D zoom and center do not describe a 3D camera.getGraph()returnsundefined.- Custom
node/linkcanvas renderers,linkLineDash, the expand glow and thelargeGraphculling apply to 2D only.
Data Types
Node
| 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) |
| shape | 'circle' | Node outline: 'circle' or 'square' (a square uses the circle's bounding box) |
| caption | 'id' | Property key to use from the data for display text |
| data | required | Node properties as key-value pairs |
Link
| 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.
GraphNode
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;
}GraphLink
Internal format with resolved node references:
{
...Link;
source: GraphNode; // Resolved node object
target: GraphNode; // Resolved node object
curve: number; // Computed curvature for rendering
}ViewportState
{
zoom: number;
centerX: number;
centerY: number;
} | undefinedTransform
{
k: number; // zoom scale
x: number; // pan x
y: number; // pan y
}Custom Rendering
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
}
}
});Utility Functions
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);Development
This package lives in web-components/canvas of the falkordb-ui repository. It was previously developed in FalkorDB/falkordb-canvas, and its history was carried over. Run the commands below from this folder, after running npm install once at the repository root:
# Install dependencies (from the repository root)
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
# Run the tests (Vitest + jsdom)
npm test
# Clean build artifacts
npm run cleanWeb Component Attributes
The 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
Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
Requires support for:
- Web Components (Custom Elements)
- ES Modules
- Shadow DOM
- HTML5 Canvas
Testing & Automation
Engine Status Indicator
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.
Performance Tips
- 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)
Large-Graph Optimizations
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)
}
});How it works
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.
Recommended settings for large graphs
// 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
}
});Backward compatibility
The feature is enabled by default (enabled: true). The lowZoomThreshold defaults to 1 (always active) and viewport culling uses zero padding.
Debugging
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.
Enable Debug Mode
const canvas = document.getElementById('graph');
// Enable debug logging
canvas.setDebug(true);
// Disable debug logging
canvas.setDebug(false);Debug Output
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.
Example Debug Session
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 moreTips for Debugging
- 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
Examples
See the examples directory for complete working examples including:
- Basic usage
- Custom node/link rendering
- Event handling
- Dynamic data updates
- Theme switching
Links
License
MIT
Contributing
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
