@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/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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
1605
|
+
const { textWidth, textHeight, textYOffset } = cached;
|
|
1431
1606
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1607
|
+
ctx.save();
|
|
1608
|
+
ctx.translate(textX, textY);
|
|
1609
|
+
ctx.rotate(angle);
|
|
1435
1610
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1611
|
+
// Draw background centered on the link line (y=0)
|
|
1612
|
+
ctx.fillStyle = this.config.backgroundColor;
|
|
1438
1613
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
|