@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 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
 
@@ -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":"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"}
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;
@@ -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;AA0E3B,cAAM,cAAe,SAAQ,WAAW;IACtC,OAAO,CAAC,KAAK,CAAqB;IAElC,OAAO,CAAC,SAAS,CAA+B;IAEhD,OAAO,CAAC,cAAc,CAA+B;IAErD,OAAO,CAAC,cAAc,CAA+B;IAErD,OAAO,CAAC,IAAI,CAAuC;IAEnD,OAAO,CAAC,YAAY,CAAkB;IAEtC,OAAO,CAAC,MAAM,CAOZ;IAEF,OAAO,CAAC,QAAQ,CAA+B;IAE/C,OAAO,CAAC,QAAQ,CAA+B;IAE/C,OAAO,CAAC,aAAa,CAAkC;IAGvD,OAAO,CAAC,mBAAmB,CAAkC;IAE7D,OAAO,CAAC,sBAAsB,CAOhB;IAEd,OAAO,CAAC,kBAAkB,CAOxB;IAEF,OAAO,CAAC,QAAQ,CAAgB;IAEhC,OAAO,CAAC,+BAA+B,CAAkB;;IAOzD;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO;IAMzB;;;OAGG;IACH,OAAO,CAAC,GAAG;IAMX,iBAAiB;IAuBjB,oBAAoB;IAYpB,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC;IA6D3C,QAAQ,CAAC,KAAK,EAAE,MAAM;IAStB,SAAS,CAAC,MAAM,EAAE,MAAM;IASxB,kBAAkB,CAAC,KAAK,EAAE,MAAM;IAahC,kBAAkB,CAAC,KAAK,EAAE,MAAM;IAQhC,YAAY,CAAC,SAAS,EAAE,OAAO;IAO/B,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;IAa1C,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,IAAI,EAAE,IAAI;IA8DlB,WAAW,IAAI,aAAa;IAc5B,WAAW,CAAC,QAAQ,EAAE,aAAa;IAKnC,YAAY,IAAI,SAAS;IAIzB,YAAY,CAAC,IAAI,EAAE,SAAS;IA8C5B,QAAQ,IAAI,kBAAkB,GAAG,SAAS;IAInC,OAAO,IAAI,MAAM;IAIjB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAOvD,SAAS,CAAC,iBAAiB,SAAI,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO;IAkB7E,OAAO,CAAC,iBAAiB;IAGzB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,4BAA4B;IAkCpC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,oCAAoC;IAkD5C,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,4BAA4B;IAQpC,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,mCAAmC;IAoB3C,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,+BAA+B;IAUvC,OAAO,CAAC,mBAAmB;IAsB3B,OAAO,CAAC,oBAAoB;IAmE5B,OAAO,CAAC,MAAM;IAuBd,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,SAAS;IAuHjB,OAAO,CAAC,WAAW;IAqDnB,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,QAAQ;IA+GhB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,QAAQ;IA8ShB,OAAO,CAAC,WAAW;IA4InB,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,gBAAgB;IAyDxB,OAAO,CAAC,mBAAmB;IA6F3B,OAAO,CAAC,mBAAmB;CAS5B;AAOD,eAAe,cAAc,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
- Object.assign(this.config, config);
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
- const shouldAnimateNonForceLayout = this.prepareNodePositionsForCurrentLayout(previousPositions);
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
- this.configureSimulationForCurrentLayout(shouldAnimateNonForceLayout);
352
- if (this.isForceLayoutMode() && this.data.nodes.length > 0) {
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
- if (!this.isForceLayoutMode()) {
356
- this.config.isLoading = false;
357
- this.config.onLoadingChange?.(false);
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
- // Separate cache entries per weight so each state is measured with its own
1190
- // font, giving equal visual padding regardless of selection state.
1191
- const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
1192
- let cached = this.relationshipsTextCache.get(cacheKey);
1193
- if (!cached) {
1194
- // ctx.font is already set to the correct weight above; measure it directly.
1195
- const metrics = ctx.measureText(link.relationship);
1196
- // Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
1197
- // line-box and adds excessive space for lighter weights.
1198
- // Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
1199
- // unreliable with textAlign="center" and can double the value on some engines.
1200
- const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
1201
- const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
1202
- const inkWidth = metrics.width;
1203
- const bgPadding = 0.3;
1204
- cached = {
1205
- textWidth: inkWidth + bgPadding * 2,
1206
- textHeight: inkAscent + inkDescent + bgPadding * 2,
1207
- // Shift baseline up so the ink block is centred inside the bg rect.
1208
- textYOffset: (inkAscent - inkDescent) / 2,
1209
- };
1210
- this.relationshipsTextCache.set(cacheKey, cached);
1211
- }
1212
- const { textWidth, textHeight, textYOffset } = cached;
1213
- ctx.save();
1214
- ctx.translate(textX, textY);
1215
- ctx.rotate(angle);
1216
- // Draw background centered on the link line (y=0)
1217
- ctx.fillStyle = this.config.backgroundColor;
1218
- // Offset background to match text visual center
1219
- ctx.fillRect(-textWidth / 2, -textHeight / 2, textWidth, textHeight);
1220
- ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
1221
- ctx.fillText(link.relationship, 0, textYOffset);
1222
- ctx.restore();
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() {