@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/src/canvas.ts CHANGED
@@ -52,6 +52,14 @@ const NON_FORCE_DRAG_COOLDOWN_TICKS = 90;
52
52
 
53
53
  type NodePosition = { x: number; y: number };
54
54
 
55
+ /** Axis-aligned bounding box in world-space coordinates. */
56
+ type WorldBounds = {
57
+ minX: number;
58
+ maxX: number;
59
+ minY: number;
60
+ maxY: number;
61
+ };
62
+
55
63
  // Create styles for the web component
56
64
  function createStyles(backgroundColor: string, foregroundColor: string): HTMLStyleElement {
57
65
  const style = document.createElement("style");
@@ -125,6 +133,19 @@ class FalkorDBCanvas extends HTMLElement {
125
133
  }
126
134
  > = new Map();
127
135
 
136
+ /**
137
+ * Cached world-space axis-aligned bounding box of the currently visible
138
+ * viewport. Updated on every zoom/pan event and on resize.
139
+ * `null` means culling is disabled or not yet computed.
140
+ */
141
+ private cullingBounds: WorldBounds | null = null;
142
+
143
+ /** Current zoom level, cached alongside cullingBounds. */
144
+ private cullingZoom: number = 1;
145
+
146
+ /** Last d3-zoom transform, cached so bounds can be recomputed on resize. */
147
+ private lastTransform: Transform | null = null;
148
+
128
149
  private onFontsLoadingDone = () => {
129
150
  this.relationshipsTextCache.clear();
130
151
  this.nodeDisplayFontSize.clear();
@@ -211,7 +232,22 @@ class FalkorDBCanvas extends HTMLElement {
211
232
  }
212
233
  }
213
234
 
214
- Object.assign(this.config, config);
235
+ // Deep-merge largeGraph to avoid wiping sibling fields on partial updates.
236
+ if (config.largeGraph && typeof config.largeGraph === 'object' && this.config.largeGraph) {
237
+ const mergedLargeGraph = { ...this.config.largeGraph, ...config.largeGraph };
238
+ Object.assign(this.config, config, { largeGraph: mergedLargeGraph });
239
+ } else {
240
+ Object.assign(this.config, config);
241
+ }
242
+
243
+ // Recompute or clear culling bounds when largeGraph config changes.
244
+ if ('largeGraph' in config) {
245
+ if (this.config.largeGraph?.enabled) {
246
+ this.recomputeCullingBoundsIfNeeded();
247
+ } else {
248
+ this.cullingBounds = null;
249
+ }
250
+ }
215
251
 
216
252
  if (layoutChanged) {
217
253
  const previousPositions = this.getNodePositionMap();
@@ -265,6 +301,7 @@ class FalkorDBCanvas extends HTMLElement {
265
301
  this.config.width = width;
266
302
  if (this.graph) {
267
303
  this.graph.width(width);
304
+ this.recomputeCullingBoundsIfNeeded();
268
305
  }
269
306
  }
270
307
 
@@ -274,6 +311,7 @@ class FalkorDBCanvas extends HTMLElement {
274
311
  this.config.height = height;
275
312
  if (this.graph) {
276
313
  this.graph.height(height);
314
+ this.recomputeCullingBoundsIfNeeded();
277
315
  }
278
316
  }
279
317
 
@@ -409,41 +447,28 @@ class FalkorDBCanvas extends HTMLElement {
409
447
 
410
448
  setGraphData(data: GraphData) {
411
449
  this.log('setGraphData called with', data.nodes.length, 'nodes and', data.links.length, 'links');
412
- const previousPositions = this.getNodePositionMap();
413
450
  this.data = applyGraphLayout(data, this.config.layoutMode, this.config.layoutOptions);
414
- const shouldAnimateNonForceLayout = this.prepareNodePositionsForCurrentLayout(previousPositions);
415
- if (this.isForceLayoutMode() && this.config.cooldownTicks === 0 && this.data.nodes.length > 0) {
416
- this.config.cooldownTicks = undefined;
417
- this.shouldZoomToFitOnNonForceSettle = false;
418
- }
451
+ this.shouldZoomToFitOnNonForceSettle = false;
452
+
419
453
  if (!this.graph) return;
420
454
 
421
455
  this.calculateNodeDegree();
422
456
 
423
457
  this.graph
424
458
  .graphData(this.data);
425
- this.configureSimulationForCurrentLayout(shouldAnimateNonForceLayout);
426
459
 
427
- if (this.isForceLayoutMode() && this.data.nodes.length > 0) {
460
+ // setGraphData restores pre-positioned data freeze simulation, just render.
461
+ this.config.cooldownTicks = 0;
462
+ this.graph.cooldownTicks(0);
463
+ this.updateCanvasSimulationAttribute(false);
464
+
465
+ if (this.data.nodes.length > 0) {
428
466
  this.triggerRender();
429
467
  }
430
468
 
431
- if (!this.isForceLayoutMode()) {
432
- this.config.isLoading = false;
433
- this.config.onLoadingChange?.(false);
434
- this.updateLoadingState();
435
- if (this.data.nodes.length > 0) {
436
- if (shouldAnimateNonForceLayout) {
437
- this.shouldZoomToFitOnNonForceSettle = true;
438
- } else {
439
- this.shouldZoomToFitOnNonForceSettle = false;
440
- this.zoomToFit(1);
441
- this.triggerRender();
442
- }
443
- } else {
444
- this.shouldZoomToFitOnNonForceSettle = false;
445
- }
446
- }
469
+ this.config.isLoading = false;
470
+ this.config.onLoadingChange?.(false);
471
+ this.updateLoadingState();
447
472
 
448
473
  if (this.viewport) {
449
474
  this.log('Applying viewport:', this.viewport);
@@ -833,6 +858,7 @@ class FalkorDBCanvas extends HTMLElement {
833
858
  if (this.graph && width > 0 && height > 0) {
834
859
  this.log('Container resized to:', width, 'x', height);
835
860
  this.graph.width(width).height(height);
861
+ this.recomputeCullingBoundsIfNeeded();
836
862
  }
837
863
  }
838
864
  });
@@ -916,6 +942,7 @@ class FalkorDBCanvas extends HTMLElement {
916
942
  }
917
943
  })
918
944
  .onZoom((transform: Transform) => {
945
+ this.updateCullingBounds(transform);
919
946
  if (this.config.onZoom) {
920
947
  this.config.onZoom(transform);
921
948
  }
@@ -1012,6 +1039,130 @@ class FalkorDBCanvas extends HTMLElement {
1012
1039
  this.log('Force simulation setup complete');
1013
1040
  }
1014
1041
 
1042
+ /**
1043
+ * Recompute the world-space culling bounds from the d3-zoom transform delivered
1044
+ * by force-graph's `onZoom` callback.
1045
+ *
1046
+ * The d3-zoom transform maps world → screen as:
1047
+ * screen_x = world_x * k + tx
1048
+ * screen_y = world_y * k + ty
1049
+ * Inverting for the canvas edges (screen_x ∈ [0, W], screen_y ∈ [0, H]):
1050
+ * world_x ∈ [(0 − tx) / k, (W − tx) / k]
1051
+ * world_y ∈ [(0 − ty) / k, (H − ty) / k]
1052
+ */
1053
+ private updateCullingBounds(transform: Transform) {
1054
+ this.lastTransform = transform;
1055
+ if (!this.config.largeGraph?.enabled) {
1056
+ this.cullingBounds = null;
1057
+ return;
1058
+ }
1059
+
1060
+ const w = this.graph?.width() ?? 0;
1061
+ const h = this.graph?.height() ?? 0;
1062
+ const { k, x: tx, y: ty } = transform;
1063
+
1064
+ if (k <= 0 || w <= 0 || h <= 0) {
1065
+ this.cullingBounds = null;
1066
+ this.cullingZoom = 1;
1067
+ return;
1068
+ }
1069
+
1070
+ const padding = this.config.largeGraph?.viewportPadding ?? 0;
1071
+
1072
+ this.cullingBounds = {
1073
+ minX: -tx / k - padding,
1074
+ maxX: (w - tx) / k + padding,
1075
+ minY: -ty / k - padding,
1076
+ maxY: (h - ty) / k + padding,
1077
+ };
1078
+ this.cullingZoom = k;
1079
+ }
1080
+
1081
+ /** Recompute culling bounds using the last known transform (e.g. after resize). */
1082
+ private recomputeCullingBoundsIfNeeded() {
1083
+ if (!this.config.largeGraph?.enabled) return;
1084
+ if (this.lastTransform) {
1085
+ this.updateCullingBounds(this.lastTransform);
1086
+ } else if (this.graph) {
1087
+ // Seed initial transform from current graph state before first onZoom fires.
1088
+ const k = this.graph.zoom() ?? 1;
1089
+ const center = this.graph.centerAt() ?? { x: 0, y: 0 };
1090
+ const w = this.graph.width() ?? 0;
1091
+ const h = this.graph.height() ?? 0;
1092
+ if (k > 0 && w > 0 && h > 0) {
1093
+ const tx = w / 2 - center.x * k;
1094
+ const ty = h / 2 - center.y * k;
1095
+ this.updateCullingBounds({ k, x: tx, y: ty });
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ /**
1101
+ * Returns `true` when the node is (at least partially) inside the current
1102
+ * culling viewport, or when culling is disabled / bounds are not yet known.
1103
+ */
1104
+ private isNodeInCullingBounds(node: GraphNode): boolean {
1105
+ if (!this.cullingBounds) return true;
1106
+ const { minX, maxX, minY, maxY } = this.cullingBounds;
1107
+ const r = node.size + PADDING;
1108
+ const x = node.x ?? 0;
1109
+ const y = node.y ?? 0;
1110
+ return x + r >= minX && x - r <= maxX && y + r >= minY && y - r <= maxY;
1111
+ }
1112
+
1113
+ /**
1114
+ * Returns `true` when a link's visual representation overlaps the current
1115
+ * culling viewport, or when culling is disabled / bounds are not yet known.
1116
+ *
1117
+ * For straight / quadratic-bezier links the test uses the convex-hull bounding
1118
+ * box of (source, control point, target), which is always a conservative
1119
+ * (never-false-negative) bound. For self-loops the test uses a square of
1120
+ * side ≈ the loop diameter centred on the node.
1121
+ */
1122
+ private isLinkInCullingBounds(link: GraphLink): boolean {
1123
+ if (!this.cullingBounds) return true;
1124
+ const { minX, maxX, minY, maxY } = this.cullingBounds;
1125
+
1126
+ const sx = link.source.x ?? 0;
1127
+ const sy = link.source.y ?? 0;
1128
+ const ex = link.target.x ?? 0;
1129
+ const ey = link.target.y ?? 0;
1130
+
1131
+ if (link.source.id === link.target.id) {
1132
+ // Self-loop: the cubic bezier extends roughly |curve| * nodeSize * factor
1133
+ // away from the node centre. Use that as a conservative radius.
1134
+ const nodeSize = link.source.size || NODE_SIZE;
1135
+ const loopRadius = Math.abs(link.curve || 1) * nodeSize * SELF_LOOP_CURVE_FACTOR;
1136
+ return (
1137
+ sx + loopRadius >= minX && sx - loopRadius <= maxX &&
1138
+ sy + loopRadius >= minY && sy - loopRadius <= maxY
1139
+ );
1140
+ }
1141
+
1142
+ // Compute quadratic-bezier control point (same formula as drawLink).
1143
+ const dx = ex - sx;
1144
+ const dy = ey - sy;
1145
+ const distance = Math.sqrt(dx * dx + dy * dy);
1146
+ if (distance === 0) {
1147
+ // Co-located nodes: just check the point.
1148
+ return sx >= minX && sx <= maxX && sy >= minY && sy <= maxY;
1149
+ }
1150
+
1151
+ const curvature = link.curve ?? 0;
1152
+ const perpX = dy / distance;
1153
+ const perpY = -dx / distance;
1154
+ const cx = (sx + ex) / 2 + perpX * curvature * distance;
1155
+ const cy = (sy + ey) / 2 + perpY * curvature * distance;
1156
+
1157
+ // Convex-hull AABB of the three control points.
1158
+ const lMinX = Math.min(sx, ex, cx);
1159
+ const lMaxX = Math.max(sx, ex, cx);
1160
+ const lMinY = Math.min(sy, ey, cy);
1161
+ const lMaxY = Math.max(sy, ey, cy);
1162
+
1163
+ return lMaxX >= minX && lMinX <= maxX && lMaxY >= minY && lMinY <= maxY;
1164
+ }
1165
+
1015
1166
  private handleNodeDrag(node: GraphNode) {
1016
1167
  if (this.isForceLayoutMode()) return;
1017
1168
  if (node.x === undefined || node.y === undefined) return;
@@ -1041,6 +1192,9 @@ class FalkorDBCanvas extends HTMLElement {
1041
1192
  node.y = 0;
1042
1193
  }
1043
1194
 
1195
+ // Viewport culling: skip nodes that are entirely outside the visible area.
1196
+ if (this.config.largeGraph?.enabled && !this.isNodeInCullingBounds(node)) return;
1197
+
1044
1198
  ctx.lineWidth = this.config.isNodeSelected?.(node) ? 1 : 0.5;
1045
1199
  ctx.strokeStyle = this.config.foregroundColor;
1046
1200
  ctx.fillStyle = node.color;
@@ -1055,6 +1209,12 @@ class FalkorDBCanvas extends HTMLElement {
1055
1209
  ctx.arc(node.x, node.y, node.size, 0, 2 * Math.PI, false);
1056
1210
  ctx.fill();
1057
1211
 
1212
+ // Low-zoom optimisation: skip labels when they would be too small to read.
1213
+ const skipLabels = this.config.largeGraph?.enabled &&
1214
+ (this.config.largeGraph?.skipLabelsAtLowZoom ?? true) &&
1215
+ this.cullingZoom < (this.config.largeGraph?.lowZoomThreshold ?? 0.5);
1216
+ if (skipLabels) return;
1217
+
1058
1218
  // Draw text
1059
1219
  ctx.fillStyle = getContrastTextColor(node.color);
1060
1220
  ctx.textAlign = "center";
@@ -1151,6 +1311,9 @@ class FalkorDBCanvas extends HTMLElement {
1151
1311
  node.y = 0;
1152
1312
  };
1153
1313
 
1314
+ // Viewport culling: skip hit-test painting for offscreen nodes.
1315
+ if (this.config.largeGraph?.enabled && !this.isNodeInCullingBounds(node)) return;
1316
+
1154
1317
  const radius = node.size + PADDING;
1155
1318
 
1156
1319
  ctx.fillStyle = color;
@@ -1170,6 +1333,11 @@ class FalkorDBCanvas extends HTMLElement {
1170
1333
  end.y = 0;
1171
1334
  }
1172
1335
 
1336
+ // Viewport culling: skip links whose visual extent is entirely outside the
1337
+ // visible area. The check is conservative (convex-hull AABB) so it never
1338
+ // produces false negatives.
1339
+ if (this.config.largeGraph?.enabled && !this.isLinkInCullingBounds(link)) return;
1340
+
1173
1341
  let textX;
1174
1342
  let textY;
1175
1343
  let angle;
@@ -1177,6 +1345,12 @@ class FalkorDBCanvas extends HTMLElement {
1177
1345
  const isLinkSelected = this.config.isLinkSelected?.(link) ?? false;
1178
1346
  const arrowLen = isLinkSelected ? 4 : 2;
1179
1347
 
1348
+ // Low-zoom flags – evaluated once per link draw.
1349
+ const lowZoomThreshold = this.config.largeGraph?.lowZoomThreshold ?? 0.5;
1350
+ const atLowZoom = this.config.largeGraph?.enabled && this.cullingZoom < lowZoomThreshold;
1351
+ const skipArrows = atLowZoom && (this.config.largeGraph?.skipArrowsAtLowZoom ?? true);
1352
+ const skipLinkLabels = atLowZoom && (this.config.largeGraph?.skipLinkLabelsAtLowZoom ?? true);
1353
+
1180
1354
  // Deferred arrowhead — drawn after the label so it is never covered by
1181
1355
  // the label background rect (which happens for short links where the
1182
1356
  // bezier midpoint and the arrow tip are at almost the same position).
@@ -1254,7 +1428,7 @@ class FalkorDBCanvas extends HTMLElement {
1254
1428
  // Guard against zero-length tangent vector (e.g. when d ≈ 0) to avoid NaN
1255
1429
  // normals and invalid arrowhead geometry. Also skip when d is too small to
1256
1430
  // place the arrowhead at the node border (canReachBorder is false).
1257
- if (tLen !== 0 && canReachBorder) {
1431
+ if (!skipArrows && tLen !== 0 && canReachBorder) {
1258
1432
  const nx = tdx / tLen;
1259
1433
  const ny = tdy / tLen;
1260
1434
  pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
@@ -1389,7 +1563,7 @@ class FalkorDBCanvas extends HTMLElement {
1389
1563
  const aty = 2 * uArrow * (controlY - start.y) + 2 * tArrow * (end.y - controlY);
1390
1564
  const atLen = Math.sqrt(atx * atx + aty * aty);
1391
1565
 
1392
- if (atLen !== 0) {
1566
+ if (!skipArrows && atLen !== 0) {
1393
1567
  const nx = atx / atLen;
1394
1568
  const ny = aty / atLen;
1395
1569
  pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
@@ -1401,52 +1575,54 @@ class FalkorDBCanvas extends HTMLElement {
1401
1575
  // Draw text with alphabetic baseline, positioned so visual center is at y=0
1402
1576
  ctx.textBaseline = "alphabetic";
1403
1577
 
1404
- // Separate cache entries per weight so each state is measured with its own
1405
- // font, giving equal visual padding regardless of selection state.
1406
- const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
1407
- let cached = this.relationshipsTextCache.get(cacheKey);
1408
-
1409
- if (!cached) {
1410
- // ctx.font is already set to the correct weight above; measure it directly.
1411
- const metrics = ctx.measureText(link.relationship);
1412
- // Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
1413
- // line-box and adds excessive space for lighter weights.
1414
- // Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
1415
- // unreliable with textAlign="center" and can double the value on some engines.
1416
- const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
1417
- const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
1418
- const inkWidth = metrics.width;
1419
- const bgPadding = 0.3;
1420
-
1421
- cached = {
1422
- textWidth: inkWidth + bgPadding * 2,
1423
- textHeight: inkAscent + inkDescent + bgPadding * 2,
1424
- // Shift baseline up so the ink block is centred inside the bg rect.
1425
- textYOffset: (inkAscent - inkDescent) / 2,
1426
- };
1427
- this.relationshipsTextCache.set(cacheKey, cached);
1428
- }
1578
+ if (!skipLinkLabels) {
1579
+ // Separate cache entries per weight so each state is measured with its own
1580
+ // font, giving equal visual padding regardless of selection state.
1581
+ const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
1582
+ let cached = this.relationshipsTextCache.get(cacheKey);
1583
+
1584
+ if (!cached) {
1585
+ // ctx.font is already set to the correct weight above; measure it directly.
1586
+ const metrics = ctx.measureText(link.relationship);
1587
+ // Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
1588
+ // line-box and adds excessive space for lighter weights.
1589
+ // Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
1590
+ // unreliable with textAlign="center" and can double the value on some engines.
1591
+ const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
1592
+ const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
1593
+ const inkWidth = metrics.width;
1594
+ const bgPadding = 0.3;
1595
+
1596
+ cached = {
1597
+ textWidth: inkWidth + bgPadding * 2,
1598
+ textHeight: inkAscent + inkDescent + bgPadding * 2,
1599
+ // Shift baseline up so the ink block is centred inside the bg rect.
1600
+ textYOffset: (inkAscent - inkDescent) / 2,
1601
+ };
1602
+ this.relationshipsTextCache.set(cacheKey, cached);
1603
+ }
1429
1604
 
1430
- const { textWidth, textHeight, textYOffset } = cached;
1605
+ const { textWidth, textHeight, textYOffset } = cached;
1431
1606
 
1432
- ctx.save();
1433
- ctx.translate(textX, textY);
1434
- ctx.rotate(angle);
1607
+ ctx.save();
1608
+ ctx.translate(textX, textY);
1609
+ ctx.rotate(angle);
1435
1610
 
1436
- // Draw background centered on the link line (y=0)
1437
- ctx.fillStyle = this.config.backgroundColor;
1611
+ // Draw background centered on the link line (y=0)
1612
+ ctx.fillStyle = this.config.backgroundColor;
1438
1613
 
1439
- // Offset background to match text visual center
1440
- ctx.fillRect(
1441
- -textWidth / 2,
1442
- -textHeight / 2,
1443
- textWidth,
1444
- textHeight
1445
- );
1614
+ // Offset background to match text visual center
1615
+ ctx.fillRect(
1616
+ -textWidth / 2,
1617
+ -textHeight / 2,
1618
+ textWidth,
1619
+ textHeight
1620
+ );
1446
1621
 
1447
- ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
1448
- ctx.fillText(link.relationship, 0, textYOffset);
1449
- ctx.restore();
1622
+ ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
1623
+ ctx.fillText(link.relationship, 0, textYOffset);
1624
+ ctx.restore();
1625
+ }
1450
1626
 
1451
1627
  // Draw arrowhead last so it always appears on top of the label background.
1452
1628
  if (pendingArrow) {
@@ -1467,6 +1643,9 @@ class FalkorDBCanvas extends HTMLElement {
1467
1643
 
1468
1644
  if (start.x == null || start.y == null || end.x == null || end.y == null) return;
1469
1645
 
1646
+ // Viewport culling: skip hit-test painting for offscreen links.
1647
+ if (this.config.largeGraph?.enabled && !this.isLinkInCullingBounds(link)) return;
1648
+
1470
1649
  ctx.strokeStyle = color;
1471
1650
  const basePointerWidth = 10; // Desired on-screen pointer area thickness
1472
1651
  const transform = typeof ctx.getTransform === 'function' ? ctx.getTransform() : null;
@@ -1721,6 +1900,7 @@ class FalkorDBCanvas extends HTMLElement {
1721
1900
  }
1722
1901
  })
1723
1902
  .onZoom((transform: Transform) => {
1903
+ this.updateCullingBounds(transform);
1724
1904
  if (this.config.onZoom) {
1725
1905
  this.config.onZoom(transform);
1726
1906
  }
@@ -1751,7 +1931,9 @@ class FalkorDBCanvas extends HTMLElement {
1751
1931
  this.config.node!.nodePointerAreaPaint(node, color, ctx);
1752
1932
  });
1753
1933
  } else {
1754
- this.graph.nodePointerAreaPaint();
1934
+ this.graph.nodePointerAreaPaint((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => {
1935
+ this.pointerNode(node, color, ctx);
1936
+ });
1755
1937
  }
1756
1938
 
1757
1939
  if (this.config.link) {
@@ -1759,7 +1941,9 @@ class FalkorDBCanvas extends HTMLElement {
1759
1941
  this.config.link!.linkPointerAreaPaint(link, color, ctx);
1760
1942
  });
1761
1943
  } else {
1762
- this.graph.linkPointerAreaPaint();
1944
+ this.graph.linkPointerAreaPaint((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => {
1945
+ this.pointerLink(link, color, ctx);
1946
+ });
1763
1947
  }
1764
1948
  }
1765
1949
 
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export type {
60
60
 
61
61
  export type {
62
62
  ForceGraphConfig,
63
+ LargeGraphConfig,
63
64
  GraphNode,
64
65
  GraphLink,
65
66
  GraphData,