@falkordb/canvas 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FalkorDB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # FalkorDB Canvas
2
+
3
+ A standalone web component for visualizing FalkorDB graphs using force-directed layouts.
4
+
5
+ ## Features
6
+
7
+ - 🎨 **Force-directed graph layout** - Automatic positioning using D3 force simulation
8
+ - 🎯 **Interactive** - Click, hover, and right-click interactions
9
+ - 🌓 **Theme support** - Light and dark mode compatible
10
+ - âš¡ **Performance** - Optimized rendering with canvas
11
+ - 💀 **Loading states** - Built-in skeleton loading with pulse animation
12
+ - 🎨 **Customizable** - Colors, sizes, and behaviors
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install falkordb-canvas
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### HTML
23
+
24
+ ```html
25
+ <!DOCTYPE html>
26
+ <html>
27
+ <head>
28
+ <title>FalkorDB Canvas Example</title>
29
+ </head>
30
+ <body>
31
+ <falkordb-canvas id="graph" style="width: 100%; height: 600px;"></falkordb-canvas>
32
+
33
+ <script type="module">
34
+ import 'falkordb-canvas';
35
+
36
+ const canvas = document.getElementById('graph');
37
+
38
+ // Set data
39
+ canvas.setData({
40
+ nodes: [
41
+ { id: 1, labels: ['Person'], color: '#FF6B6B', visible: true, data: { name: 'Alice' } },
42
+ { id: 2, labels: ['Person'], color: '#4ECDC4', visible: true, data: { name: 'Bob' } }
43
+ ],
44
+ links: [
45
+ { id: 1, relationship: 'KNOWS', color: '#999', source: 1, target: 2, visible: true, data: {} }
46
+ ]
47
+ });
48
+
49
+ // Configure
50
+ canvas.setConfig({
51
+ width: 800,
52
+ height: 600,
53
+ backgroundColor: '#FFFFFF',
54
+ foregroundColor: '#1A1A1A',
55
+ onNodeClick: (node) => console.log('Clicked:', node)
56
+ });
57
+ </script>
58
+ </body>
59
+ </html>
60
+ ```
61
+
62
+ ## API
63
+
64
+ ### Methods
65
+
66
+ #### `setData(data: Data)`
67
+ Set the graph data (nodes and links).
68
+
69
+ ```typescript
70
+ canvas.setData({
71
+ nodes: [
72
+ { id: 1, labels: ['Person'], color: '#FF6B6B', visible: true, data: { name: 'Alice' } }
73
+ ],
74
+ links: [
75
+ { id: 1, relationship: 'KNOWS', color: '#999', source: 1, target: 2, visible: true, data: {} }
76
+ ]
77
+ });
78
+ ```
79
+
80
+ #### `getData(): Data`
81
+ Get the current graph data.
82
+
83
+ #### `setConfig(config: ForceGraphConfig)`
84
+ Configure the graph visualization and behavior.
85
+
86
+ ```typescript
87
+ canvas.setConfig({
88
+ width: 800,
89
+ height: 600,
90
+ backgroundColor: '#FFFFFF',
91
+ foregroundColor: '#1A1A1A',
92
+ displayTextPriority: [
93
+ { name: 'name', ignore: false },
94
+ { name: 'title', ignore: false }
95
+ ],
96
+ cooldownTicks: 300,
97
+ isLoading: false,
98
+ onNodeClick: (node, event) => {},
99
+ onNodeRightClick: (node, event) => {},
100
+ onLinkRightClick: (link, event) => {},
101
+ onNodeHover: (node) => {},
102
+ onLinkHover: (link) => {},
103
+ onBackgroundClick: (event) => {},
104
+ onEngineStop: () => {},
105
+ isNodeSelected: (node) => false,
106
+ isLinkSelected: (link) => false
107
+ });
108
+ ```
109
+
110
+ #### `getGraph(): ForceGraphInstance | undefined`
111
+ Get the underlying force-graph instance for advanced control.
112
+
113
+ ### Configuration Options
114
+
115
+ | Option | Type | Description |
116
+ |--------|------|-------------|
117
+ | `width` | `number` | Canvas width in pixels |
118
+ | `height` | `number` | Canvas height in pixels |
119
+ | `backgroundColor` | `string` | Background color (hex or CSS color) |
120
+ | `foregroundColor` | `string` | Foreground color for borders and text |
121
+ | `displayTextPriority` | `TextPriority[]` | Priority order for displaying node text |
122
+ | `cooldownTicks` | `number \| undefined` | Number of simulation ticks before stopping |
123
+ | `isLoading` | `boolean` | Show/hide loading skeleton |
124
+ | `onNodeClick` | `function` | Callback when a node is clicked |
125
+ | `onNodeRightClick` | `function` | Callback when a node is right-clicked |
126
+ | `onLinkRightClick` | `function` | Callback when a link is right-clicked |
127
+ | `onNodeHover` | `function` | Callback when hovering over a node |
128
+ | `onLinkHover` | `function` | Callback when hovering over a link |
129
+ | `onBackgroundClick` | `function` | Callback when clicking the background |
130
+ | `onEngineStop` | `function` | Callback when the force simulation stops |
131
+ | `isNodeSelected` | `function` | Function to determine if a node is selected |
132
+ | `isLinkSelected` | `function` | Function to determine if a link is selected |
133
+
134
+ ### Data Types
135
+
136
+ #### Node
137
+ ```typescript
138
+ {
139
+ id: number;
140
+ labels: string[];
141
+ color: string;
142
+ visible: boolean;
143
+ data: Record<string, any>;
144
+ }
145
+ ```
146
+
147
+ #### Link
148
+ ```typescript
149
+ {
150
+ id: number;
151
+ relationship: string;
152
+ color: string;
153
+ source: number; // Node ID
154
+ target: number; // Node ID
155
+ visible: boolean;
156
+ data: Record<string, any>;
157
+ }
158
+ ```
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ # Install dependencies
164
+ npm install
165
+
166
+ # Build
167
+ npm run build
168
+
169
+ # Watch mode
170
+ npm run dev
171
+
172
+ # Run example
173
+ npm run example
174
+ # Then open http://localhost:8080/examples/falkordb-canvas.example.html
175
+ ```
176
+
177
+ ## Browser Support
178
+
179
+ - Chrome/Edge (latest)
180
+ - Firefox (latest)
181
+ - Safari (latest)
182
+
183
+ ## License
184
+
185
+ MIT
186
+
187
+ ## Contributing
188
+
189
+ Contributions are welcome! Please feel free to submit a Pull Request.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@falkordb/canvas",
3
+ "version": "0.0.3",
4
+ "description": "A standalone web component for visualizing FalkorDB graphs using force-directed layouts",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./dist/*": "./dist/*"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist",
23
+ "dev": "tsc --watch",
24
+ "example": "python3 -m http.server 8080",
25
+ "clean": "rm -rf dist",
26
+ "lint": "eslint ."
27
+ },
28
+ "keywords": [
29
+ "falkordb",
30
+ "graph",
31
+ "visualization",
32
+ "web-component",
33
+ "force-directed",
34
+ "canvas"
35
+ ],
36
+ "author": "FalkorDB",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@types/react": "^19.2.7",
40
+ "d3": "^7.9.0",
41
+ "force-graph": "^1.44.4",
42
+ "react": "^19.2.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/d3": "^7.4.3",
46
+ "@typescript-eslint/eslint-plugin": "^8.18.2",
47
+ "@typescript-eslint/parser": "^8.18.2",
48
+ "eslint": "^9.17.0",
49
+ "tsup": "^8.5.1",
50
+ "typescript": "^5.9.3"
51
+ }
52
+ }
@@ -0,0 +1,90 @@
1
+ import { NodeObject } from "force-graph";
2
+
3
+ export interface ForceGraphConfig {
4
+ width?: number;
5
+ height?: number;
6
+ backgroundColor?: string;
7
+ foregroundColor?: string;
8
+ displayTextPriority?: TextPriority[];
9
+ onNodeClick?: (node: GraphNode, event: MouseEvent) => void;
10
+ onLinkClick?: (link: GraphLink, event: MouseEvent) => void;
11
+ onNodeRightClick?: (node: GraphNode, event: MouseEvent) => void;
12
+ onLinkRightClick?: (link: GraphLink, event: MouseEvent) => void;
13
+ onNodeHover?: (node: GraphNode | null) => void;
14
+ onLinkHover?: (link: GraphLink | null) => void;
15
+ onBackgroundClick?: (event: MouseEvent) => void;
16
+ onEngineStop?: () => void;
17
+ onLoadingChange?: (loading: boolean) => void;
18
+ cooldownTicks?: number | undefined;
19
+ cooldownTime?: number;
20
+ isLinkSelected?: (link: GraphLink) => boolean;
21
+ isNodeSelected?: (node: GraphNode) => boolean;
22
+ isLoading?: boolean;
23
+ }
24
+
25
+ export type GraphNode = NodeObject & {
26
+ id: number;
27
+ labels: string[];
28
+ color: string;
29
+ visible: boolean;
30
+ displayName: [string, string];
31
+ data: {
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ [key: string]: any;
34
+ };
35
+ x?: number;
36
+ y?: number;
37
+ vx?: number;
38
+ vy?: number;
39
+ fx?: number;
40
+ fy?: number;
41
+ };
42
+
43
+ export type GraphLink = {
44
+ id: number;
45
+ relationship: string;
46
+ color: string;
47
+ source: GraphNode;
48
+ target: GraphNode;
49
+ visible: boolean;
50
+ curve: number;
51
+ data: {
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ [key: string]: any;
54
+ };
55
+ };
56
+
57
+ export interface GraphData {
58
+ nodes: GraphNode[];
59
+ links: GraphLink[];
60
+ }
61
+
62
+ export type Node = Omit<
63
+ GraphNode,
64
+ "x" | "y" | "vx" | "vy" | "fx" | "fy" | "displayName"
65
+ >;
66
+
67
+ export type Link = Omit<GraphLink, "curve" | "source" | "target"> & {
68
+ source: number;
69
+ target: number;
70
+ };
71
+
72
+ export interface Data {
73
+ nodes: Node[];
74
+ links: Link[];
75
+ }
76
+
77
+ export type TextPriority = {
78
+ name: string;
79
+ ignore: boolean;
80
+ };
81
+
82
+ export type ViewportState = {
83
+ zoom: number;
84
+ centerX: number;
85
+ centerY: number;
86
+ } | undefined;
87
+
88
+ // Force graph instance type from force-graph library
89
+ // The instance is created by calling ForceGraph as a function with a container element
90
+ export type ForceGraphInstance = import("force-graph").default<GraphNode, GraphLink> | undefined;
@@ -0,0 +1,201 @@
1
+ import {
2
+ Data,
3
+ GraphData,
4
+ Node,
5
+ Link,
6
+ GraphNode,
7
+ GraphLink,
8
+ TextPriority,
9
+ } from "./falkordb-canvas-types.js";
10
+
11
+ /**
12
+ * Converts Data format to GraphData format
13
+ * Adds runtime properties (x, y, vx, vy, fx, fy, displayName, curve)
14
+ */
15
+ export function dataToGraphData(data: Data): GraphData {
16
+ const nodes: GraphNode[] = data.nodes.map((node) => ({
17
+ ...node,
18
+ displayName: ["", ""] as [string, string],
19
+ x: undefined,
20
+ y: undefined,
21
+ vx: undefined,
22
+ vy: undefined,
23
+ fx: undefined,
24
+ fy: undefined,
25
+ }));
26
+
27
+ // Create a Map for O(1) node lookups by id
28
+ const nodeMap = new Map<number, GraphNode>();
29
+ nodes.forEach((node) => {
30
+ nodeMap.set(node.id, node);
31
+ });
32
+
33
+ const links: GraphLink[] = data.links.map((link) => {
34
+ const sourceNode = nodeMap.get(link.source);
35
+ const targetNode = nodeMap.get(link.target);
36
+
37
+ return {
38
+ ...link,
39
+ source: sourceNode!,
40
+ target: targetNode!,
41
+ curve: 0,
42
+ };
43
+ });
44
+
45
+ return { nodes, links };
46
+ }
47
+
48
+ /**
49
+ * Converts GraphData format to Data format
50
+ * Removes runtime properties (x, y, vx, vy, fx, fy, displayName, curve)
51
+ */
52
+ export function graphDataToData(graphData: GraphData): Data {
53
+ const nodes: Node[] = graphData.nodes.map((node) => {
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
+ const { x, y, vx, vy, fx, fy, displayName, ...rest } = node;
56
+ return rest;
57
+ });
58
+
59
+ const links: Link[] = graphData.links.map((link) => {
60
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
+ const { curve, source, target, ...rest } = link;
62
+ return {
63
+ ...rest,
64
+ source: source.id,
65
+ target: target.id,
66
+ };
67
+ });
68
+
69
+ return { nodes, links };
70
+ }
71
+
72
+ export const getNodeDisplayText = (
73
+ node: Node,
74
+ displayTextPriority: TextPriority[]
75
+ ) => {
76
+ const { data: nodeData } = node;
77
+ const displayText = displayTextPriority.find(({ name, ignore }) => {
78
+ const key = ignore
79
+ ? Object.keys(nodeData).find(
80
+ (k) => k.toLowerCase() === name.toLowerCase()
81
+ )
82
+ : name;
83
+
84
+ return (
85
+ key &&
86
+ nodeData[key] &&
87
+ typeof nodeData[key] === "string" &&
88
+ nodeData[key].trim().length > 0
89
+ );
90
+ });
91
+
92
+ if (displayText) {
93
+ const key = displayText.ignore
94
+ ? Object.keys(nodeData).find(
95
+ (k) => k.toLowerCase() === displayText.name.toLowerCase()
96
+ )
97
+ : displayText.name;
98
+
99
+ if (key) {
100
+ return String(nodeData[key]);
101
+ }
102
+ }
103
+
104
+ return String(node.id);
105
+ };
106
+
107
+ export const getNodeDisplayKey = (
108
+ node: Node,
109
+ displayTextPriority: TextPriority[]
110
+ ) => {
111
+ const { data: nodeData } = node;
112
+ const displayText = displayTextPriority.find(({ name, ignore }) => {
113
+ const key = ignore
114
+ ? Object.keys(nodeData).find(
115
+ (k) => k.toLowerCase() === name.toLowerCase()
116
+ )
117
+ : name;
118
+
119
+ return (
120
+ key &&
121
+ nodeData[key] &&
122
+ typeof nodeData[key] === "string" &&
123
+ nodeData[key].trim().length > 0
124
+ );
125
+ });
126
+
127
+ if (displayText) {
128
+ const key = displayText.ignore
129
+ ? Object.keys(nodeData).find(
130
+ (k) => k.toLowerCase() === displayText.name.toLowerCase()
131
+ )
132
+ : displayText.name;
133
+
134
+ if (key) {
135
+ return key;
136
+ }
137
+ }
138
+
139
+ return String(node.id);
140
+ };
141
+
142
+ /**
143
+ * Wraps text into two lines with ellipsis handling for circular nodes
144
+ */
145
+ export const wrapTextForCircularNode = (
146
+ ctx: CanvasRenderingContext2D,
147
+ text: string,
148
+ maxRadius: number
149
+ ): [string, string] => {
150
+ const ellipsis = "...";
151
+ const ellipsisWidth = ctx.measureText(ellipsis).width;
152
+ const halfTextHeight = 1.125;
153
+ const availableRadius = Math.sqrt(
154
+ Math.max(0, maxRadius * maxRadius - halfTextHeight * halfTextHeight)
155
+ );
156
+ const lineWidth = availableRadius * 2;
157
+
158
+ const words = text.split(/\s+/);
159
+ let line1 = "";
160
+ let line2 = "";
161
+
162
+ for (let i = 0; i < words.length; i += 1) {
163
+ const word = words[i];
164
+ const testLine = line1 ? `${line1} ${word}` : word;
165
+ const testWidth = ctx.measureText(testLine).width;
166
+
167
+ if (testWidth <= lineWidth) {
168
+ line1 = testLine;
169
+ } else if (!line1) {
170
+ let partialWord = word;
171
+ while (
172
+ partialWord.length > 0 &&
173
+ ctx.measureText(partialWord).width > lineWidth
174
+ ) {
175
+ partialWord = partialWord.slice(0, -1);
176
+ }
177
+ line1 = partialWord;
178
+ const remainingWords = [
179
+ word.slice(partialWord.length),
180
+ ...words.slice(i + 1),
181
+ ];
182
+ line2 = remainingWords.join(" ");
183
+ break;
184
+ } else {
185
+ line2 = words.slice(i).join(" ");
186
+ break;
187
+ }
188
+ }
189
+
190
+ if (line2 && ctx.measureText(line2).width > lineWidth) {
191
+ while (
192
+ line2.length > 0 &&
193
+ ctx.measureText(line2).width + ellipsisWidth > lineWidth
194
+ ) {
195
+ line2 = line2.slice(0, -1);
196
+ }
197
+ line2 += ellipsis;
198
+ }
199
+
200
+ return [line1, line2 || ""];
201
+ };
@@ -0,0 +1,786 @@
1
+ /* eslint-disable no-param-reassign */
2
+
3
+ import ForceGraph from "force-graph";
4
+ import * as d3 from "d3";
5
+ import {
6
+ Data,
7
+ ForceGraphInstance,
8
+ GraphData,
9
+ GraphLink,
10
+ GraphNode,
11
+ ForceGraphConfig,
12
+ ViewportState,
13
+ } from "./falkordb-canvas-types.js";
14
+ import {
15
+ dataToGraphData,
16
+ getNodeDisplayText,
17
+ graphDataToData,
18
+ wrapTextForCircularNode,
19
+ } from "./falkordb-canvas-utils.js";
20
+
21
+ const NODE_SIZE = 6;
22
+ const PADDING = 2;
23
+
24
+ // Force constants
25
+ const LINK_DISTANCE = 50;
26
+ const MAX_LINK_DISTANCE = 80;
27
+ const LINK_STRENGTH = 0.5;
28
+ const MIN_LINK_STRENGTH = 0.3;
29
+ const COLLISION_STRENGTH = 1.35;
30
+ const CHARGE_STRENGTH = -5;
31
+ const CENTER_STRENGTH = 0.4;
32
+ const COLLISION_BASE_RADIUS = NODE_SIZE * 2;
33
+ const HIGH_DEGREE_PADDING = 1.25;
34
+ const DEGREE_STRENGTH_DECAY = 15;
35
+ const CROWDING_THRESHOLD = 20;
36
+
37
+ // Create styles for the web component
38
+ function createStyles(): HTMLStyleElement {
39
+ const style = document.createElement("style");
40
+ style.textContent = `
41
+ :host {
42
+ display: block;
43
+ width: 100%;
44
+ height: 100%;
45
+ }
46
+ @keyframes pulse {
47
+ 0%, 100% {
48
+ opacity: 1;
49
+ }
50
+ 50% {
51
+ opacity: 0.5;
52
+ }
53
+ }
54
+ `;
55
+ return style;
56
+ }
57
+
58
+ class FalkorDBCanvas extends HTMLElement {
59
+ private graph: ForceGraphInstance;
60
+
61
+ private container: HTMLDivElement | null = null;
62
+
63
+ private loadingOverlay: HTMLDivElement | null = null;
64
+
65
+ private data: GraphData = { nodes: [], links: [] };
66
+
67
+ private config: ForceGraphConfig = {};
68
+
69
+ private nodeDegreeMap: Map<number, number> = new Map();
70
+
71
+ private relationshipsTextCache: Map<
72
+ string,
73
+ {
74
+ textWidth: number;
75
+ textHeight: number;
76
+ textAscent: number;
77
+ textDescent: number;
78
+ }
79
+ > = new Map();
80
+
81
+ private viewport: ViewportState;
82
+
83
+ constructor() {
84
+ super();
85
+ this.attachShadow({ mode: "open" });
86
+ }
87
+
88
+ connectedCallback() {
89
+ this.render();
90
+ }
91
+
92
+ disconnectedCallback() {
93
+ if (this.graph) {
94
+ // eslint-disable-next-line no-underscore-dangle
95
+ this.graph._destructor();
96
+ }
97
+ }
98
+
99
+ setConfig(config: Partial<ForceGraphConfig>) {
100
+ Object.assign(this.config, config);
101
+
102
+ // Update event handlers if they were provided
103
+ if (config.onNodeClick || config.onNodeRightClick || config.onLinkRightClick ||
104
+ config.onNodeHover || config.onLinkHover || config.onBackgroundClick ||
105
+ config.onEngineStop || config.isNodeSelected || config.isLinkSelected) {
106
+ this.updateEventHandlers();
107
+ }
108
+ }
109
+
110
+ setWidth(width: number) {
111
+ if (this.config.width === width) return;
112
+ this.config.width = width;
113
+ if (this.graph) {
114
+ this.graph.width(width);
115
+ }
116
+ }
117
+
118
+ setHeight(height: number) {
119
+ if (this.config.height === height) return;
120
+ this.config.height = height;
121
+ if (this.graph) {
122
+ this.graph.height(height);
123
+ }
124
+ }
125
+
126
+ setBackgroundColor(color: string) {
127
+ if (this.config.backgroundColor === color) return;
128
+ this.config.backgroundColor = color;
129
+ if (this.graph) {
130
+ this.graph.backgroundColor(color);
131
+ }
132
+ if (this.loadingOverlay) {
133
+ this.loadingOverlay.style.background = color;
134
+ }
135
+ }
136
+
137
+ setForegroundColor(color: string) {
138
+ if (this.config.foregroundColor === color) return;
139
+ this.config.foregroundColor = color;
140
+ this.triggerRender();
141
+ }
142
+
143
+ setIsLoading(isLoading: boolean) {
144
+ if (this.config.isLoading === isLoading) return;
145
+ this.config.isLoading = isLoading;
146
+ this.updateLoadingState();
147
+ }
148
+
149
+ setCooldownTicks(ticks: number | undefined) {
150
+ if (this.config.cooldownTicks === ticks) return;
151
+ this.config.cooldownTicks = ticks;
152
+ if (this.graph) {
153
+ this.graph.cooldownTicks(ticks ?? Infinity);
154
+ }
155
+ }
156
+
157
+ setDisplayTextPriority(priority: ForceGraphConfig['displayTextPriority']) {
158
+ this.config.displayTextPriority = priority;
159
+ this.triggerRender();
160
+ }
161
+
162
+ getData(): Data {
163
+ return graphDataToData(this.data);
164
+ }
165
+
166
+
167
+ setData(data: Data) {
168
+ this.data = dataToGraphData(data);
169
+ this.config.cooldownTicks = this.data.nodes.length > 0 ? undefined : 0;
170
+ this.config.isLoading = this.data.nodes.length > 0;
171
+ this.config.onLoadingChange?.(this.config.isLoading);
172
+
173
+ // Initialize graph if it hasn't been initialized yet
174
+ if (!this.graph && this.container) {
175
+ this.initGraph();
176
+ }
177
+
178
+ if (!this.graph) return;
179
+
180
+ this.calculateNodeDegree();
181
+
182
+ // Update graph data and properties
183
+ this.graph
184
+ .graphData(this.data)
185
+ .cooldownTicks(this.config.cooldownTicks ?? Infinity);
186
+
187
+ this.updateLoadingState();
188
+ this.setupForces();
189
+ }
190
+
191
+ getViewport(): ViewportState {
192
+ if (!this.graph) return undefined;
193
+
194
+ const { x: centerX, y: centerY } = this.graph.centerAt();
195
+ const zoom = this.graph.zoom();
196
+
197
+ return {
198
+ zoom,
199
+ centerX,
200
+ centerY,
201
+ };
202
+ }
203
+
204
+ setViewport(viewport: ViewportState) {
205
+ this.viewport = viewport;
206
+ }
207
+
208
+ getGraphData(): GraphData {
209
+ return this.data;
210
+ }
211
+
212
+ setGraphData(data: GraphData) {
213
+ this.data = data;
214
+
215
+ this.config.cooldownTicks = 0;
216
+ this.config.isLoading = false;
217
+
218
+ if (!this.graph) return;
219
+
220
+ this.calculateNodeDegree();
221
+ this.graph
222
+ .graphData(this.data)
223
+ .cooldownTicks(this.config.cooldownTicks ?? Infinity);
224
+
225
+ if (this.viewport) {
226
+ this.graph.zoom(this.viewport.zoom, 0);
227
+ this.graph.centerAt(this.viewport.centerX, this.viewport.centerY, 0);
228
+ this.viewport = undefined;
229
+ }
230
+
231
+ this.updateLoadingState();
232
+ this.setupForces();
233
+ }
234
+
235
+ getGraph(): ForceGraphInstance | undefined {
236
+ return this.graph;
237
+ }
238
+
239
+ public getZoom(): number {
240
+ return this.graph?.zoom() || 0;
241
+ }
242
+
243
+ public zoom(zoomLevel: number): ForceGraphInstance | undefined {
244
+ if (!this.graph) return;
245
+
246
+ return this.graph.zoom(zoomLevel);
247
+ }
248
+
249
+ public zoomToFit(paddingMultiplier = 1, filter?: (node: GraphNode) => boolean) {
250
+ if (!this.graph || !this.shadowRoot) return;
251
+
252
+ // Get canvas from shadow DOM
253
+ const canvas = this.shadowRoot.querySelector("canvas") as HTMLCanvasElement;
254
+ if (!canvas) return;
255
+
256
+ const rect = canvas.getBoundingClientRect();
257
+
258
+ // Calculate padding as 10% of the smallest canvas dimension
259
+ const minDimension = Math.min(rect.width, rect.height);
260
+ const padding = minDimension * 0.1;
261
+
262
+ // Use the force-graph's built-in zoomToFit method
263
+ this.graph.zoomToFit(500, padding * paddingMultiplier, filter);
264
+ }
265
+
266
+ private triggerRender() {
267
+ if (!this.graph || this.graph.cooldownTicks() !== 0) return;
268
+
269
+ // If simulation is stopped (0), trigger one tick to re-render
270
+ this.graph.cooldownTicks(1);
271
+ }
272
+
273
+ private calculateNodeDegree() {
274
+ this.nodeDegreeMap.clear();
275
+ const { nodes, links } = this.data;
276
+
277
+ nodes.forEach((node) => this.nodeDegreeMap.set(node.id, 0));
278
+
279
+ links.forEach((link) => {
280
+ const sourceId = link.source.id;
281
+ const targetId = link.target.id;
282
+
283
+ this.nodeDegreeMap.set(
284
+ sourceId,
285
+ (this.nodeDegreeMap.get(sourceId) || 0) + 1
286
+ );
287
+ this.nodeDegreeMap.set(
288
+ targetId,
289
+ (this.nodeDegreeMap.get(targetId) || 0) + 1
290
+ );
291
+ });
292
+ }
293
+
294
+ private createLoadingOverlay(): HTMLDivElement {
295
+ const overlay = document.createElement("div");
296
+ overlay.style.cssText = `
297
+ position: absolute;
298
+ inset: 0;
299
+ display: none;
300
+ align-items: center;
301
+ justify-content: center;
302
+ background: ${this.config.backgroundColor || "#FFFFFF"};
303
+ z-index: 10;
304
+ `;
305
+
306
+ // Create skeleton loading structure (matching Spinning component pattern)
307
+ const skeletonContainer = document.createElement("div");
308
+ skeletonContainer.style.cssText = `
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 1rem;
312
+ `;
313
+
314
+ // Create circular skeleton (matching h-12 w-12 rounded-full)
315
+ const circle = document.createElement("div");
316
+ circle.style.cssText = `
317
+ height: 3rem;
318
+ width: 3rem;
319
+ border-radius: 9999px;
320
+ background-color: #CCCCCC;
321
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
322
+ `;
323
+
324
+ // Create lines container (matching space-y-2)
325
+ const linesContainer = document.createElement("div");
326
+ linesContainer.style.cssText = `
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 0.5rem;
330
+ `;
331
+
332
+ // Create first line (matching h-4 w-[250px])
333
+ const line1 = document.createElement("div");
334
+ line1.style.cssText = `
335
+ height: 1rem;
336
+ width: 250px;
337
+ border-radius: 0.375rem;
338
+ background-color: #CCCCCC;
339
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
340
+ `;
341
+
342
+ // Create second line (matching h-4 w-[200px])
343
+ const line2 = document.createElement("div");
344
+ line2.style.cssText = `
345
+ height: 1rem;
346
+ width: 200px;
347
+ border-radius: 0.375rem;
348
+ background-color: #CCCCCC;
349
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
350
+ `;
351
+
352
+ linesContainer.appendChild(line1);
353
+ linesContainer.appendChild(line2);
354
+ skeletonContainer.appendChild(circle);
355
+ skeletonContainer.appendChild(linesContainer);
356
+ overlay.appendChild(skeletonContainer);
357
+
358
+ return overlay;
359
+ }
360
+
361
+ private render() {
362
+ if (!this.shadowRoot) return;
363
+
364
+ // Create container
365
+ this.container = document.createElement("div");
366
+ this.container.style.width = "100%";
367
+ this.container.style.height = "100%";
368
+ this.container.style.position = "relative";
369
+
370
+ // Create loading overlay
371
+ this.loadingOverlay = this.createLoadingOverlay();
372
+
373
+ // Add styles using standalone function
374
+ const style = createStyles();
375
+
376
+ this.shadowRoot.appendChild(style);
377
+ this.shadowRoot.appendChild(this.container);
378
+ this.initGraph();
379
+ this.container.appendChild(this.loadingOverlay);
380
+ }
381
+
382
+ private initGraph() {
383
+ if (!this.container) return;
384
+
385
+ this.calculateNodeDegree();
386
+
387
+ // Initialize force-graph
388
+ // Cast to any for the factory call pattern, result is properly typed as ForceGraphInstance
389
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
+ this.graph = (ForceGraph as any)()(this.container)
391
+ .width(this.config.width || 800)
392
+ .height(this.config.height || 600)
393
+ .backgroundColor(this.config.backgroundColor || "#FFFFFF")
394
+ .graphData(this.data)
395
+ .nodeRelSize(NODE_SIZE)
396
+ .nodeCanvasObjectMode(() => "after")
397
+ .linkCanvasObjectMode(() => "after")
398
+ .nodeLabel((node: GraphNode) =>
399
+ getNodeDisplayText(node, this.config.displayTextPriority || [])
400
+ )
401
+ .linkLabel((link: GraphLink) => link.relationship)
402
+ .linkDirectionalArrowRelPos(1)
403
+ .linkDirectionalArrowLength((link: GraphLink) => {
404
+ if (link.source === link.target) return 0;
405
+ return this.config.isLinkSelected?.(link) ? 4 : 2;
406
+ })
407
+ .linkDirectionalArrowColor((link: GraphLink) => link.color)
408
+ .linkWidth((link: GraphLink) =>
409
+ this.config.isLinkSelected?.(link) ? 2 : 1
410
+ )
411
+ .linkCurvature("curve")
412
+ .linkVisibility("visible")
413
+ .nodeVisibility("visible")
414
+ .cooldownTicks(this.config.cooldownTicks ?? Infinity) // undefined = infinite
415
+ .cooldownTime(this.config.cooldownTime ?? 1000)
416
+ .enableNodeDrag(true)
417
+ .enableZoomInteraction(true)
418
+ .enablePanInteraction(true)
419
+ .onNodeClick((node: GraphNode, event: MouseEvent) => {
420
+ if (this.config.onNodeClick) {
421
+ this.config.onNodeClick(node, event);
422
+ }
423
+ })
424
+ .onLinkClick((link: GraphLink, event: MouseEvent) => {
425
+ if (this.config.onLinkClick) {
426
+ this.config.onLinkClick(link, event);
427
+ }
428
+ })
429
+ .onNodeRightClick((node: GraphNode, event: MouseEvent) => {
430
+ if (this.config.onNodeRightClick) {
431
+ this.config.onNodeRightClick(node, event);
432
+ }
433
+ })
434
+ .onLinkRightClick((link: GraphLink, event: MouseEvent) => {
435
+ if (this.config.onLinkRightClick) {
436
+ this.config.onLinkRightClick(link, event);
437
+ }
438
+ })
439
+ .onNodeHover((node: GraphNode | null) => {
440
+ if (this.config.onNodeHover) {
441
+ this.config.onNodeHover(node);
442
+ }
443
+ })
444
+ .onLinkHover((link: GraphLink | null) => {
445
+ if (this.config.onLinkHover) {
446
+ this.config.onLinkHover(link);
447
+ }
448
+ })
449
+ .onBackgroundClick((event: MouseEvent) => {
450
+ if (this.config.onBackgroundClick) {
451
+ this.config.onBackgroundClick(event);
452
+ }
453
+ })
454
+ .onEngineStop(() => {
455
+ this.handleEngineStop();
456
+ if (this.config.onEngineStop) {
457
+ this.config.onEngineStop();
458
+ }
459
+ })
460
+ .nodeCanvasObject((node: GraphNode, ctx: CanvasRenderingContext2D) => {
461
+ this.drawNode(node, ctx);
462
+ })
463
+ .linkCanvasObject((link: GraphLink, ctx: CanvasRenderingContext2D) => {
464
+ this.drawLink(link, ctx);
465
+ });
466
+
467
+ // Setup forces
468
+ this.setupForces();
469
+ }
470
+
471
+ private setupForces() {
472
+ const linkForce = this.graph?.d3Force("link");
473
+
474
+ if (!linkForce) return;
475
+ if (!this.graph) return;
476
+
477
+ // Link force with dynamic distance and strength
478
+ linkForce
479
+ .distance((link: GraphLink) => {
480
+ const sourceId = link.source.id;
481
+ const targetId = link.target.id;
482
+ const sourceDegree = this.nodeDegreeMap.get(sourceId) || 0;
483
+ const targetDegree = this.nodeDegreeMap.get(targetId) || 0;
484
+ const maxDegree = Math.max(sourceDegree, targetDegree);
485
+
486
+ if (maxDegree >= CROWDING_THRESHOLD) {
487
+ const extraDistance = Math.min(
488
+ MAX_LINK_DISTANCE - LINK_DISTANCE,
489
+ (maxDegree - CROWDING_THRESHOLD) * 1.5
490
+ );
491
+ return LINK_DISTANCE + extraDistance;
492
+ }
493
+
494
+ return LINK_DISTANCE;
495
+ })
496
+ .strength((link: GraphLink) => {
497
+ const sourceId = link.source.id;
498
+ const targetId = link.target.id;
499
+ const sourceDegree = this.nodeDegreeMap.get(sourceId) || 0;
500
+ const targetDegree = this.nodeDegreeMap.get(targetId) || 0;
501
+ const maxDegree = Math.max(sourceDegree, targetDegree);
502
+
503
+ if (maxDegree <= DEGREE_STRENGTH_DECAY) {
504
+ return LINK_STRENGTH;
505
+ }
506
+
507
+ const strengthReduction = Math.max(
508
+ 0,
509
+ (maxDegree - DEGREE_STRENGTH_DECAY) / DEGREE_STRENGTH_DECAY
510
+ );
511
+ const scaledStrength =
512
+ MIN_LINK_STRENGTH +
513
+ (LINK_STRENGTH - MIN_LINK_STRENGTH) * Math.exp(-strengthReduction);
514
+
515
+ return Math.max(MIN_LINK_STRENGTH, scaledStrength);
516
+ });
517
+
518
+ // Collision force
519
+ this.graph.d3Force(
520
+ "collision",
521
+ d3
522
+ .forceCollide((node: GraphNode) => {
523
+ const degree = this.nodeDegreeMap.get(node.id) || 0;
524
+ return (
525
+ COLLISION_BASE_RADIUS + Math.sqrt(degree) * HIGH_DEGREE_PADDING
526
+ );
527
+ })
528
+ .strength(COLLISION_STRENGTH)
529
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
530
+ .iterations(2) as any
531
+ );
532
+
533
+ // Center force
534
+ const centerForce = this.graph.d3Force("center");
535
+ if (centerForce) {
536
+ centerForce.strength(CENTER_STRENGTH);
537
+ }
538
+
539
+ // Charge force
540
+ const chargeForce = this.graph.d3Force("charge");
541
+ if (chargeForce) {
542
+ chargeForce.strength(CHARGE_STRENGTH).distanceMax(300);
543
+ }
544
+ }
545
+
546
+ private drawNode(node: GraphNode, ctx: CanvasRenderingContext2D) {
547
+ if (!node.x || !node.y) {
548
+ node.x = 0;
549
+ node.y = 0;
550
+ }
551
+
552
+ ctx.lineWidth = this.config.isNodeSelected?.(node) ? 1.5 : 0.5;
553
+ ctx.strokeStyle = this.config.foregroundColor || "#1A1A1A";
554
+
555
+ ctx.beginPath();
556
+ ctx.arc(node.x, node.y, NODE_SIZE, 0, 2 * Math.PI, false);
557
+ ctx.fillStyle = node.color;
558
+ ctx.fill();
559
+ ctx.stroke();
560
+
561
+ // Draw text
562
+ ctx.fillStyle = "black";
563
+ ctx.textAlign = "center";
564
+ ctx.textBaseline = "middle";
565
+ ctx.font = "400 2px SofiaSans";
566
+
567
+ let [line1, line2] = node.displayName || ["", ""];
568
+
569
+ if (!line1 && !line2) {
570
+ const text = getNodeDisplayText(
571
+ node,
572
+ this.config.displayTextPriority || []
573
+ );
574
+ const textRadius = NODE_SIZE - PADDING / 2;
575
+ [line1, line2] = wrapTextForCircularNode(ctx, text, textRadius);
576
+ node.displayName = [line1, line2];
577
+ }
578
+
579
+ const textMetrics = ctx.measureText(line1);
580
+ const textHeight =
581
+ textMetrics.actualBoundingBoxAscent +
582
+ textMetrics.actualBoundingBoxDescent;
583
+ const halfTextHeight = (textHeight / 2) * 1.5;
584
+
585
+ if (line1) {
586
+ ctx.fillText(line1, node.x, line2 ? node.y - halfTextHeight : node.y);
587
+ }
588
+ if (line2) {
589
+ ctx.fillText(line2, node.x, node.y + halfTextHeight);
590
+ }
591
+ }
592
+
593
+ private drawLink(link: GraphLink, ctx: CanvasRenderingContext2D) {
594
+ const start = link.source;
595
+ const end = link.target;
596
+
597
+ if (!start.x || !start.y || !end.x || !end.y) {
598
+ start.x = 0;
599
+ start.y = 0;
600
+ end.x = 0;
601
+ end.y = 0;
602
+ }
603
+
604
+ let textX;
605
+ let textY;
606
+ let angle;
607
+
608
+ if (start.id === end.id) {
609
+ const radius = NODE_SIZE * (link.curve || 0) * 6.2;
610
+ const angleOffset = -Math.PI / 4;
611
+ textX = start.x + radius * Math.cos(angleOffset);
612
+ textY = start.y + radius * Math.sin(angleOffset);
613
+ angle = -angleOffset;
614
+ } else {
615
+ const dx = end.x - start.x;
616
+ const dy = end.y - start.y;
617
+ const distance = Math.sqrt(dx * dx + dy * dy);
618
+
619
+ const perpX = dy / distance;
620
+ const perpY = -dx / distance;
621
+
622
+ const curvature = link.curve || 0;
623
+ const controlX =
624
+ (start.x + end.x) / 2 + perpX * curvature * distance * 1.0;
625
+ const controlY =
626
+ (start.y + end.y) / 2 + perpY * curvature * distance * 1.0;
627
+
628
+ const t = 0.5;
629
+ const oneMinusT = 1 - t;
630
+ textX =
631
+ oneMinusT * oneMinusT * start.x +
632
+ 2 * oneMinusT * t * controlX +
633
+ t * t * end.x;
634
+ textY =
635
+ oneMinusT * oneMinusT * start.y +
636
+ 2 * oneMinusT * t * controlY +
637
+ t * t * end.y;
638
+
639
+ const tangentX =
640
+ 2 * oneMinusT * (controlX - start.x) + 2 * t * (end.x - controlX);
641
+ const tangentY =
642
+ 2 * oneMinusT * (controlY - start.y) + 2 * t * (end.y - controlY);
643
+ angle = Math.atan2(tangentY, tangentX);
644
+
645
+ if (angle > Math.PI / 2) angle = -(Math.PI - angle);
646
+ if (angle < -Math.PI / 2) angle = -(-Math.PI - angle);
647
+ }
648
+
649
+ ctx.font = "400 2px SofiaSans";
650
+ ctx.textAlign = "center";
651
+ ctx.textBaseline = "middle";
652
+
653
+ let cached = this.relationshipsTextCache.get(link.relationship);
654
+
655
+ if (!cached) {
656
+ const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
657
+ ctx.measureText(link.relationship);
658
+ cached = {
659
+ textWidth: width,
660
+ textHeight: actualBoundingBoxAscent + actualBoundingBoxDescent,
661
+ textAscent: actualBoundingBoxAscent,
662
+ textDescent: actualBoundingBoxDescent,
663
+ };
664
+ this.relationshipsTextCache.set(link.relationship, cached);
665
+ }
666
+
667
+ const { textWidth, textHeight, textAscent, textDescent } = cached;
668
+
669
+ ctx.save();
670
+ ctx.translate(textX, textY);
671
+ ctx.rotate(angle);
672
+
673
+ // Draw background
674
+ ctx.fillStyle = this.config.backgroundColor || "#FFFFFF";
675
+ const backgroundHeight = textHeight * 0.7;
676
+
677
+ // Move background up to align with text that appears at top of bg
678
+ // Use the actual text metrics to calculate proper vertical offset
679
+ const bgOffsetY = -(textAscent - textDescent) - 0.18;
680
+ ctx.fillRect(
681
+ -textWidth / 2,
682
+ -backgroundHeight / 2 + bgOffsetY,
683
+ textWidth,
684
+ backgroundHeight
685
+ );
686
+
687
+ // Draw text
688
+ ctx.fillStyle = this.config.foregroundColor || "#1A1A1A";
689
+ ctx.textBaseline = "middle";
690
+ ctx.fillText(link.relationship, 0, 0);
691
+ ctx.restore();
692
+ }
693
+
694
+ private updateLoadingState() {
695
+ if (!this.loadingOverlay) return;
696
+
697
+ if (this.config.isLoading) {
698
+ this.loadingOverlay.style.display = "flex";
699
+ } else {
700
+ this.loadingOverlay.style.display = "none";
701
+ }
702
+ }
703
+
704
+ private handleEngineStop() {
705
+ if (!this.graph) return;
706
+
707
+ // If already stopped, don't do anything
708
+ if (this.config.cooldownTicks === 0) return;
709
+
710
+ // Zoom to fit using the same logic as handleZoomToFit from utils
711
+ const nodeCount = this.data.nodes.length;
712
+ const paddingMultiplier = nodeCount < 2 ? 4 : 1;
713
+ this.zoomToFit(paddingMultiplier);
714
+
715
+ // Stop the force simulation after centering (like it was in ForceGraph.tsx)
716
+ setTimeout(() => {
717
+ if (!this.graph) return;
718
+ // Stop loading
719
+ this.config.isLoading = false;
720
+ this.config.onLoadingChange?.(this.config.isLoading);
721
+ this.updateLoadingState();
722
+ this.config.cooldownTicks = 0;
723
+ this.graph.cooldownTicks(0);
724
+ }, 1000);
725
+ }
726
+
727
+ private updateEventHandlers() {
728
+ if (!this.graph) return;
729
+
730
+ this.graph
731
+ .onNodeClick((node: GraphNode, event: MouseEvent) => {
732
+ if (this.config.onNodeClick) {
733
+ this.config.onNodeClick(node, event);
734
+ }
735
+ })
736
+ .onLinkClick((link: GraphLink, event: MouseEvent) => {
737
+ if (this.config.onLinkClick) {
738
+ this.config.onLinkClick(link, event);
739
+ }
740
+ })
741
+ .onNodeRightClick((node: GraphNode, event: MouseEvent) => {
742
+ if (this.config.onNodeRightClick) {
743
+ this.config.onNodeRightClick(node, event);
744
+ }
745
+ })
746
+ .onLinkRightClick((link: GraphLink, event: MouseEvent) => {
747
+ if (this.config.onLinkRightClick) {
748
+ this.config.onLinkRightClick(link, event);
749
+ }
750
+ })
751
+ .onNodeHover((node: GraphNode | null) => {
752
+ if (this.config.onNodeHover) {
753
+ this.config.onNodeHover(node);
754
+ }
755
+ })
756
+ .onLinkHover((link: GraphLink | null) => {
757
+ if (this.config.onLinkHover) {
758
+ this.config.onLinkHover(link);
759
+ }
760
+ })
761
+ .onBackgroundClick((event: MouseEvent) => {
762
+ if (this.config.onBackgroundClick) {
763
+ this.config.onBackgroundClick(event);
764
+ }
765
+ })
766
+ .onEngineStop(() => {
767
+ this.handleEngineStop();
768
+ if (this.config.onEngineStop) {
769
+ this.config.onEngineStop();
770
+ }
771
+ })
772
+ .nodeCanvasObject((node: GraphNode, ctx: CanvasRenderingContext2D) => {
773
+ this.drawNode(node, ctx);
774
+ })
775
+ .linkCanvasObject((link: GraphLink, ctx: CanvasRenderingContext2D) => {
776
+ this.drawLink(link, ctx);
777
+ });
778
+ }
779
+ }
780
+
781
+ // Define the custom element
782
+ if (typeof window !== "undefined" && !customElements.get("falkordb-canvas")) {
783
+ customElements.define("falkordb-canvas", FalkorDBCanvas);
784
+ }
785
+
786
+ export default FalkorDBCanvas;
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import FalkorDBCanvas from "./falkordb-canvas.js";
2
+ import type React from "react";
3
+
4
+ declare global {
5
+ interface HTMLElementTagNameMap {
6
+ "falkordb-canvas": FalkorDBCanvas;
7
+ }
8
+
9
+ namespace JSX {
10
+ interface IntrinsicElements {
11
+ "falkordb-canvas": React.DetailedHTMLProps<
12
+ React.HTMLAttributes<FalkorDBCanvas>,
13
+ FalkorDBCanvas
14
+ >;
15
+ }
16
+ }
17
+ }
18
+
19
+ // Main canvas class
20
+ export { FalkorDBCanvas as default, FalkorDBCanvas };
21
+
22
+ // Types
23
+ export type {
24
+ ForceGraphConfig,
25
+ GraphNode,
26
+ GraphLink,
27
+ GraphData,
28
+ Node,
29
+ Link,
30
+ Data,
31
+ TextPriority,
32
+ ViewportState,
33
+ ForceGraphInstance,
34
+ } from "./falkordb-canvas-types.js";
35
+
36
+ // Utils
37
+ export {
38
+ dataToGraphData,
39
+ graphDataToData,
40
+ getNodeDisplayText,
41
+ getNodeDisplayKey,
42
+ wrapTextForCircularNode,
43
+ } from "./falkordb-canvas-utils.js";