@falkordb/canvas 0.0.49 → 0.0.50
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/README.md +59 -1
- package/dist/canvas-types.d.ts +43 -0
- package/dist/canvas-types.d.ts.map +1 -1
- package/dist/canvas.d.ts +39 -0
- package/dist/canvas.d.ts.map +1 -1
- package/dist/canvas.js +222 -65
- package/dist/canvas.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/canvas-types.ts +49 -0
- package/src/canvas.ts +254 -70
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A standalone web component for visualizing FalkorDB graphs using force-directed
|
|
|
8
8
|
- 🧭 **Multiple layout modes** - Switch between `force`, `tree`, `flow`, `radial-tree`, `concentric`, `components`, and `arc` graph views
|
|
9
9
|
- 🎯 **Interactive** - Click, hover, right-click interactions on nodes, links, and background
|
|
10
10
|
- 🌓 **Theme support** - Light and dark mode compatible with customizable colors
|
|
11
|
-
- ⚡ **Performance** - Optimized rendering with HTML5 canvas
|
|
11
|
+
- ⚡ **Performance** - Optimized rendering with HTML5 canvas, including viewport culling and low-zoom draw skipping for large graphs
|
|
12
12
|
- 💫 **Loading states** - Built-in skeleton loading with pulse animation
|
|
13
13
|
- 🎨 **Customizable** - Colors, sizes, behaviors, and custom rendering functions
|
|
14
14
|
- 📦 **TypeScript support** - Full type definitions included
|
|
@@ -159,6 +159,7 @@ function GraphVisualization() {
|
|
|
159
159
|
| `isLinkSelected` | | Function to determine if a link is selected. Signature: `(link: GraphLink) => boolean` |
|
|
160
160
|
| `node` | | Custom node rendering functions (see Custom Rendering) |
|
|
161
161
|
| `link` | | Custom link rendering functions (see Custom Rendering) |
|
|
162
|
+
| `largeGraph` | | Large-graph rendering optimizations (see [Large-Graph Optimizations](#large-graph-optimizations)) |
|
|
162
163
|
|
|
163
164
|
### Layout Modes
|
|
164
165
|
|
|
@@ -432,6 +433,63 @@ while (true) {
|
|
|
432
433
|
2. **Static graphs**: Set `cooldownTicks: 0` after initial layout
|
|
433
434
|
3. **Custom rendering**: Optimize your custom `nodeCanvasObject` and `linkCanvasObject` functions
|
|
434
435
|
4. **Viewport**: Use `getViewport()` and `setViewport()` to preserve user's view when updating data
|
|
436
|
+
5. **Very large graphs**: Enable viewport culling via the `largeGraph` option (see below)
|
|
437
|
+
|
|
438
|
+
## Large-Graph Optimizations
|
|
439
|
+
|
|
440
|
+
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.
|
|
441
|
+
|
|
442
|
+
> **Note:** These optimizations are applied by the default renderer. If you provide custom
|
|
443
|
+
> `nodeCanvasObject` / `linkCanvasObject` callbacks, the library will not cull or skip
|
|
444
|
+
> drawing for those elements automatically. Implement equivalent viewport culling and
|
|
445
|
+
> low-zoom checks in your custom renderer if needed.
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
canvas.setConfig({
|
|
449
|
+
largeGraph: {
|
|
450
|
+
enabled: true, // master switch (false by default – no behavioural change unless true)
|
|
451
|
+
viewportPadding: 50, // world-unit padding around the visible viewport (default: 0)
|
|
452
|
+
lowZoomThreshold: 0.5, // zoom level below which expensive details are skipped
|
|
453
|
+
skipLabelsAtLowZoom: true, // skip node labels at low zoom (default: true)
|
|
454
|
+
skipArrowsAtLowZoom: true, // skip link arrowheads at low zoom (default: true)
|
|
455
|
+
skipLinkLabelsAtLowZoom: true, // skip link relationship labels at low zoom (default: true)
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### How it works
|
|
461
|
+
|
|
462
|
+
**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.
|
|
463
|
+
|
|
464
|
+
- For **nodes**: the bounding box is a circle of radius `node.size + 2`.
|
|
465
|
+
- 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.
|
|
466
|
+
- For **self-loops**: the bounding box is a square centered on the node with a side length derived from the loop curvature.
|
|
467
|
+
|
|
468
|
+
**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:
|
|
469
|
+
|
|
470
|
+
- Node labels (text inside nodes) – controlled by `skipLabelsAtLowZoom`
|
|
471
|
+
- Link arrowheads – controlled by `skipArrowsAtLowZoom`
|
|
472
|
+
- Link relationship labels – controlled by `skipLinkLabelsAtLowZoom`
|
|
473
|
+
|
|
474
|
+
Node and link shapes are always drawn so the overall graph structure remains visible.
|
|
475
|
+
|
|
476
|
+
### Recommended settings for large graphs
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// Good starting point for graphs with 1,000 – 10,000+ elements
|
|
480
|
+
canvas.setConfig({
|
|
481
|
+
cooldownTicks: 300, // limit physics simulation ticks
|
|
482
|
+
largeGraph: {
|
|
483
|
+
enabled: true,
|
|
484
|
+
viewportPadding: 100, // pre-render elements slightly off-screen to avoid pop-in
|
|
485
|
+
lowZoomThreshold: 0.4, // tune to match your typical minimum zoom
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Backward compatibility
|
|
491
|
+
|
|
492
|
+
The feature is **disabled by default** (`enabled: false` / property absent). Existing code that does not set `largeGraph` is completely unaffected.
|
|
435
493
|
|
|
436
494
|
## Debugging
|
|
437
495
|
|
package/dist/canvas-types.d.ts
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
import { NodeObject } from "force-graph";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for large-graph rendering optimisations.
|
|
4
|
+
* All options are optional; the feature is disabled by default (`enabled: false`).
|
|
5
|
+
*/
|
|
6
|
+
export interface LargeGraphConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Master switch. When `false` (default) the component behaves exactly as before –
|
|
9
|
+
* no culling is applied and no draw calls are skipped.
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Extra padding added around the visible viewport (in world-space units) when
|
|
14
|
+
* deciding whether a node or link is offscreen. Increase this to pre-render
|
|
15
|
+
* elements just outside the visible area and avoid pop-in during fast panning.
|
|
16
|
+
* Default: `0`.
|
|
17
|
+
*/
|
|
18
|
+
viewportPadding?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Zoom level below which expensive per-element details are skipped.
|
|
21
|
+
* At a zoom of `1` each world-unit covers one screen pixel; at `0.5` elements
|
|
22
|
+
* are drawn at half size, making labels and arrows too small to be useful.
|
|
23
|
+
* Default: `0.5`.
|
|
24
|
+
*/
|
|
25
|
+
lowZoomThreshold?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Skip drawing node labels when the current zoom is below `lowZoomThreshold`.
|
|
28
|
+
* Node circles are still drawn so the graph shape remains visible.
|
|
29
|
+
* Default: `true`.
|
|
30
|
+
*/
|
|
31
|
+
skipLabelsAtLowZoom?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Skip drawing link arrowheads when the current zoom is below `lowZoomThreshold`.
|
|
34
|
+
* Default: `true`.
|
|
35
|
+
*/
|
|
36
|
+
skipArrowsAtLowZoom?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Skip drawing link relationship labels when the current zoom is below `lowZoomThreshold`.
|
|
39
|
+
* Default: `true`.
|
|
40
|
+
*/
|
|
41
|
+
skipLinkLabelsAtLowZoom?: boolean;
|
|
42
|
+
}
|
|
2
43
|
export interface ForceGraphConfig {
|
|
3
44
|
width?: number;
|
|
4
45
|
height?: number;
|
|
@@ -34,6 +75,8 @@ export interface ForceGraphConfig {
|
|
|
34
75
|
linkCanvasObject: (link: GraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => void;
|
|
35
76
|
linkPointerAreaPaint: (link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => void;
|
|
36
77
|
};
|
|
78
|
+
/** Large-graph rendering optimisations (viewport culling and low-zoom draw skipping). */
|
|
79
|
+
largeGraph?: LargeGraphConfig;
|
|
37
80
|
}
|
|
38
81
|
export interface InternalForceGraphConfig extends Omit<ForceGraphConfig, 'backgroundColor' | 'foregroundColor' | 'captionsKeys' | 'showPropertyKeyPrefix' | 'layoutMode' | 'layoutOptions'> {
|
|
39
82
|
backgroundColor: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-types.d.ts","sourceRoot":"","sources":["../src/canvas-types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3D,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC;IAC9C,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE;QACL,gBAAgB,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,wBAAwB,KAAK,IAAI,CAAC;QAC3E,oBAAoB,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,wBAAwB,KAAK,IAAI,CAAC;KAC/F,CAAC;IACF,IAAI,CAAC,EAAE;QACL,gBAAgB,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;QAChG,oBAAoB,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,wBAAwB,KAAK,IAAI,CAAC;KAC/F,CAAC;
|
|
1
|
+
{"version":3,"file":"canvas-types.d.ts","sourceRoot":"","sources":["../src/canvas-types.ts"],"names":[],"mappings":"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"}
|
package/dist/canvas.d.ts
CHANGED
|
@@ -12,6 +12,16 @@ declare class FalkorDBCanvas extends HTMLElement {
|
|
|
12
12
|
private nodeDegreeMap;
|
|
13
13
|
private nodeDisplayFontSize;
|
|
14
14
|
private relationshipsTextCache;
|
|
15
|
+
/**
|
|
16
|
+
* Cached world-space axis-aligned bounding box of the currently visible
|
|
17
|
+
* viewport. Updated on every zoom/pan event and on resize.
|
|
18
|
+
* `null` means culling is disabled or not yet computed.
|
|
19
|
+
*/
|
|
20
|
+
private cullingBounds;
|
|
21
|
+
/** Current zoom level, cached alongside cullingBounds. */
|
|
22
|
+
private cullingZoom;
|
|
23
|
+
/** Last d3-zoom transform, cached so bounds can be recomputed on resize. */
|
|
24
|
+
private lastTransform;
|
|
15
25
|
private onFontsLoadingDone;
|
|
16
26
|
private viewport;
|
|
17
27
|
private shouldZoomToFitOnNonForceSettle;
|
|
@@ -63,6 +73,35 @@ declare class FalkorDBCanvas extends HTMLElement {
|
|
|
63
73
|
private setupResizeObserver;
|
|
64
74
|
private initGraph;
|
|
65
75
|
private setupForces;
|
|
76
|
+
/**
|
|
77
|
+
* Recompute the world-space culling bounds from the d3-zoom transform delivered
|
|
78
|
+
* by force-graph's `onZoom` callback.
|
|
79
|
+
*
|
|
80
|
+
* The d3-zoom transform maps world → screen as:
|
|
81
|
+
* screen_x = world_x * k + tx
|
|
82
|
+
* screen_y = world_y * k + ty
|
|
83
|
+
* Inverting for the canvas edges (screen_x ∈ [0, W], screen_y ∈ [0, H]):
|
|
84
|
+
* world_x ∈ [(0 − tx) / k, (W − tx) / k]
|
|
85
|
+
* world_y ∈ [(0 − ty) / k, (H − ty) / k]
|
|
86
|
+
*/
|
|
87
|
+
private updateCullingBounds;
|
|
88
|
+
/** Recompute culling bounds using the last known transform (e.g. after resize). */
|
|
89
|
+
private recomputeCullingBoundsIfNeeded;
|
|
90
|
+
/**
|
|
91
|
+
* Returns `true` when the node is (at least partially) inside the current
|
|
92
|
+
* culling viewport, or when culling is disabled / bounds are not yet known.
|
|
93
|
+
*/
|
|
94
|
+
private isNodeInCullingBounds;
|
|
95
|
+
/**
|
|
96
|
+
* Returns `true` when a link's visual representation overlaps the current
|
|
97
|
+
* culling viewport, or when culling is disabled / bounds are not yet known.
|
|
98
|
+
*
|
|
99
|
+
* For straight / quadratic-bezier links the test uses the convex-hull bounding
|
|
100
|
+
* box of (source, control point, target), which is always a conservative
|
|
101
|
+
* (never-false-negative) bound. For self-loops the test uses a square of
|
|
102
|
+
* side ≈ the loop diameter centred on the node.
|
|
103
|
+
*/
|
|
104
|
+
private isLinkInCullingBounds;
|
|
66
105
|
private handleNodeDrag;
|
|
67
106
|
private handleNodeDragEnd;
|
|
68
107
|
private drawNode;
|
package/dist/canvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,IAAI,EACJ,kBAAkB,EAClB,SAAS,EAET,SAAS,EACT,gBAAgB,EAChB,aAAa,EAId,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
|
package/dist/canvas.js
CHANGED
|
@@ -81,6 +81,16 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
81
81
|
// Per-node font size cache: computed once per node, read every frame.
|
|
82
82
|
this.nodeDisplayFontSize = new Map();
|
|
83
83
|
this.relationshipsTextCache = new Map();
|
|
84
|
+
/**
|
|
85
|
+
* Cached world-space axis-aligned bounding box of the currently visible
|
|
86
|
+
* viewport. Updated on every zoom/pan event and on resize.
|
|
87
|
+
* `null` means culling is disabled or not yet computed.
|
|
88
|
+
*/
|
|
89
|
+
this.cullingBounds = null;
|
|
90
|
+
/** Current zoom level, cached alongside cullingBounds. */
|
|
91
|
+
this.cullingZoom = 1;
|
|
92
|
+
/** Last d3-zoom transform, cached so bounds can be recomputed on resize. */
|
|
93
|
+
this.lastTransform = null;
|
|
84
94
|
this.onFontsLoadingDone = () => {
|
|
85
95
|
this.relationshipsTextCache.clear();
|
|
86
96
|
this.nodeDisplayFontSize.clear();
|
|
@@ -151,7 +161,23 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
151
161
|
node.displayName = ["", ""];
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
|
-
|
|
164
|
+
// Deep-merge largeGraph to avoid wiping sibling fields on partial updates.
|
|
165
|
+
if (config.largeGraph && typeof config.largeGraph === 'object' && this.config.largeGraph) {
|
|
166
|
+
const mergedLargeGraph = { ...this.config.largeGraph, ...config.largeGraph };
|
|
167
|
+
Object.assign(this.config, config, { largeGraph: mergedLargeGraph });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
Object.assign(this.config, config);
|
|
171
|
+
}
|
|
172
|
+
// Recompute or clear culling bounds when largeGraph config changes.
|
|
173
|
+
if ('largeGraph' in config) {
|
|
174
|
+
if (this.config.largeGraph?.enabled) {
|
|
175
|
+
this.recomputeCullingBoundsIfNeeded();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.cullingBounds = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
155
181
|
if (layoutChanged) {
|
|
156
182
|
const previousPositions = this.getNodePositionMap();
|
|
157
183
|
if (this.isForceLayoutMode() && this.config.cooldownTicks === 0 && this.data.nodes.length > 0) {
|
|
@@ -206,6 +232,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
206
232
|
this.config.width = width;
|
|
207
233
|
if (this.graph) {
|
|
208
234
|
this.graph.width(width);
|
|
235
|
+
this.recomputeCullingBoundsIfNeeded();
|
|
209
236
|
}
|
|
210
237
|
}
|
|
211
238
|
setHeight(height) {
|
|
@@ -215,6 +242,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
215
242
|
this.config.height = height;
|
|
216
243
|
if (this.graph) {
|
|
217
244
|
this.graph.height(height);
|
|
245
|
+
this.recomputeCullingBoundsIfNeeded();
|
|
218
246
|
}
|
|
219
247
|
}
|
|
220
248
|
setBackgroundColor(color) {
|
|
@@ -336,40 +364,23 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
336
364
|
}
|
|
337
365
|
setGraphData(data) {
|
|
338
366
|
this.log('setGraphData called with', data.nodes.length, 'nodes and', data.links.length, 'links');
|
|
339
|
-
const previousPositions = this.getNodePositionMap();
|
|
340
367
|
this.data = applyGraphLayout(data, this.config.layoutMode, this.config.layoutOptions);
|
|
341
|
-
|
|
342
|
-
if (this.isForceLayoutMode() && this.config.cooldownTicks === 0 && this.data.nodes.length > 0) {
|
|
343
|
-
this.config.cooldownTicks = undefined;
|
|
344
|
-
this.shouldZoomToFitOnNonForceSettle = false;
|
|
345
|
-
}
|
|
368
|
+
this.shouldZoomToFitOnNonForceSettle = false;
|
|
346
369
|
if (!this.graph)
|
|
347
370
|
return;
|
|
348
371
|
this.calculateNodeDegree();
|
|
349
372
|
this.graph
|
|
350
373
|
.graphData(this.data);
|
|
351
|
-
|
|
352
|
-
|
|
374
|
+
// setGraphData restores pre-positioned data — freeze simulation, just render.
|
|
375
|
+
this.config.cooldownTicks = 0;
|
|
376
|
+
this.graph.cooldownTicks(0);
|
|
377
|
+
this.updateCanvasSimulationAttribute(false);
|
|
378
|
+
if (this.data.nodes.length > 0) {
|
|
353
379
|
this.triggerRender();
|
|
354
380
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.updateLoadingState();
|
|
359
|
-
if (this.data.nodes.length > 0) {
|
|
360
|
-
if (shouldAnimateNonForceLayout) {
|
|
361
|
-
this.shouldZoomToFitOnNonForceSettle = true;
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
this.shouldZoomToFitOnNonForceSettle = false;
|
|
365
|
-
this.zoomToFit(1);
|
|
366
|
-
this.triggerRender();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
this.shouldZoomToFitOnNonForceSettle = false;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
381
|
+
this.config.isLoading = false;
|
|
382
|
+
this.config.onLoadingChange?.(false);
|
|
383
|
+
this.updateLoadingState();
|
|
373
384
|
if (this.viewport) {
|
|
374
385
|
this.log('Applying viewport:', this.viewport);
|
|
375
386
|
this.graph.zoom(this.viewport.zoom, 0);
|
|
@@ -679,6 +690,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
679
690
|
if (this.graph && width > 0 && height > 0) {
|
|
680
691
|
this.log('Container resized to:', width, 'x', height);
|
|
681
692
|
this.graph.width(width).height(height);
|
|
693
|
+
this.recomputeCullingBoundsIfNeeded();
|
|
682
694
|
}
|
|
683
695
|
}
|
|
684
696
|
});
|
|
@@ -757,6 +769,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
757
769
|
}
|
|
758
770
|
})
|
|
759
771
|
.onZoom((transform) => {
|
|
772
|
+
this.updateCullingBounds(transform);
|
|
760
773
|
if (this.config.onZoom) {
|
|
761
774
|
this.config.onZoom(transform);
|
|
762
775
|
}
|
|
@@ -841,6 +854,118 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
841
854
|
}
|
|
842
855
|
this.log('Force simulation setup complete');
|
|
843
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Recompute the world-space culling bounds from the d3-zoom transform delivered
|
|
859
|
+
* by force-graph's `onZoom` callback.
|
|
860
|
+
*
|
|
861
|
+
* The d3-zoom transform maps world → screen as:
|
|
862
|
+
* screen_x = world_x * k + tx
|
|
863
|
+
* screen_y = world_y * k + ty
|
|
864
|
+
* Inverting for the canvas edges (screen_x ∈ [0, W], screen_y ∈ [0, H]):
|
|
865
|
+
* world_x ∈ [(0 − tx) / k, (W − tx) / k]
|
|
866
|
+
* world_y ∈ [(0 − ty) / k, (H − ty) / k]
|
|
867
|
+
*/
|
|
868
|
+
updateCullingBounds(transform) {
|
|
869
|
+
this.lastTransform = transform;
|
|
870
|
+
if (!this.config.largeGraph?.enabled) {
|
|
871
|
+
this.cullingBounds = null;
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const w = this.graph?.width() ?? 0;
|
|
875
|
+
const h = this.graph?.height() ?? 0;
|
|
876
|
+
const { k, x: tx, y: ty } = transform;
|
|
877
|
+
if (k <= 0 || w <= 0 || h <= 0) {
|
|
878
|
+
this.cullingBounds = null;
|
|
879
|
+
this.cullingZoom = 1;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const padding = this.config.largeGraph?.viewportPadding ?? 0;
|
|
883
|
+
this.cullingBounds = {
|
|
884
|
+
minX: -tx / k - padding,
|
|
885
|
+
maxX: (w - tx) / k + padding,
|
|
886
|
+
minY: -ty / k - padding,
|
|
887
|
+
maxY: (h - ty) / k + padding,
|
|
888
|
+
};
|
|
889
|
+
this.cullingZoom = k;
|
|
890
|
+
}
|
|
891
|
+
/** Recompute culling bounds using the last known transform (e.g. after resize). */
|
|
892
|
+
recomputeCullingBoundsIfNeeded() {
|
|
893
|
+
if (!this.config.largeGraph?.enabled)
|
|
894
|
+
return;
|
|
895
|
+
if (this.lastTransform) {
|
|
896
|
+
this.updateCullingBounds(this.lastTransform);
|
|
897
|
+
}
|
|
898
|
+
else if (this.graph) {
|
|
899
|
+
// Seed initial transform from current graph state before first onZoom fires.
|
|
900
|
+
const k = this.graph.zoom() ?? 1;
|
|
901
|
+
const center = this.graph.centerAt() ?? { x: 0, y: 0 };
|
|
902
|
+
const w = this.graph.width() ?? 0;
|
|
903
|
+
const h = this.graph.height() ?? 0;
|
|
904
|
+
if (k > 0 && w > 0 && h > 0) {
|
|
905
|
+
const tx = w / 2 - center.x * k;
|
|
906
|
+
const ty = h / 2 - center.y * k;
|
|
907
|
+
this.updateCullingBounds({ k, x: tx, y: ty });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Returns `true` when the node is (at least partially) inside the current
|
|
913
|
+
* culling viewport, or when culling is disabled / bounds are not yet known.
|
|
914
|
+
*/
|
|
915
|
+
isNodeInCullingBounds(node) {
|
|
916
|
+
if (!this.cullingBounds)
|
|
917
|
+
return true;
|
|
918
|
+
const { minX, maxX, minY, maxY } = this.cullingBounds;
|
|
919
|
+
const r = node.size + PADDING;
|
|
920
|
+
const x = node.x ?? 0;
|
|
921
|
+
const y = node.y ?? 0;
|
|
922
|
+
return x + r >= minX && x - r <= maxX && y + r >= minY && y - r <= maxY;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Returns `true` when a link's visual representation overlaps the current
|
|
926
|
+
* culling viewport, or when culling is disabled / bounds are not yet known.
|
|
927
|
+
*
|
|
928
|
+
* For straight / quadratic-bezier links the test uses the convex-hull bounding
|
|
929
|
+
* box of (source, control point, target), which is always a conservative
|
|
930
|
+
* (never-false-negative) bound. For self-loops the test uses a square of
|
|
931
|
+
* side ≈ the loop diameter centred on the node.
|
|
932
|
+
*/
|
|
933
|
+
isLinkInCullingBounds(link) {
|
|
934
|
+
if (!this.cullingBounds)
|
|
935
|
+
return true;
|
|
936
|
+
const { minX, maxX, minY, maxY } = this.cullingBounds;
|
|
937
|
+
const sx = link.source.x ?? 0;
|
|
938
|
+
const sy = link.source.y ?? 0;
|
|
939
|
+
const ex = link.target.x ?? 0;
|
|
940
|
+
const ey = link.target.y ?? 0;
|
|
941
|
+
if (link.source.id === link.target.id) {
|
|
942
|
+
// Self-loop: the cubic bezier extends roughly |curve| * nodeSize * factor
|
|
943
|
+
// away from the node centre. Use that as a conservative radius.
|
|
944
|
+
const nodeSize = link.source.size || NODE_SIZE;
|
|
945
|
+
const loopRadius = Math.abs(link.curve || 1) * nodeSize * SELF_LOOP_CURVE_FACTOR;
|
|
946
|
+
return (sx + loopRadius >= minX && sx - loopRadius <= maxX &&
|
|
947
|
+
sy + loopRadius >= minY && sy - loopRadius <= maxY);
|
|
948
|
+
}
|
|
949
|
+
// Compute quadratic-bezier control point (same formula as drawLink).
|
|
950
|
+
const dx = ex - sx;
|
|
951
|
+
const dy = ey - sy;
|
|
952
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
953
|
+
if (distance === 0) {
|
|
954
|
+
// Co-located nodes: just check the point.
|
|
955
|
+
return sx >= minX && sx <= maxX && sy >= minY && sy <= maxY;
|
|
956
|
+
}
|
|
957
|
+
const curvature = link.curve ?? 0;
|
|
958
|
+
const perpX = dy / distance;
|
|
959
|
+
const perpY = -dx / distance;
|
|
960
|
+
const cx = (sx + ex) / 2 + perpX * curvature * distance;
|
|
961
|
+
const cy = (sy + ey) / 2 + perpY * curvature * distance;
|
|
962
|
+
// Convex-hull AABB of the three control points.
|
|
963
|
+
const lMinX = Math.min(sx, ex, cx);
|
|
964
|
+
const lMaxX = Math.max(sx, ex, cx);
|
|
965
|
+
const lMinY = Math.min(sy, ey, cy);
|
|
966
|
+
const lMaxY = Math.max(sy, ey, cy);
|
|
967
|
+
return lMaxX >= minX && lMinX <= maxX && lMaxY >= minY && lMinY <= maxY;
|
|
968
|
+
}
|
|
844
969
|
handleNodeDrag(node) {
|
|
845
970
|
if (this.isForceLayoutMode())
|
|
846
971
|
return;
|
|
@@ -868,6 +993,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
868
993
|
node.x = 0;
|
|
869
994
|
node.y = 0;
|
|
870
995
|
}
|
|
996
|
+
// Viewport culling: skip nodes that are entirely outside the visible area.
|
|
997
|
+
if (this.config.largeGraph?.enabled && !this.isNodeInCullingBounds(node))
|
|
998
|
+
return;
|
|
871
999
|
ctx.lineWidth = this.config.isNodeSelected?.(node) ? 1 : 0.5;
|
|
872
1000
|
ctx.strokeStyle = this.config.foregroundColor;
|
|
873
1001
|
ctx.fillStyle = node.color;
|
|
@@ -878,6 +1006,12 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
878
1006
|
ctx.beginPath();
|
|
879
1007
|
ctx.arc(node.x, node.y, node.size, 0, 2 * Math.PI, false);
|
|
880
1008
|
ctx.fill();
|
|
1009
|
+
// Low-zoom optimisation: skip labels when they would be too small to read.
|
|
1010
|
+
const skipLabels = this.config.largeGraph?.enabled &&
|
|
1011
|
+
(this.config.largeGraph?.skipLabelsAtLowZoom ?? true) &&
|
|
1012
|
+
this.cullingZoom < (this.config.largeGraph?.lowZoomThreshold ?? 0.5);
|
|
1013
|
+
if (skipLabels)
|
|
1014
|
+
return;
|
|
881
1015
|
// Draw text
|
|
882
1016
|
ctx.fillStyle = getContrastTextColor(node.color);
|
|
883
1017
|
ctx.textAlign = "center";
|
|
@@ -963,6 +1097,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
963
1097
|
node.y = 0;
|
|
964
1098
|
}
|
|
965
1099
|
;
|
|
1100
|
+
// Viewport culling: skip hit-test painting for offscreen nodes.
|
|
1101
|
+
if (this.config.largeGraph?.enabled && !this.isNodeInCullingBounds(node))
|
|
1102
|
+
return;
|
|
966
1103
|
const radius = node.size + PADDING;
|
|
967
1104
|
ctx.fillStyle = color;
|
|
968
1105
|
ctx.beginPath();
|
|
@@ -978,11 +1115,21 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
978
1115
|
end.x = 0;
|
|
979
1116
|
end.y = 0;
|
|
980
1117
|
}
|
|
1118
|
+
// Viewport culling: skip links whose visual extent is entirely outside the
|
|
1119
|
+
// visible area. The check is conservative (convex-hull AABB) so it never
|
|
1120
|
+
// produces false negatives.
|
|
1121
|
+
if (this.config.largeGraph?.enabled && !this.isLinkInCullingBounds(link))
|
|
1122
|
+
return;
|
|
981
1123
|
let textX;
|
|
982
1124
|
let textY;
|
|
983
1125
|
let angle;
|
|
984
1126
|
const isLinkSelected = this.config.isLinkSelected?.(link) ?? false;
|
|
985
1127
|
const arrowLen = isLinkSelected ? 4 : 2;
|
|
1128
|
+
// Low-zoom flags – evaluated once per link draw.
|
|
1129
|
+
const lowZoomThreshold = this.config.largeGraph?.lowZoomThreshold ?? 0.5;
|
|
1130
|
+
const atLowZoom = this.config.largeGraph?.enabled && this.cullingZoom < lowZoomThreshold;
|
|
1131
|
+
const skipArrows = atLowZoom && (this.config.largeGraph?.skipArrowsAtLowZoom ?? true);
|
|
1132
|
+
const skipLinkLabels = atLowZoom && (this.config.largeGraph?.skipLinkLabelsAtLowZoom ?? true);
|
|
986
1133
|
// Deferred arrowhead — drawn after the label so it is never covered by
|
|
987
1134
|
// the label background rect (which happens for short links where the
|
|
988
1135
|
// bezier midpoint and the arrow tip are at almost the same position).
|
|
@@ -1050,7 +1197,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1050
1197
|
// Guard against zero-length tangent vector (e.g. when d ≈ 0) to avoid NaN
|
|
1051
1198
|
// normals and invalid arrowhead geometry. Also skip when d is too small to
|
|
1052
1199
|
// place the arrowhead at the node border (canReachBorder is false).
|
|
1053
|
-
if (tLen !== 0 && canReachBorder) {
|
|
1200
|
+
if (!skipArrows && tLen !== 0 && canReachBorder) {
|
|
1054
1201
|
const nx = tdx / tLen;
|
|
1055
1202
|
const ny = tdy / tLen;
|
|
1056
1203
|
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
@@ -1176,7 +1323,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1176
1323
|
const atx = 2 * uArrow * (controlX - start.x) + 2 * tArrow * (end.x - controlX);
|
|
1177
1324
|
const aty = 2 * uArrow * (controlY - start.y) + 2 * tArrow * (end.y - controlY);
|
|
1178
1325
|
const atLen = Math.sqrt(atx * atx + aty * aty);
|
|
1179
|
-
if (atLen !== 0) {
|
|
1326
|
+
if (!skipArrows && atLen !== 0) {
|
|
1180
1327
|
const nx = atx / atLen;
|
|
1181
1328
|
const ny = aty / atLen;
|
|
1182
1329
|
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
@@ -1186,40 +1333,42 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1186
1333
|
ctx.textAlign = "center";
|
|
1187
1334
|
// Draw text with alphabetic baseline, positioned so visual center is at y=0
|
|
1188
1335
|
ctx.textBaseline = "alphabetic";
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1336
|
+
if (!skipLinkLabels) {
|
|
1337
|
+
// Separate cache entries per weight so each state is measured with its own
|
|
1338
|
+
// font, giving equal visual padding regardless of selection state.
|
|
1339
|
+
const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
|
|
1340
|
+
let cached = this.relationshipsTextCache.get(cacheKey);
|
|
1341
|
+
if (!cached) {
|
|
1342
|
+
// ctx.font is already set to the correct weight above; measure it directly.
|
|
1343
|
+
const metrics = ctx.measureText(link.relationship);
|
|
1344
|
+
// Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
|
|
1345
|
+
// line-box and adds excessive space for lighter weights.
|
|
1346
|
+
// Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
|
|
1347
|
+
// unreliable with textAlign="center" and can double the value on some engines.
|
|
1348
|
+
const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
|
|
1349
|
+
const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
|
|
1350
|
+
const inkWidth = metrics.width;
|
|
1351
|
+
const bgPadding = 0.3;
|
|
1352
|
+
cached = {
|
|
1353
|
+
textWidth: inkWidth + bgPadding * 2,
|
|
1354
|
+
textHeight: inkAscent + inkDescent + bgPadding * 2,
|
|
1355
|
+
// Shift baseline up so the ink block is centred inside the bg rect.
|
|
1356
|
+
textYOffset: (inkAscent - inkDescent) / 2,
|
|
1357
|
+
};
|
|
1358
|
+
this.relationshipsTextCache.set(cacheKey, cached);
|
|
1359
|
+
}
|
|
1360
|
+
const { textWidth, textHeight, textYOffset } = cached;
|
|
1361
|
+
ctx.save();
|
|
1362
|
+
ctx.translate(textX, textY);
|
|
1363
|
+
ctx.rotate(angle);
|
|
1364
|
+
// Draw background centered on the link line (y=0)
|
|
1365
|
+
ctx.fillStyle = this.config.backgroundColor;
|
|
1366
|
+
// Offset background to match text visual center
|
|
1367
|
+
ctx.fillRect(-textWidth / 2, -textHeight / 2, textWidth, textHeight);
|
|
1368
|
+
ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
|
|
1369
|
+
ctx.fillText(link.relationship, 0, textYOffset);
|
|
1370
|
+
ctx.restore();
|
|
1371
|
+
}
|
|
1223
1372
|
// Draw arrowhead last so it always appears on top of the label background.
|
|
1224
1373
|
if (pendingArrow) {
|
|
1225
1374
|
const { tipX, tipY, nx, ny, arrowLen: aLen, arrowHalfWidth: aHW } = pendingArrow;
|
|
@@ -1237,6 +1386,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1237
1386
|
const end = link.target;
|
|
1238
1387
|
if (start.x == null || start.y == null || end.x == null || end.y == null)
|
|
1239
1388
|
return;
|
|
1389
|
+
// Viewport culling: skip hit-test painting for offscreen links.
|
|
1390
|
+
if (this.config.largeGraph?.enabled && !this.isLinkInCullingBounds(link))
|
|
1391
|
+
return;
|
|
1240
1392
|
ctx.strokeStyle = color;
|
|
1241
1393
|
const basePointerWidth = 10; // Desired on-screen pointer area thickness
|
|
1242
1394
|
const transform = typeof ctx.getTransform === 'function' ? ctx.getTransform() : null;
|
|
@@ -1482,6 +1634,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1482
1634
|
}
|
|
1483
1635
|
})
|
|
1484
1636
|
.onZoom((transform) => {
|
|
1637
|
+
this.updateCullingBounds(transform);
|
|
1485
1638
|
if (this.config.onZoom) {
|
|
1486
1639
|
this.config.onZoom(transform);
|
|
1487
1640
|
}
|
|
@@ -1514,7 +1667,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1514
1667
|
});
|
|
1515
1668
|
}
|
|
1516
1669
|
else {
|
|
1517
|
-
this.graph.nodePointerAreaPaint()
|
|
1670
|
+
this.graph.nodePointerAreaPaint((node, color, ctx) => {
|
|
1671
|
+
this.pointerNode(node, color, ctx);
|
|
1672
|
+
});
|
|
1518
1673
|
}
|
|
1519
1674
|
if (this.config.link) {
|
|
1520
1675
|
this.graph.linkPointerAreaPaint((link, color, ctx) => {
|
|
@@ -1522,7 +1677,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1522
1677
|
});
|
|
1523
1678
|
}
|
|
1524
1679
|
else {
|
|
1525
|
-
this.graph.linkPointerAreaPaint()
|
|
1680
|
+
this.graph.linkPointerAreaPaint((link, color, ctx) => {
|
|
1681
|
+
this.pointerLink(link, color, ctx);
|
|
1682
|
+
});
|
|
1526
1683
|
}
|
|
1527
1684
|
}
|
|
1528
1685
|
updateTooltipStyles() {
|