@opendata-ai/openchart-vanilla 8.4.1 → 8.5.1

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.
@@ -0,0 +1,1556 @@
1
+ import {
2
+ ENTRANCE_STAGGER_MAX_NODES,
3
+ GraphSearchManager,
4
+ categoryHighlightSet,
5
+ composeStandingFocus,
6
+ createGraph,
7
+ createGraphLegend,
8
+ diffGraphUpdate,
9
+ entranceOrder,
10
+ layerHoverFocus,
11
+ popAlpha,
12
+ popScale,
13
+ registerGraphRenderer,
14
+ resolveHighlightTarget,
15
+ seedNodePositions
16
+ } from "../chunk-WOLX5EZT.js";
17
+ import {
18
+ AnimationScheduler,
19
+ createTween,
20
+ linear,
21
+ prefersReducedMotion,
22
+ resolveEase
23
+ } from "../chunk-2OJZBMXM.js";
24
+ import {
25
+ resolvedSurface
26
+ } from "../chunk-NG7CVCI2.js";
27
+
28
+ // src/graph-3d/mount.ts
29
+ import { buildEdgeTooltip } from "@opendata-ai/openchart-engine";
30
+ import ForceGraph3D from "3d-force-graph";
31
+ import { Raycaster, Vector2 } from "three";
32
+
33
+ // src/graph-3d/emphasis.ts
34
+ var EDGE_ALPHA_DEFAULT = 0.3;
35
+ var EDGE_ALPHA_CONNECTED = 1;
36
+ var SEARCH_NON_MATCH_ALPHA = 0.25;
37
+ function resolveEmphasis(input) {
38
+ const { focus, exemptIds, dimOpacity, edgeBaseAlpha } = input;
39
+ const search = focus.searchMatches;
40
+ const nodes = /* @__PURE__ */ new Map();
41
+ for (const n of input.nodes) {
42
+ let alpha = 1;
43
+ if (focus.hasActive && !exemptIds.has(n.id) && !focus.connected.has(n.id)) {
44
+ alpha = dimOpacity;
45
+ }
46
+ if (search !== null && !search.has(n.id)) alpha *= SEARCH_NON_MATCH_ALPHA;
47
+ nodes.set(n.id, alpha * n.opacity);
48
+ }
49
+ const edges = /* @__PURE__ */ new Map();
50
+ for (let i = 0; i < input.edges.length; i++) {
51
+ const e = input.edges[i];
52
+ const resting = edgeBaseAlpha?.get(i) ?? EDGE_ALPHA_DEFAULT;
53
+ let alpha;
54
+ if (!focus.hasActive) {
55
+ alpha = resting;
56
+ } else if (focus.connected.has(e.source) && focus.connected.has(e.target)) {
57
+ alpha = EDGE_ALPHA_CONNECTED;
58
+ } else {
59
+ alpha = dimOpacity / 3;
60
+ }
61
+ if (search !== null && !search.has(e.source) && !search.has(e.target)) {
62
+ alpha *= SEARCH_NON_MATCH_ALPHA;
63
+ }
64
+ edges.set(i, alpha);
65
+ }
66
+ return { nodes, edges };
67
+ }
68
+
69
+ // src/graph-3d/entrance.ts
70
+ var ENTRANCE_STAGGER_FRACTION = 0.5;
71
+ var LINK_ENTRANCE_DELAY = 0.25;
72
+ var ENTRANCE_CAMERA_PULLBACK = 1.6;
73
+ function planEntrance(nodes, edges, duration, stagger) {
74
+ const nodeOffset = /* @__PURE__ */ new Map();
75
+ const linkOffset = /* @__PURE__ */ new Map();
76
+ if (!stagger || nodes.length === 0) {
77
+ for (const n of nodes) nodeOffset.set(n.id, 0);
78
+ for (let i = 0; i < edges.length; i++) linkOffset.set(i, duration * LINK_ENTRANCE_DELAY);
79
+ return { nodeOffset, linkOffset, span: duration * (1 + LINK_ENTRANCE_DELAY) };
80
+ }
81
+ const rank = entranceOrder(nodes);
82
+ const last = Math.max(1, nodes.length - 1);
83
+ const spread = duration * ENTRANCE_STAGGER_FRACTION;
84
+ let maxOffset = 0;
85
+ for (const n of nodes) {
86
+ const offset = (rank.get(n.id) ?? 0) / last * spread;
87
+ nodeOffset.set(n.id, offset);
88
+ if (offset > maxOffset) maxOffset = offset;
89
+ }
90
+ const delay = duration * LINK_ENTRANCE_DELAY;
91
+ for (let i = 0; i < edges.length; i++) {
92
+ const a = nodeOffset.get(edges[i].source) ?? 0;
93
+ const b = nodeOffset.get(edges[i].target) ?? 0;
94
+ const offset = Math.max(a, b) + delay;
95
+ linkOffset.set(i, offset);
96
+ if (offset > maxOffset) maxOffset = offset;
97
+ }
98
+ return { nodeOffset, linkOffset, span: duration + maxOffset };
99
+ }
100
+ function elementProgress(elapsed, offset, duration, ease) {
101
+ if (duration <= 0) return elapsed >= offset ? 1 : 0;
102
+ const local = (elapsed - offset) / duration;
103
+ if (local <= 0) return 0;
104
+ if (local >= 1) return 1;
105
+ return ease(local);
106
+ }
107
+
108
+ // src/graph-3d/fit.ts
109
+ var MIN_FIT_DISTANCE = 60;
110
+ function normalize(v) {
111
+ const len = Math.hypot(v.x, v.y, v.z);
112
+ if (!(len > 1e-6)) return { x: 0, y: 0, z: 1 };
113
+ return { x: v.x / len, y: v.y / len, z: v.z / len };
114
+ }
115
+ function cross(a, b) {
116
+ return {
117
+ x: a.y * b.z - a.z * b.y,
118
+ y: a.z * b.x - a.x * b.z,
119
+ z: a.x * b.y - a.y * b.x
120
+ };
121
+ }
122
+ function computeFit(points, view) {
123
+ let minX = Number.POSITIVE_INFINITY;
124
+ let minY = Number.POSITIVE_INFINITY;
125
+ let minZ = Number.POSITIVE_INFINITY;
126
+ let maxX = Number.NEGATIVE_INFINITY;
127
+ let maxY = Number.NEGATIVE_INFINITY;
128
+ let maxZ = Number.NEGATIVE_INFINITY;
129
+ for (const p of points) {
130
+ if (p.x < minX) minX = p.x;
131
+ if (p.y < minY) minY = p.y;
132
+ if (p.z < minZ) minZ = p.z;
133
+ if (p.x > maxX) maxX = p.x;
134
+ if (p.y > maxY) maxY = p.y;
135
+ if (p.z > maxZ) maxZ = p.z;
136
+ }
137
+ if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(minZ) || !Number.isFinite(maxX) || !Number.isFinite(maxY) || !Number.isFinite(maxZ)) {
138
+ return null;
139
+ }
140
+ const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2, z: (minZ + maxZ) / 2 };
141
+ const dir = normalize({
142
+ x: view.cameraPos.x - center.x,
143
+ y: view.cameraPos.y - center.y,
144
+ z: view.cameraPos.z - center.z
145
+ });
146
+ const worldUp = Math.abs(dir.y) > 0.999 ? { x: 0, y: 0, z: 1 } : { x: 0, y: 1, z: 0 };
147
+ const right = normalize(cross(worldUp, dir));
148
+ const up = cross(dir, right);
149
+ let halfRight = 0;
150
+ let halfUp = 0;
151
+ let nearDepth = 0;
152
+ for (const p of points) {
153
+ const dx = p.x - center.x;
154
+ const dy = p.y - center.y;
155
+ const dz = p.z - center.z;
156
+ halfRight = Math.max(
157
+ halfRight,
158
+ Math.abs(dx * right.x + dy * right.y + dz * right.z) + p.radius
159
+ );
160
+ halfUp = Math.max(halfUp, Math.abs(dx * up.x + dy * up.y + dz * up.z) + p.radius);
161
+ nearDepth = Math.max(nearDepth, dx * dir.x + dy * dir.y + dz * dir.z + p.radius);
162
+ }
163
+ const vFov = view.fovDeg * Math.PI / 180;
164
+ const hFov = 2 * Math.atan(Math.tan(vFov / 2) * view.aspect);
165
+ const slack = 1 + Math.min(0.5, 2 * view.paddingPx / (view.viewportHeight || 1));
166
+ const distance = Math.max(
167
+ halfUp * slack / Math.tan(vFov / 2),
168
+ halfRight * slack / Math.tan(hFov / 2),
169
+ MIN_FIT_DISTANCE
170
+ ) + Math.max(0, nearDepth);
171
+ return { center, dir, distance };
172
+ }
173
+
174
+ // src/graph-3d/forces.ts
175
+ import { forceCenter, forceCollide } from "d3-force-3d";
176
+ var DEFAULT_ALPHA_MIN = 1e-3;
177
+ var MAX_COOLDOWN_TICKS = 800;
178
+ var COLLIDE_ITERATIONS = 1;
179
+ var WARMUP_MS_PER_NODE_TICK = 35e-4;
180
+ var MIN_WARMUP_TICKS = 5;
181
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
182
+ function budgetedWarmupTicks(requested, nodeCount, budgetMs = DEFAULT_WARMUP_BUDGET_MS) {
183
+ if (requested <= 0 || nodeCount <= 0) return Math.max(0, requested);
184
+ const perTickMs = Math.max(WARMUP_MS_PER_NODE_TICK * nodeCount, 0.05);
185
+ const affordable = Math.floor(budgetMs / perTickMs);
186
+ return Math.max(MIN_WARMUP_TICKS, Math.min(requested, affordable));
187
+ }
188
+ function ticksToSettle(alphaDecay, alphaMin = DEFAULT_ALPHA_MIN) {
189
+ if (!(alphaDecay > 0) || alphaDecay >= 1) return MAX_COOLDOWN_TICKS;
190
+ const n = Math.ceil(Math.log(alphaMin) / Math.log(1 - alphaDecay));
191
+ return Math.min(MAX_COOLDOWN_TICKS, Math.max(1, n));
192
+ }
193
+ function forceCluster3D(strength) {
194
+ let nodes = [];
195
+ const force = (alpha) => {
196
+ const cx = /* @__PURE__ */ new Map();
197
+ const cy = /* @__PURE__ */ new Map();
198
+ const cz = /* @__PURE__ */ new Map();
199
+ const count = /* @__PURE__ */ new Map();
200
+ for (const node of nodes) {
201
+ const c = node.node?.community;
202
+ if (!c) continue;
203
+ cx.set(c, (cx.get(c) ?? 0) + (node.x ?? 0));
204
+ cy.set(c, (cy.get(c) ?? 0) + (node.y ?? 0));
205
+ cz.set(c, (cz.get(c) ?? 0) + (node.z ?? 0));
206
+ count.set(c, (count.get(c) ?? 0) + 1);
207
+ }
208
+ for (const [c, n] of count) {
209
+ cx.set(c, cx.get(c) / n);
210
+ cy.set(c, cy.get(c) / n);
211
+ cz.set(c, cz.get(c) / n);
212
+ }
213
+ const k = strength * alpha;
214
+ for (const node of nodes) {
215
+ const c = node.node?.community;
216
+ if (!c) continue;
217
+ node.vx = (node.vx ?? 0) + (cx.get(c) - (node.x ?? 0)) * k;
218
+ node.vy = (node.vy ?? 0) + (cy.get(c) - (node.y ?? 0)) * k;
219
+ node.vz = (node.vz ?? 0) + (cz.get(c) - (node.z ?? 0)) * k;
220
+ }
221
+ };
222
+ force.initialize = (ns) => {
223
+ nodes = ns;
224
+ };
225
+ return force;
226
+ }
227
+ function applySimulationConfig(graph, config, nodeCount) {
228
+ const charge = graph.d3Force("charge");
229
+ if (charge && typeof charge.strength === "function") {
230
+ charge.strength(config.chargeStrength);
231
+ }
232
+ const link = graph.d3Force("link");
233
+ if (link) {
234
+ const l = link;
235
+ l.distance?.(config.linkDistance);
236
+ if (config.linkStrength !== void 0) l.strength?.(config.linkStrength);
237
+ }
238
+ const padding = config.collisionPadding ?? 2;
239
+ graph.d3Force(
240
+ "collide",
241
+ forceCollide((n) => (n.node?.radius ?? 1) + padding).iterations(COLLIDE_ITERATIONS)
242
+ );
243
+ graph.d3Force("cluster", config.clustering ? forceCluster3D(config.clustering.strength) : null);
244
+ graph.d3Force(
245
+ "center",
246
+ config.centerForce === false ? null : graph.d3Force("center") ?? forceCenter()
247
+ );
248
+ graph.d3VelocityDecay(config.velocityDecay);
249
+ graph.d3AlphaDecay(config.alphaDecay);
250
+ graph.warmupTicks(budgetedWarmupTicks(config.warmupTicks ?? 0, nodeCount, config.warmupBudgetMs));
251
+ graph.cooldownTicks(ticksToSettle(config.alphaDecay));
252
+ }
253
+
254
+ // src/graph-3d/labels.ts
255
+ var LABEL_BUDGET_3D = 40;
256
+ function resolveVisibleLabels(nodes, forced, cameraPos, budget) {
257
+ const visible = [];
258
+ for (const n of nodes) if (forced.has(n.id)) visible.push(n.id);
259
+ if (budget <= 0) return visible;
260
+ const ranked = [];
261
+ for (const n of nodes) {
262
+ if (forced.has(n.id)) continue;
263
+ const dx = n.x - cameraPos.x;
264
+ const dy = n.y - cameraPos.y;
265
+ const dz = n.z - cameraPos.z;
266
+ ranked.push({ id: n.id, priority: n.priority, dist: dx * dx + dy * dy + dz * dz });
267
+ }
268
+ ranked.sort((a, b) => b.priority - a.priority || a.dist - b.dist || (a.id < b.id ? -1 : 1));
269
+ for (let i = 0; i < ranked.length && i < budget; i++) visible.push(ranked[i].id);
270
+ return visible;
271
+ }
272
+ var LABEL_BOX_PAD = 2;
273
+ function labelBox(projected, text, heightPx) {
274
+ if (!projected || !Number.isFinite(projected.x) || !Number.isFinite(projected.y)) return null;
275
+ const width = Math.max(1, text.length) * heightPx * 0.55;
276
+ const cy = projected.y - heightPx;
277
+ return {
278
+ x0: projected.x - width / 2 - LABEL_BOX_PAD,
279
+ x1: projected.x + width / 2 + LABEL_BOX_PAD,
280
+ y0: cy - heightPx / 2 - LABEL_BOX_PAD,
281
+ y1: cy + heightPx / 2 + LABEL_BOX_PAD
282
+ };
283
+ }
284
+ function overlaps(a, b) {
285
+ return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
286
+ }
287
+
288
+ // src/graph-3d/links.ts
289
+ import {
290
+ BufferAttribute,
291
+ BufferGeometry,
292
+ CylinderGeometry,
293
+ Line,
294
+ LineBasicMaterial,
295
+ LineDashedMaterial,
296
+ Matrix4,
297
+ Mesh,
298
+ MeshLambertMaterial
299
+ } from "three";
300
+ var LINK_WIDTH_MAX_EDGES = 2e3;
301
+ var CYLINDER_SEGMENTS = 6;
302
+ var DASH = { dashed: { dash: 6, gap: 4 }, dotted: { dash: 1.5, gap: 3 } };
303
+ function isDashed(edge) {
304
+ return edge.style === "dashed" || edge.style === "dotted";
305
+ }
306
+ function cylinderRadius(edge) {
307
+ return Math.max(edge.strokeWidth, 0.1) / 2;
308
+ }
309
+ function buildCylinder(edge) {
310
+ const r = cylinderRadius(edge);
311
+ const geometry = new CylinderGeometry(r, r, 1, CYLINDER_SEGMENTS, 1, false);
312
+ geometry.applyMatrix4(new Matrix4().makeTranslation(0, 0.5, 0));
313
+ geometry.applyMatrix4(new Matrix4().makeRotationX(Math.PI / 2));
314
+ return geometry;
315
+ }
316
+ function buildLineMaterial(edge, opacity) {
317
+ if (!isDashed(edge)) {
318
+ return new LineBasicMaterial({
319
+ color: edge.stroke,
320
+ transparent: true,
321
+ opacity,
322
+ depthWrite: false
323
+ });
324
+ }
325
+ const dash = DASH[edge.style];
326
+ return new LineDashedMaterial({
327
+ color: edge.stroke,
328
+ transparent: true,
329
+ opacity,
330
+ depthWrite: false,
331
+ dashSize: dash.dash,
332
+ gapSize: dash.gap
333
+ });
334
+ }
335
+ function createLinkObject(edge, useWidth, restingAlpha) {
336
+ if (useWidth) {
337
+ const geometry2 = buildCylinder(edge);
338
+ const material2 = new MeshLambertMaterial({
339
+ color: edge.stroke,
340
+ transparent: true,
341
+ opacity: restingAlpha,
342
+ depthWrite: false
343
+ });
344
+ return { object: new Mesh(geometry2, material2), geometry: geometry2, material: material2, dashed: false };
345
+ }
346
+ const geometry = new BufferGeometry();
347
+ geometry.setAttribute("position", new BufferAttribute(new Float32Array(2 * 3), 3));
348
+ const material = buildLineMaterial(edge, restingAlpha);
349
+ return { object: new Line(geometry, material), geometry, material, dashed: isDashed(edge) };
350
+ }
351
+ function applyLinkVisuals(obj, edge, useWidth) {
352
+ obj.material.color.set(edge.stroke);
353
+ if (useWidth) {
354
+ const r = cylinderRadius(edge);
355
+ if (obj.geometry.parameters.radiusTop === r) return;
356
+ const next2 = buildCylinder(edge);
357
+ obj.object.geometry = next2;
358
+ obj.geometry.dispose();
359
+ obj.geometry = next2;
360
+ return;
361
+ }
362
+ const dashed = isDashed(edge);
363
+ const current = obj.material;
364
+ const dash = dashed ? DASH[edge.style] : null;
365
+ const sameDash = dashed === obj.dashed && (dash === null || current.dashSize === dash.dash && current.gapSize === dash.gap);
366
+ if (sameDash) return;
367
+ const next = buildLineMaterial(edge, obj.material.opacity);
368
+ obj.object.material = next;
369
+ obj.material.dispose();
370
+ obj.material = next;
371
+ obj.dashed = dashed;
372
+ if (dashed) obj.object.computeLineDistances();
373
+ }
374
+ function linkShapeMatches(obj, useWidth) {
375
+ return useWidth ? obj.object instanceof Mesh : obj.object instanceof Line;
376
+ }
377
+ function updateLinkPosition(obj, start, end) {
378
+ if (!(obj.object instanceof Line)) return false;
379
+ const attr = obj.geometry.getAttribute("position");
380
+ const a = attr.array;
381
+ a[0] = start.x;
382
+ a[1] = start.y || 0;
383
+ a[2] = start.z || 0;
384
+ a[3] = end.x;
385
+ a[4] = end.y || 0;
386
+ a[5] = end.z || 0;
387
+ attr.needsUpdate = true;
388
+ obj.geometry.computeBoundingSphere();
389
+ if (obj.dashed) obj.object.computeLineDistances();
390
+ return true;
391
+ }
392
+ function disposeLinkObject(obj) {
393
+ obj.geometry.dispose();
394
+ obj.material.dispose();
395
+ }
396
+ function widthAsAlpha(edges, min = 0.15) {
397
+ const out = /* @__PURE__ */ new Map();
398
+ if (edges.length === 0) return out;
399
+ let lo = Number.POSITIVE_INFINITY;
400
+ let hi = Number.NEGATIVE_INFINITY;
401
+ for (const e of edges) {
402
+ if (e.strokeWidth < lo) lo = e.strokeWidth;
403
+ if (e.strokeWidth > hi) hi = e.strokeWidth;
404
+ }
405
+ const span = hi - lo;
406
+ for (let i = 0; i < edges.length; i++) {
407
+ const t = span > 0 ? (edges[i].strokeWidth - lo) / span : 1;
408
+ const step = Math.round(t * 3) / 3;
409
+ out.set(i, min + (1 - min) * step);
410
+ }
411
+ return out;
412
+ }
413
+
414
+ // src/graph-3d/nodes.ts
415
+ import { Group, Mesh as Mesh2, MeshLambertMaterial as MeshLambertMaterial2, SphereGeometry } from "three";
416
+ import SpriteText from "three-spritetext";
417
+ var SPHERE_SEGMENTS = 12;
418
+ var LABEL_TEXT_HEIGHT = 4;
419
+ var LABEL_GAP = 2;
420
+ function createNodeObject(node, labelColor) {
421
+ const geometry = new SphereGeometry(node.radius, SPHERE_SEGMENTS, SPHERE_SEGMENTS);
422
+ const material = new MeshLambertMaterial2({
423
+ color: node.fill,
424
+ transparent: true,
425
+ opacity: node.opacity
426
+ });
427
+ const mesh = new Mesh2(geometry, material);
428
+ const group = new Group();
429
+ group.add(mesh);
430
+ return {
431
+ group,
432
+ mesh,
433
+ geometry,
434
+ material,
435
+ sprite: null,
436
+ labelBaseScale: null,
437
+ labelText: node.label ?? null,
438
+ labelColor,
439
+ labelOffset: node.radius + LABEL_GAP
440
+ };
441
+ }
442
+ function ensureLabelSprite(obj) {
443
+ if (obj.sprite) return obj.sprite;
444
+ if (!obj.labelText) return null;
445
+ const sprite = new SpriteText(obj.labelText, LABEL_TEXT_HEIGHT, obj.labelColor);
446
+ sprite.position.y = obj.labelOffset;
447
+ sprite.raycast = () => {
448
+ };
449
+ sprite.visible = false;
450
+ obj.group.add(sprite);
451
+ obj.sprite = sprite;
452
+ obj.labelBaseScale = { x: sprite.scale.x, y: sprite.scale.y };
453
+ return sprite;
454
+ }
455
+ function applyNodeVisuals(obj, node, labelColor) {
456
+ obj.material.color.set(node.fill);
457
+ if (obj.geometry.parameters.radius !== node.radius) {
458
+ const next = new SphereGeometry(node.radius, SPHERE_SEGMENTS, SPHERE_SEGMENTS);
459
+ obj.mesh.geometry = next;
460
+ obj.geometry.dispose();
461
+ obj.geometry = next;
462
+ }
463
+ obj.labelText = node.label ?? null;
464
+ obj.labelColor = labelColor;
465
+ obj.labelOffset = node.radius + LABEL_GAP;
466
+ if (obj.sprite) {
467
+ const regenerates = node.label && obj.sprite.text !== node.label || obj.sprite.color !== labelColor;
468
+ if (node.label && obj.sprite.text !== node.label) obj.sprite.text = node.label;
469
+ obj.sprite.color = labelColor;
470
+ obj.sprite.position.y = obj.labelOffset;
471
+ if (regenerates) obj.labelBaseScale = { x: obj.sprite.scale.x, y: obj.sprite.scale.y };
472
+ }
473
+ }
474
+ function disposeNodeObject(obj) {
475
+ obj.geometry.dispose();
476
+ obj.material.dispose();
477
+ if (obj.sprite) {
478
+ const mat = obj.sprite.material;
479
+ mat.map?.dispose();
480
+ mat.dispose();
481
+ }
482
+ }
483
+
484
+ // src/graph-3d/mount.ts
485
+ var FIT_DISTANCE = 1e3;
486
+ var FIT_PADDING_PX = 40;
487
+ var LABEL_SCREEN_PX = 11;
488
+ var LABEL_SCALE_MIN = 0.4;
489
+ var LABEL_SCALE_MAX = 8;
490
+ var MAX_PIXEL_RATIO = 2;
491
+ var DEFAULT_FOV_DEG = 50;
492
+ var AUTO_FIT_INTERVAL_MS = 250;
493
+ var NODE_FOCUS_DISTANCE = 120;
494
+ var AUTO_FLIGHT_MS = 800;
495
+ var ENTRANCE_MIN_SCALE = 0.01;
496
+ var LABEL_ENTRANCE_MIN = 0.4;
497
+ var LABEL_RERANK_MS = 100;
498
+ var K_MIN = 0.05;
499
+ var K_MAX = 40;
500
+ var Z_SEED_SALT = 2654435769;
501
+ function createGraph3DRenderer(ctx) {
502
+ const { shell, options } = ctx;
503
+ let currentSpec = ctx.spec;
504
+ let compilation = ctx.compilation;
505
+ let destroyed = false;
506
+ const surfaceEl = document.createElement("div");
507
+ surfaceEl.className = "oc-graph-3d";
508
+ surfaceEl.setAttribute("role", "img");
509
+ if (compilation.a11y?.altText) surfaceEl.setAttribute("aria-label", compilation.a11y.altText);
510
+ surfaceEl.style.position = "absolute";
511
+ surfaceEl.style.inset = "0";
512
+ shell.mountSurface(surfaceEl);
513
+ const graph = new ForceGraph3D(surfaceEl, {
514
+ controlType: "trackball",
515
+ // `alpha` is what lets a transparent theme show the host page through the
516
+ // canvas; both are constructor-only in 3d-force-graph.
517
+ rendererConfig: { alpha: true, antialias: true }
518
+ });
519
+ const nodeObjects = /* @__PURE__ */ new Map();
520
+ const linkObjects = /* @__PURE__ */ new Map();
521
+ let nodeData = [];
522
+ let nodeById = /* @__PURE__ */ new Map();
523
+ let linkData = [];
524
+ let adjacency = /* @__PURE__ */ new Map();
525
+ let nodeDataMap = /* @__PURE__ */ new Map();
526
+ let nodeCategory = /* @__PURE__ */ new Map();
527
+ let seedIds = /* @__PURE__ */ new Set();
528
+ let edgeBaseAlpha;
529
+ let useLinkWidth = false;
530
+ const searchManager = new GraphSearchManager();
531
+ let legendController = null;
532
+ let hoveredNodeId = null;
533
+ let hoveredLinkIndex = null;
534
+ let selectedNodeIds = /* @__PURE__ */ new Set();
535
+ let activeCategories = /* @__PURE__ */ new Set();
536
+ let transientHighlight = null;
537
+ let transientTarget = null;
538
+ let highlightSet = null;
539
+ let highlightDimOpacity = null;
540
+ const displayNodeAlpha = /* @__PURE__ */ new Map();
541
+ const displayEdgeAlpha = /* @__PURE__ */ new Map();
542
+ let entranceActive = false;
543
+ const entranceNodeAlpha = /* @__PURE__ */ new Map();
544
+ const entranceNodeScale = /* @__PURE__ */ new Map();
545
+ const entranceEdgeAlpha = /* @__PURE__ */ new Map();
546
+ let entranceCameraPull = 1;
547
+ let emphasisTween = null;
548
+ let pumpId = null;
549
+ const scheduler = new AnimationScheduler(() => pump());
550
+ function pump() {
551
+ if (pumpId !== null || destroyed) return;
552
+ pumpId = requestAnimationFrame((now) => {
553
+ pumpId = null;
554
+ if (destroyed) return;
555
+ scheduler.tick(now);
556
+ if (scheduler.active) pump();
557
+ });
558
+ }
559
+ let autoFit = true;
560
+ let lastAutoFit = 0;
561
+ let cameraChangeQueued = false;
562
+ const labelShown = /* @__PURE__ */ new Set();
563
+ let lastLabelRank = 0;
564
+ let labelRankQueued = false;
565
+ let controlsListener = null;
566
+ let openTooltip = null;
567
+ let renderedTooltip = null;
568
+ function rebuildDerived() {
569
+ adjacency = /* @__PURE__ */ new Map();
570
+ for (const e of compilation.edges) {
571
+ if (!adjacency.has(e.source)) adjacency.set(e.source, /* @__PURE__ */ new Set());
572
+ if (!adjacency.has(e.target)) adjacency.set(e.target, /* @__PURE__ */ new Set());
573
+ adjacency.get(e.source).add(e.target);
574
+ adjacency.get(e.target).add(e.source);
575
+ }
576
+ nodeDataMap = new Map(compilation.nodes.map((n) => [n.id, n.data ?? {}]));
577
+ nodeCategory = /* @__PURE__ */ new Map();
578
+ const field = compilation.legendField;
579
+ if (field) {
580
+ for (const n of compilation.nodes) {
581
+ const v = n.data?.[field];
582
+ if (v != null) nodeCategory.set(n.id, String(v));
583
+ }
584
+ }
585
+ seedIds = new Set(compilation.seedNodeIds);
586
+ const widthEncoded = currentSpec.encoding?.edgeWidth != null;
587
+ useLinkWidth = widthEncoded && compilation.edges.length <= LINK_WIDTH_MAX_EDGES;
588
+ edgeBaseAlpha = widthEncoded && !useLinkWidth ? widthAsAlpha(compilation.edges) : void 0;
589
+ }
590
+ function labelColor() {
591
+ return compilation.theme.colors.text;
592
+ }
593
+ function seedPositions(nodes) {
594
+ const xy = nodes.map((n) => ({
595
+ id: n.id,
596
+ radius: n.radius,
597
+ community: n.community
598
+ }));
599
+ const seed = compilation.simulationConfig.seed ?? 0;
600
+ seedNodePositions(xy, seed);
601
+ const z = nodes.map((n) => ({
602
+ id: n.id,
603
+ radius: n.radius,
604
+ community: n.community
605
+ }));
606
+ seedNodePositions(z, seed ^ Z_SEED_SALT);
607
+ const out = /* @__PURE__ */ new Map();
608
+ for (let i = 0; i < nodes.length; i++) {
609
+ out.set(nodes[i].id, [xy[i].x ?? 0, xy[i].y ?? 0, z[i].x ?? 0]);
610
+ }
611
+ return out;
612
+ }
613
+ function seedZ(id, radius, community) {
614
+ const one = [{ id, radius, community }];
615
+ seedNodePositions(one, (compilation.simulationConfig.seed ?? 0) ^ Z_SEED_SALT);
616
+ return one[0].x ?? 0;
617
+ }
618
+ function buildGraphData(positions) {
619
+ nodeData = compilation.nodes.map((node) => {
620
+ const p = positions.get(node.id) ?? [0, 0, 0];
621
+ return { id: node.id, node, x: p[0], y: p[1], z: p[2] };
622
+ });
623
+ linkData = compilation.edges.map((edge, edgeIndex) => ({
624
+ source: edge.source,
625
+ target: edge.target,
626
+ edge,
627
+ edgeIndex
628
+ }));
629
+ nodeById = new Map(nodeData.map((d) => [d.id, d]));
630
+ graph.graphData({ nodes: nodeData, links: linkData });
631
+ }
632
+ function nodeObjectFor(datum) {
633
+ let obj = nodeObjects.get(datum.id);
634
+ if (!obj) {
635
+ obj = createNodeObject(datum.node, labelColor());
636
+ obj.group.userData.nodeId = datum.id;
637
+ nodeObjects.set(datum.id, obj);
638
+ const base = displayNodeAlpha.get(datum.id) ?? datum.node.opacity;
639
+ if (entranceActive) {
640
+ obj.material.opacity = base * (entranceNodeAlpha.get(datum.id) ?? 0);
641
+ obj.mesh.scale.setScalar(entranceNodeScale.get(datum.id) ?? ENTRANCE_MIN_SCALE);
642
+ } else {
643
+ obj.material.opacity = base;
644
+ }
645
+ }
646
+ return obj;
647
+ }
648
+ const linkThreeObject = (datum) => linkObjectFor(datum).object;
649
+ function linkObjectFor(datum) {
650
+ let obj = linkObjects.get(datum.edgeIndex);
651
+ if (!obj) {
652
+ const resting = edgeBaseAlpha?.get(datum.edgeIndex) ?? 0.3;
653
+ obj = createLinkObject(datum.edge, useLinkWidth, resting);
654
+ linkObjects.set(datum.edgeIndex, obj);
655
+ const base = displayEdgeAlpha.get(datum.edgeIndex) ?? resting;
656
+ obj.material.opacity = entranceActive ? base * (entranceEdgeAlpha.get(datum.edgeIndex) ?? 0) : base;
657
+ }
658
+ return obj;
659
+ }
660
+ function hoverConnectedSet(nodeId) {
661
+ if (nodeId === null) return null;
662
+ if (compilation.interaction.hoverMode === "none") return null;
663
+ if (compilation.interaction.hoverMode === "category") {
664
+ const cat = nodeCategory.get(nodeId);
665
+ const set2 = /* @__PURE__ */ new Set([nodeId]);
666
+ if (cat !== void 0) {
667
+ for (const [id, c] of nodeCategory) if (c === cat) set2.add(id);
668
+ }
669
+ return set2;
670
+ }
671
+ if (compilation.interaction.hoverMode === "node") return /* @__PURE__ */ new Set([nodeId]);
672
+ const set = /* @__PURE__ */ new Set([nodeId]);
673
+ const neighbors = adjacency.get(nodeId);
674
+ if (neighbors) for (const nid of neighbors) set.add(nid);
675
+ return set;
676
+ }
677
+ function currentFocus() {
678
+ const standing = composeStandingFocus(
679
+ highlightSet,
680
+ searchManager.getMatches(),
681
+ selectedNodeIds,
682
+ adjacency
683
+ );
684
+ return layerHoverFocus(standing, hoveredNodeId, hoverConnectedSet(hoveredNodeId));
685
+ }
686
+ function effectiveDimOpacity() {
687
+ const custom = transientHighlight !== null ? highlightDimOpacity : null;
688
+ return custom ?? compilation.interaction.dimOpacity;
689
+ }
690
+ function paint() {
691
+ for (const [id, obj] of nodeObjects) {
692
+ const a = displayNodeAlpha.get(id);
693
+ if (a === void 0) continue;
694
+ if (entranceActive) {
695
+ const factor = entranceNodeAlpha.get(id) ?? 0;
696
+ obj.material.opacity = a * factor;
697
+ obj.mesh.scale.setScalar(entranceNodeScale.get(id) ?? ENTRANCE_MIN_SCALE);
698
+ if (obj.sprite) obj.sprite.visible = labelShown.has(id) && factor > LABEL_ENTRANCE_MIN;
699
+ } else {
700
+ obj.material.opacity = a;
701
+ }
702
+ }
703
+ for (const [index, obj] of linkObjects) {
704
+ const a = displayEdgeAlpha.get(index);
705
+ if (a === void 0) continue;
706
+ obj.material.opacity = entranceActive ? a * (entranceEdgeAlpha.get(index) ?? 0) : a;
707
+ }
708
+ }
709
+ function armEmphasis() {
710
+ const targets = resolveEmphasis({
711
+ nodes: compilation.nodes,
712
+ edges: compilation.edges,
713
+ focus: currentFocus(),
714
+ exemptIds: seedIds,
715
+ dimOpacity: effectiveDimOpacity(),
716
+ edgeBaseAlpha
717
+ });
718
+ if (emphasisTween) {
719
+ scheduler.remove(emphasisTween);
720
+ emphasisTween = null;
721
+ }
722
+ const hoverCfg = compilation.animation?.hover ?? null;
723
+ const duration = hoverCfg && !prefersReducedMotion() ? hoverCfg.duration : 0;
724
+ if (duration <= 0) {
725
+ for (const [id, a] of targets.nodes) displayNodeAlpha.set(id, a);
726
+ for (const [i, a] of targets.edges) displayEdgeAlpha.set(i, a);
727
+ paint();
728
+ refreshLabels();
729
+ return;
730
+ }
731
+ const fromNodes = new Map(displayNodeAlpha);
732
+ const fromEdges = new Map(displayEdgeAlpha);
733
+ const tween = createTween({
734
+ duration,
735
+ ease: resolveEase(hoverCfg?.ease ?? "smooth"),
736
+ apply: (t) => {
737
+ for (const [id, to] of targets.nodes) {
738
+ const from = fromNodes.get(id) ?? to;
739
+ displayNodeAlpha.set(id, from + (to - from) * t);
740
+ }
741
+ for (const [i, to] of targets.edges) {
742
+ const from = fromEdges.get(i) ?? to;
743
+ displayEdgeAlpha.set(i, from + (to - from) * t);
744
+ }
745
+ paint();
746
+ },
747
+ onDone: () => {
748
+ emphasisTween = null;
749
+ }
750
+ });
751
+ emphasisTween = tween;
752
+ scheduler.add(tween);
753
+ refreshLabels();
754
+ }
755
+ function resolveHighlightTarget2(target) {
756
+ return resolveHighlightTarget(target, compilation.nodes, adjacency);
757
+ }
758
+ function categoryHighlightSet2() {
759
+ return categoryHighlightSet(activeCategories, nodeCategory);
760
+ }
761
+ function recomputeHighlight() {
762
+ const filter = categoryHighlightSet2();
763
+ const transient = transientHighlight !== null && transientHighlight.size > 0 ? transientHighlight : null;
764
+ if (filter !== null && transient !== null) {
765
+ const inter = /* @__PURE__ */ new Set();
766
+ for (const id of transient) if (filter.has(id)) inter.add(id);
767
+ highlightSet = inter.size > 0 ? inter : transient;
768
+ return;
769
+ }
770
+ highlightSet = filter ?? transient;
771
+ }
772
+ function refreshTransientHighlight() {
773
+ if (transientTarget === null) {
774
+ transientHighlight = null;
775
+ return;
776
+ }
777
+ const nextIds = new Set(compilation.nodes.map((n) => n.id));
778
+ const resolved = new Set(
779
+ [...resolveHighlightTarget2(transientTarget)].filter((id) => nextIds.has(id))
780
+ );
781
+ transientHighlight = resolved.size > 0 ? resolved : null;
782
+ }
783
+ function emitHighlightChange() {
784
+ options?.onHighlightChange?.(highlightSet ? [...highlightSet] : null);
785
+ }
786
+ function forcedLabelIds() {
787
+ const forced = /* @__PURE__ */ new Set();
788
+ for (const id of seedIds) forced.add(id);
789
+ for (const [id, override] of Object.entries(currentSpec.nodeOverrides ?? {})) {
790
+ if (override?.alwaysShowLabel) forced.add(id);
791
+ }
792
+ if (hoveredNodeId) forced.add(hoveredNodeId);
793
+ for (const id of selectedNodeIds) forced.add(id);
794
+ const matches = searchManager.getMatches();
795
+ if (matches) for (const id of matches) forced.add(id);
796
+ return forced;
797
+ }
798
+ function refreshLabels() {
799
+ if (destroyed || nodeObjects.size === 0) return;
800
+ const camera = graph.camera();
801
+ const cameraPos = camera?.position ?? { x: 0, y: 0, z: FIT_DISTANCE };
802
+ const forced = forcedLabelIds();
803
+ const ranked = resolveVisibleLabels(
804
+ // Only nodes that actually compiled a label are candidates. Filtering on
805
+ // the datum rather than `nodeObjects` matters: the library's digest is
806
+ // deferred, so on the first rank the objects may not exist yet and an
807
+ // object-side filter would let unlabelled nodes eat budget slots.
808
+ nodeData.filter((d) => d.node.label).map((d) => ({
809
+ id: d.id,
810
+ priority: d.node.labelPriority,
811
+ x: d.x ?? 0,
812
+ y: d.y ?? 0,
813
+ z: d.z ?? 0
814
+ })),
815
+ forced,
816
+ cameraPos,
817
+ LABEL_BUDGET_3D
818
+ );
819
+ const fov = (camera?.fov ?? DEFAULT_FOV_DEG) * (Math.PI / 180);
820
+ const viewportHeight = shell.getSize().height || 1;
821
+ const worldPerPixel = 2 * Math.tan(fov / 2) / viewportHeight;
822
+ labelShown.clear();
823
+ const placed = [];
824
+ for (const id of ranked) {
825
+ const obj = nodeObjects.get(id);
826
+ const datum = nodeById.get(id);
827
+ if (!obj || !datum || !obj.labelText) continue;
828
+ const dist = Math.hypot(
829
+ (datum.x ?? 0) - cameraPos.x,
830
+ (datum.y ?? 0) - cameraPos.y,
831
+ (datum.z ?? 0) - cameraPos.z
832
+ );
833
+ const desiredHeight = LABEL_SCREEN_PX * worldPerPixel * dist;
834
+ const factor = Math.min(
835
+ LABEL_SCALE_MAX,
836
+ Math.max(LABEL_SCALE_MIN, desiredHeight / LABEL_TEXT_HEIGHT)
837
+ );
838
+ const projected = graph.graph2ScreenCoords(datum.x ?? 0, datum.y ?? 0, datum.z ?? 0);
839
+ const box = labelBox(projected, obj.labelText, LABEL_SCREEN_PX * factor);
840
+ if (box && !forced.has(id) && placed.some((other) => overlaps(box, other))) continue;
841
+ if (box) placed.push(box);
842
+ const sprite = ensureLabelSprite(obj);
843
+ if (!sprite) continue;
844
+ labelShown.add(id);
845
+ if (obj.labelBaseScale) {
846
+ sprite.scale.set(obj.labelBaseScale.x * factor, obj.labelBaseScale.y * factor, 0);
847
+ }
848
+ }
849
+ for (const [id, obj] of nodeObjects) {
850
+ if (obj.sprite) {
851
+ obj.sprite.visible = labelShown.has(id) && (!entranceActive || (entranceNodeAlpha.get(id) ?? 0) > LABEL_ENTRANCE_MIN);
852
+ }
853
+ }
854
+ lastLabelRank = performance.now();
855
+ }
856
+ function scheduleLabelRank() {
857
+ if (labelRankQueued || destroyed) return;
858
+ const wait = Math.max(0, LABEL_RERANK_MS - (performance.now() - lastLabelRank));
859
+ labelRankQueued = true;
860
+ setTimeout(() => {
861
+ labelRankQueued = false;
862
+ if (!destroyed) refreshLabels();
863
+ }, wait);
864
+ }
865
+ function controlsTarget() {
866
+ const t = graph.controls().target;
867
+ return t ? { x: t.x, y: t.y, z: t.z } : { x: 0, y: 0, z: 0 };
868
+ }
869
+ function getCamera() {
870
+ const pos = graph.cameraPosition();
871
+ const target = controlsTarget();
872
+ const dist = Math.hypot(pos.x - target.x, pos.y - target.y, pos.z - target.z) || FIT_DISTANCE;
873
+ return {
874
+ x: target.x,
875
+ y: target.y,
876
+ k: FIT_DISTANCE / dist,
877
+ position: { x: pos.x, y: pos.y, z: pos.z },
878
+ target
879
+ };
880
+ }
881
+ function flightMs(opts) {
882
+ const cfg = compilation.animation?.camera ?? null;
883
+ if (cfg === null || prefersReducedMotion() || opts?.duration === 0) return 0;
884
+ const requested = opts?.duration ?? cfg.duration;
885
+ return requested === "auto" ? AUTO_FLIGHT_MS : requested;
886
+ }
887
+ function clampK(k) {
888
+ return Math.min(K_MAX, Math.max(K_MIN, k));
889
+ }
890
+ function flyTo(target, opts) {
891
+ if (destroyed) return;
892
+ autoFit = false;
893
+ const ms = flightMs(opts);
894
+ if (target.position) {
895
+ graph.cameraPosition(
896
+ target.position,
897
+ target.target ?? { x: target.x, y: target.y, z: 0 },
898
+ ms
899
+ );
900
+ queueCameraChange();
901
+ return;
902
+ }
903
+ const look = { x: target.x, y: target.y, z: 0 };
904
+ const cam = getCamera();
905
+ const dist = FIT_DISTANCE / clampK(target.k ?? cam.k);
906
+ const dir = normalize({
907
+ x: cam.position.x - cam.target.x,
908
+ y: cam.position.y - cam.target.y,
909
+ z: cam.position.z - cam.target.z
910
+ });
911
+ graph.cameraPosition(
912
+ { x: look.x + dir.x * dist, y: look.y + dir.y * dist, z: look.z + dir.z * dist },
913
+ look,
914
+ ms
915
+ );
916
+ queueCameraChange();
917
+ }
918
+ function zoomToFit(opts) {
919
+ autoFit = false;
920
+ fitNow(opts);
921
+ }
922
+ function fitNow(opts) {
923
+ if (destroyed || nodeData.length === 0) return;
924
+ const camera = graph.camera();
925
+ const fit = computeFit(
926
+ nodeData.map((d) => ({
927
+ x: d.x ?? 0,
928
+ y: d.y ?? 0,
929
+ z: d.z ?? 0,
930
+ radius: d.node.radius ?? 0
931
+ })),
932
+ {
933
+ cameraPos: getCamera().position,
934
+ fovDeg: camera?.fov ?? DEFAULT_FOV_DEG,
935
+ aspect: camera?.aspect ?? 1,
936
+ viewportHeight: shell.getSize().height,
937
+ paddingPx: opts?.padding ?? FIT_PADDING_PX
938
+ }
939
+ );
940
+ if (!fit) return;
941
+ const distance = fit.distance * entranceCameraPull;
942
+ graph.cameraPosition(
943
+ {
944
+ x: fit.center.x + fit.dir.x * distance,
945
+ y: fit.center.y + fit.dir.y * distance,
946
+ z: fit.center.z + fit.dir.z * distance
947
+ },
948
+ fit.center,
949
+ flightMs(opts)
950
+ );
951
+ queueCameraChange();
952
+ }
953
+ function zoomToNode(nodeId, opts) {
954
+ if (destroyed) return;
955
+ autoFit = false;
956
+ const datum = nodeById.get(nodeId);
957
+ if (!datum) return;
958
+ const at = { x: datum.x ?? 0, y: datum.y ?? 0, z: datum.z ?? 0 };
959
+ const dir = normalize(at);
960
+ const dist = opts?.scale ? NODE_FOCUS_DISTANCE / clampK(opts.scale) : NODE_FOCUS_DISTANCE;
961
+ graph.cameraPosition(
962
+ { x: at.x + dir.x * dist, y: at.y + dir.y * dist, z: at.z + dir.z * dist },
963
+ at,
964
+ flightMs(opts)
965
+ );
966
+ queueCameraChange();
967
+ }
968
+ function queueCameraChange() {
969
+ if (cameraChangeQueued || destroyed || !options?.onCameraChange) return;
970
+ cameraChangeQueued = true;
971
+ requestAnimationFrame(() => {
972
+ cameraChangeQueued = false;
973
+ if (destroyed) return;
974
+ options.onCameraChange?.(getCamera());
975
+ });
976
+ }
977
+ function tooltipFormatter() {
978
+ const t = options?.tooltip;
979
+ return t && typeof t === "object" ? t.formatter ?? null : null;
980
+ }
981
+ function toWrapperXY(p) {
982
+ const screen = graph.graph2ScreenCoords(p.x, p.y, p.z);
983
+ if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) return null;
984
+ const s = surfaceEl.getBoundingClientRect();
985
+ const w = shell.wrapper.getBoundingClientRect();
986
+ return { x: screen.x + (s.left - w.left), y: screen.y + (s.top - w.top) };
987
+ }
988
+ function applyFormatterResult(result, x, y) {
989
+ const tm = shell.tooltipManager;
990
+ if (!tm) return;
991
+ if (result === null) hideTooltip();
992
+ else if (typeof result === "string") tm.show({ text: result }, x, y);
993
+ else if (result instanceof HTMLElement) tm.show({ element: result }, x, y);
994
+ else tm.show(result, x, y);
995
+ }
996
+ function anchorTooltip() {
997
+ const tm = shell.tooltipManager;
998
+ if (!tm || openTooltip === null) return;
999
+ const formatter = tooltipFormatter();
1000
+ const key = openTooltip.kind === "node" ? `node:${openTooltip.id}` : `edge:${openTooltip.index}`;
1001
+ if (openTooltip.kind === "node") {
1002
+ const id = openTooltip.id;
1003
+ const datum = nodeById.get(id);
1004
+ const defaults2 = compilation.tooltipDescriptors.get(id);
1005
+ if (!datum || !defaults2) return;
1006
+ const at2 = toWrapperXY({ x: datum.x ?? 0, y: datum.y ?? 0, z: datum.z ?? 0 });
1007
+ if (!at2) return;
1008
+ if (key === renderedTooltip) {
1009
+ tm.move(at2.x, at2.y);
1010
+ return;
1011
+ }
1012
+ renderedTooltip = key;
1013
+ if (!formatter) {
1014
+ tm.show(defaults2, at2.x, at2.y);
1015
+ return;
1016
+ }
1017
+ applyFormatterResult(
1018
+ formatter({ kind: "node", data: nodeDataMap.get(id) ?? {} }, defaults2),
1019
+ at2.x,
1020
+ at2.y
1021
+ );
1022
+ return;
1023
+ }
1024
+ const link = linkData[openTooltip.index];
1025
+ if (!link) return;
1026
+ const a = endpointPos(link.source);
1027
+ const b = endpointPos(link.target);
1028
+ const at = toWrapperXY({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 });
1029
+ if (!at) return;
1030
+ if (key === renderedTooltip) {
1031
+ tm.move(at.x, at.y);
1032
+ return;
1033
+ }
1034
+ renderedTooltip = key;
1035
+ const defaults = buildEdgeTooltip(link.edge);
1036
+ if (!formatter) {
1037
+ tm.show(defaults, at.x, at.y);
1038
+ return;
1039
+ }
1040
+ applyFormatterResult(
1041
+ formatter({ kind: "edge", data: link.edge.data ?? {} }, defaults),
1042
+ at.x,
1043
+ at.y
1044
+ );
1045
+ }
1046
+ function endpointPos(endpoint) {
1047
+ if (typeof endpoint !== "string") {
1048
+ return { x: endpoint.x ?? 0, y: endpoint.y ?? 0, z: endpoint.z ?? 0 };
1049
+ }
1050
+ const datum = nodeById.get(endpoint);
1051
+ return { x: datum?.x ?? 0, y: datum?.y ?? 0, z: datum?.z ?? 0 };
1052
+ }
1053
+ function hideTooltip() {
1054
+ openTooltip = null;
1055
+ renderedTooltip = null;
1056
+ shell.tooltipManager?.hide();
1057
+ }
1058
+ function legendSetting() {
1059
+ return options?.legend ?? currentSpec.legend;
1060
+ }
1061
+ function legendConfig() {
1062
+ const l = legendSetting();
1063
+ if (l && typeof l === "object") {
1064
+ return { interactive: l.interactive ?? true, counts: l.counts ?? true };
1065
+ }
1066
+ return { interactive: true, counts: true };
1067
+ }
1068
+ function getLegend() {
1069
+ const nodeEntries = "entries" in compilation.legend ? compilation.legend.entries : [];
1070
+ return {
1071
+ field: compilation.legendField,
1072
+ nodes: nodeEntries.filter((e) => !e.overflow).map((e) => ({
1073
+ label: e.label,
1074
+ color: e.color,
1075
+ count: e.count,
1076
+ active: activeCategories.size === 0 || activeCategories.has(e.label)
1077
+ })),
1078
+ edges: (compilation.edgeLegend ?? []).map((e) => ({
1079
+ label: e.label,
1080
+ color: e.color,
1081
+ count: e.count
1082
+ }))
1083
+ };
1084
+ }
1085
+ function renderLegend() {
1086
+ if (!shell.legendEl) return;
1087
+ const view = { nodes: getLegend().nodes, edges: getLegend().edges };
1088
+ if (!legendController) {
1089
+ const cfg = legendConfig();
1090
+ legendController = createGraphLegend(shell.legendEl, view, {
1091
+ interactive: cfg.interactive,
1092
+ counts: cfg.counts,
1093
+ onToggle: (value) => toggleLegendCategory(value),
1094
+ onHover: (value) => {
1095
+ const field = compilation.legendField;
1096
+ options?.onLegendHover?.(value !== null && field ? { field, value } : null);
1097
+ }
1098
+ });
1099
+ } else {
1100
+ legendController.update(view);
1101
+ }
1102
+ shell.syncChromeInset();
1103
+ }
1104
+ function toggleLegendCategory(value) {
1105
+ if (activeCategories.has(value)) activeCategories.delete(value);
1106
+ else activeCategories.add(value);
1107
+ recomputeHighlight();
1108
+ renderLegend();
1109
+ armEmphasis();
1110
+ options?.onLegendToggle?.([...activeCategories]);
1111
+ emitHighlightChange();
1112
+ }
1113
+ function handleNodeClick(nodeId, shiftKey) {
1114
+ if (shiftKey) {
1115
+ if (selectedNodeIds.has(nodeId)) selectedNodeIds.delete(nodeId);
1116
+ else selectedNodeIds.add(nodeId);
1117
+ } else {
1118
+ selectedNodeIds = /* @__PURE__ */ new Set([nodeId]);
1119
+ }
1120
+ armEmphasis();
1121
+ options?.onSelectionChange?.([...selectedNodeIds]);
1122
+ options?.onNodeClick?.(nodeDataMap.get(nodeId) ?? {});
1123
+ }
1124
+ const raycaster = new Raycaster();
1125
+ const pointer = new Vector2();
1126
+ function pickNode(event) {
1127
+ const canvas = graph.renderer()?.domElement;
1128
+ const camera = graph.camera();
1129
+ if (!canvas || !camera) return null;
1130
+ const rect = canvas.getBoundingClientRect();
1131
+ pointer.x = (event.clientX - rect.left) / (rect.width || 1) * 2 - 1;
1132
+ pointer.y = -((event.clientY - rect.top) / (rect.height || 1)) * 2 + 1;
1133
+ raycaster.setFromCamera(pointer, camera);
1134
+ const groups = [...nodeObjects.values()].map((o) => o.group);
1135
+ const hits = raycaster.intersectObjects(groups, true);
1136
+ for (const hit of hits) {
1137
+ let obj = hit.object;
1138
+ while (obj) {
1139
+ const id = obj.userData?.nodeId;
1140
+ if (typeof id === "string") return id;
1141
+ obj = obj.parent;
1142
+ }
1143
+ }
1144
+ return null;
1145
+ }
1146
+ const onCanvasCameraInput = () => {
1147
+ autoFit = false;
1148
+ };
1149
+ const onCanvasDblClick = (event) => {
1150
+ const id = pickNode(event);
1151
+ if (id) options?.onNodeDoubleClick?.(nodeDataMap.get(id) ?? {});
1152
+ };
1153
+ const onContextLost = (event) => {
1154
+ event.preventDefault();
1155
+ };
1156
+ function endEntrance() {
1157
+ entranceActive = false;
1158
+ entranceCameraPull = 1;
1159
+ for (const obj of nodeObjects.values()) obj.mesh.scale.setScalar(1);
1160
+ paint();
1161
+ }
1162
+ function startEntrance() {
1163
+ const enter = compilation.animation?.enter ?? null;
1164
+ if (options?.suppressEntrance || !enter || prefersReducedMotion()) {
1165
+ endEntrance();
1166
+ return;
1167
+ }
1168
+ const stagger = enter.stagger && compilation.nodes.length <= ENTRANCE_STAGGER_MAX_NODES;
1169
+ const plan = planEntrance(
1170
+ nodeData.map((d) => ({ id: d.id, x: d.x ?? 0, y: d.y ?? 0 })),
1171
+ compilation.edges,
1172
+ enter.duration,
1173
+ stagger
1174
+ );
1175
+ const ease = resolveEase(enter.ease);
1176
+ entranceActive = true;
1177
+ entranceCameraPull = enter.cameraFit ? ENTRANCE_CAMERA_PULLBACK : 1;
1178
+ for (const id of plan.nodeOffset.keys()) {
1179
+ entranceNodeAlpha.set(id, 0);
1180
+ entranceNodeScale.set(id, ENTRANCE_MIN_SCALE);
1181
+ }
1182
+ for (const index of plan.linkOffset.keys()) entranceEdgeAlpha.set(index, 0);
1183
+ paint();
1184
+ const tween = createTween({
1185
+ duration: plan.span,
1186
+ ease: linear,
1187
+ apply: (t) => {
1188
+ const elapsed = t * plan.span;
1189
+ for (const [id, offset] of plan.nodeOffset) {
1190
+ const p = elementProgress(elapsed, offset, enter.duration, ease);
1191
+ entranceNodeAlpha.set(id, popAlpha(p));
1192
+ entranceNodeScale.set(id, Math.max(ENTRANCE_MIN_SCALE, popScale(p)));
1193
+ }
1194
+ for (const [index, offset] of plan.linkOffset) {
1195
+ entranceEdgeAlpha.set(index, elementProgress(elapsed, offset, enter.duration, ease));
1196
+ }
1197
+ if (enter.cameraFit) {
1198
+ entranceCameraPull = ENTRANCE_CAMERA_PULLBACK + (1 - ENTRANCE_CAMERA_PULLBACK) * ease(t);
1199
+ if (autoFit && options?.fitOnLoad !== false) fitNow({ duration: 0 });
1200
+ }
1201
+ paint();
1202
+ },
1203
+ onDone: endEntrance
1204
+ });
1205
+ scheduler.add(tween);
1206
+ }
1207
+ function search(query) {
1208
+ if (destroyed) return;
1209
+ searchManager.search(query, compilation.nodes);
1210
+ armEmphasis();
1211
+ }
1212
+ function clearSearch() {
1213
+ if (destroyed) return;
1214
+ searchManager.clearSearch();
1215
+ armEmphasis();
1216
+ }
1217
+ function selectNode(nodeId, opts) {
1218
+ if (destroyed) return;
1219
+ selectedNodeIds = /* @__PURE__ */ new Set([nodeId]);
1220
+ armEmphasis();
1221
+ options?.onSelectionChange?.([nodeId]);
1222
+ const shouldFly = opts?.fly ?? compilation.interaction.selectFlyTo;
1223
+ if (shouldFly) zoomToNode(nodeId, opts);
1224
+ }
1225
+ function highlight(target, opts) {
1226
+ if (destroyed) return;
1227
+ transientTarget = target;
1228
+ refreshTransientHighlight();
1229
+ highlightDimOpacity = opts?.dimOpacity ?? null;
1230
+ recomputeHighlight();
1231
+ armEmphasis();
1232
+ emitHighlightChange();
1233
+ }
1234
+ function clearHighlight() {
1235
+ if (destroyed) return;
1236
+ transientTarget = null;
1237
+ transientHighlight = null;
1238
+ highlightDimOpacity = null;
1239
+ recomputeHighlight();
1240
+ armEmphasis();
1241
+ emitHighlightChange();
1242
+ }
1243
+ function setActiveCategories(values) {
1244
+ if (destroyed) return;
1245
+ activeCategories = new Set(values);
1246
+ recomputeHighlight();
1247
+ renderLegend();
1248
+ armEmphasis();
1249
+ emitHighlightChange();
1250
+ }
1251
+ function doResize() {
1252
+ if (destroyed) return;
1253
+ const renderer = graph.renderer();
1254
+ const dpr = Math.min(MAX_PIXEL_RATIO, globalThis.devicePixelRatio || 1);
1255
+ if (renderer?.getPixelRatio?.() !== dpr) renderer?.setPixelRatio?.(dpr);
1256
+ const { width: width2, height: height2 } = shell.getSize();
1257
+ graph.width(width2).height(height2);
1258
+ shell.syncChromeInset();
1259
+ }
1260
+ function update(newSpec) {
1261
+ if (destroyed) return;
1262
+ const next = ctx.compile(newSpec);
1263
+ if (next.numDimensions !== 3) {
1264
+ shell.warn("createGraph: update() cannot change dimensions; remount the graph");
1265
+ return;
1266
+ }
1267
+ scheduler.finishAll();
1268
+ const prevNodes = nodeData.map((d, index) => ({
1269
+ ...d.node,
1270
+ x: d.x ?? 0,
1271
+ y: d.y ?? 0,
1272
+ index
1273
+ }));
1274
+ const prevEdges = compilation.edges.map((edge) => {
1275
+ const a = endpointPos(edge.source);
1276
+ const b = endpointPos(edge.target);
1277
+ return { ...edge, sourceX: a.x, sourceY: a.y, targetX: b.x, targetY: b.y };
1278
+ });
1279
+ const prevConfig = compilation.simulationConfig;
1280
+ const prevUseLinkWidth = useLinkWidth;
1281
+ currentSpec = newSpec;
1282
+ compilation = next;
1283
+ renderedTooltip = null;
1284
+ const diff = diffGraphUpdate(
1285
+ prevNodes,
1286
+ prevEdges,
1287
+ compilation,
1288
+ prevConfig,
1289
+ prevConfig.seed ?? 0
1290
+ );
1291
+ rebuildDerived();
1292
+ shell.renderChrome(compilation);
1293
+ renderLegend();
1294
+ refreshTransientHighlight();
1295
+ recomputeHighlight();
1296
+ reRunSearch();
1297
+ const shapeClassChanged = useLinkWidth !== prevUseLinkWidth || [...linkObjects.values()].some((obj) => !linkShapeMatches(obj, useLinkWidth));
1298
+ if (useLinkWidth !== prevUseLinkWidth) graph.linkHoverPrecision(useLinkWidth ? 4 : 8);
1299
+ if (diff.visualOnly) {
1300
+ for (let i = 0; i < compilation.nodes.length; i++) {
1301
+ const node = compilation.nodes[i];
1302
+ nodeData[i].node = node;
1303
+ const obj = nodeObjects.get(node.id);
1304
+ if (obj) applyNodeVisuals(obj, node, labelColor());
1305
+ }
1306
+ for (let i = 0; i < compilation.edges.length; i++) {
1307
+ const edge = compilation.edges[i];
1308
+ linkData[i].edge = edge;
1309
+ if (shapeClassChanged) continue;
1310
+ const obj = linkObjects.get(i);
1311
+ if (obj) applyLinkVisuals(obj, edge, useLinkWidth);
1312
+ }
1313
+ if (shapeClassChanged) {
1314
+ for (const obj of linkObjects.values()) disposeLinkObject(obj);
1315
+ linkObjects.clear();
1316
+ graph.linkThreeObject(linkThreeObject);
1317
+ }
1318
+ pruneInteractionState();
1319
+ armEmphasis();
1320
+ return;
1321
+ }
1322
+ const nextNodeIds = new Set(compilation.nodes.map((n) => n.id));
1323
+ for (const [id, obj] of nodeObjects) {
1324
+ if (!nextNodeIds.has(id)) {
1325
+ disposeNodeObject(obj);
1326
+ nodeObjects.delete(id);
1327
+ displayNodeAlpha.delete(id);
1328
+ entranceNodeAlpha.delete(id);
1329
+ entranceNodeScale.delete(id);
1330
+ }
1331
+ }
1332
+ for (const obj of linkObjects.values()) disposeLinkObject(obj);
1333
+ linkObjects.clear();
1334
+ displayEdgeAlpha.clear();
1335
+ entranceEdgeAlpha.clear();
1336
+ const survivors = new Map(nodeById);
1337
+ nodeData = compilation.nodes.map((node) => {
1338
+ const existing = survivors.get(node.id);
1339
+ if (existing) {
1340
+ existing.node = node;
1341
+ const obj = nodeObjects.get(node.id);
1342
+ if (obj) applyNodeVisuals(obj, node, labelColor());
1343
+ return existing;
1344
+ }
1345
+ const spawn = diff.spawnPositions.get(node.id) ?? { x: 0, y: 0 };
1346
+ return {
1347
+ id: node.id,
1348
+ node,
1349
+ x: spawn.x,
1350
+ y: spawn.y,
1351
+ z: seedZ(node.id, node.radius, node.community)
1352
+ };
1353
+ });
1354
+ linkData = compilation.edges.map((edge, edgeIndex) => ({
1355
+ source: edge.source,
1356
+ target: edge.target,
1357
+ edge,
1358
+ edgeIndex
1359
+ }));
1360
+ nodeById = new Map(nodeData.map((d) => [d.id, d]));
1361
+ applySimulationConfig(graph, compilation.simulationConfig, nodeData.length);
1362
+ graph.graphData({ nodes: nodeData, links: linkData });
1363
+ pruneInteractionState();
1364
+ armEmphasis();
1365
+ }
1366
+ function pruneInteractionState() {
1367
+ const ids = new Set(compilation.nodes.map((n) => n.id));
1368
+ if (hoveredNodeId && !ids.has(hoveredNodeId)) hoveredNodeId = null;
1369
+ const hadEdgeHover = hoveredLinkIndex !== null;
1370
+ hoveredLinkIndex = null;
1371
+ if (hadEdgeHover) options?.onEdgeHover?.(null);
1372
+ selectedNodeIds = new Set([...selectedNodeIds].filter((id) => ids.has(id)));
1373
+ if (openTooltip?.kind === "node" && !ids.has(openTooltip.id)) hideTooltip();
1374
+ if (openTooltip?.kind === "edge") hideTooltip();
1375
+ }
1376
+ function reRunSearch() {
1377
+ const q = searchManager.getQuery();
1378
+ if (q !== null) searchManager.search(q, compilation.nodes);
1379
+ }
1380
+ function destroy() {
1381
+ if (destroyed) return;
1382
+ destroyed = true;
1383
+ scheduler.cancelAll();
1384
+ if (pumpId !== null) {
1385
+ cancelAnimationFrame(pumpId);
1386
+ pumpId = null;
1387
+ }
1388
+ const canvas = graph.renderer()?.domElement;
1389
+ canvas?.removeEventListener("dblclick", onCanvasDblClick);
1390
+ canvas?.removeEventListener("pointerdown", onCanvasCameraInput);
1391
+ canvas?.removeEventListener("wheel", onCanvasCameraInput);
1392
+ canvas?.removeEventListener("webglcontextlost", onContextLost);
1393
+ if (controlsListener) {
1394
+ graph.controls().removeEventListener?.("change", controlsListener);
1395
+ controlsListener = null;
1396
+ }
1397
+ disconnectResize();
1398
+ const renderer = graph.renderer();
1399
+ graph._destructor();
1400
+ for (const obj of nodeObjects.values()) disposeNodeObject(obj);
1401
+ nodeObjects.clear();
1402
+ for (const obj of linkObjects.values()) disposeLinkObject(obj);
1403
+ linkObjects.clear();
1404
+ renderer?.dispose();
1405
+ renderer?.forceContextLoss?.();
1406
+ legendController?.destroy();
1407
+ legendController = null;
1408
+ shell.destroy();
1409
+ }
1410
+ rebuildDerived();
1411
+ shell.renderChrome(compilation);
1412
+ renderLegend();
1413
+ const initial = compilation.initialHighlight;
1414
+ if (initial) {
1415
+ activeCategories = new Set(initial.values);
1416
+ recomputeHighlight();
1417
+ }
1418
+ const requestedBackground = themeBackground(options?.theme) ?? themeBackground(currentSpec.theme);
1419
+ if (requestedBackground === "transparent") {
1420
+ graph.backgroundColor("rgba(0,0,0,0)");
1421
+ shell.wrapper.style.background = "transparent";
1422
+ } else {
1423
+ graph.backgroundColor(resolvedSurface(compilation.theme));
1424
+ }
1425
+ graph.showNavInfo(false).enableNodeDrag(false).nodeId("id").nodeLabel("").linkLabel("").nodeVal((d) => d.node.radius).nodeColor((d) => d.node.fill).nodeThreeObject((d) => nodeObjectFor(d).group).linkColor((d) => d.edge.stroke).linkWidth((d) => useLinkWidth ? d.edge.strokeWidth : 0).linkThreeObject(linkThreeObject).linkPositionUpdate((_obj, coords, d) => {
1426
+ const link = linkObjects.get(d.edgeIndex);
1427
+ return link ? updateLinkPosition(link, coords.start, coords.end) : false;
1428
+ }).linkHoverPrecision(useLinkWidth ? 4 : 8);
1429
+ const { width, height } = shell.getSize();
1430
+ graph.width(width).height(height);
1431
+ applySimulationConfig(graph, compilation.simulationConfig, compilation.nodes.length);
1432
+ buildGraphData(seedPositions(compilation.nodes));
1433
+ for (const n of compilation.nodes) displayNodeAlpha.set(n.id, n.opacity);
1434
+ for (let i = 0; i < compilation.edges.length; i++) {
1435
+ displayEdgeAlpha.set(i, edgeBaseAlpha?.get(i) ?? 0.3);
1436
+ }
1437
+ armEmphasis();
1438
+ startEntrance();
1439
+ graph.onNodeHover((node) => {
1440
+ const id = node ? node.id : null;
1441
+ if (id === hoveredNodeId) return;
1442
+ hoveredNodeId = id;
1443
+ if (id && hoveredLinkIndex !== null) {
1444
+ hoveredLinkIndex = null;
1445
+ options?.onEdgeHover?.(null);
1446
+ }
1447
+ armEmphasis();
1448
+ options?.onNodeHover?.(id ? nodeDataMap.get(id) ?? {} : null);
1449
+ if (id && shell.tooltipManager) {
1450
+ openTooltip = { kind: "node", id };
1451
+ anchorTooltip();
1452
+ } else {
1453
+ hideTooltip();
1454
+ }
1455
+ }).onLinkHover((link) => {
1456
+ const index = link ? link.edgeIndex : null;
1457
+ if (index === hoveredLinkIndex) return;
1458
+ if (hoveredNodeId !== null) return;
1459
+ hoveredLinkIndex = index;
1460
+ options?.onEdgeHover?.(link ? link.edge.data ?? {} : null);
1461
+ if (index !== null && shell.tooltipManager) {
1462
+ openTooltip = { kind: "edge", index };
1463
+ anchorTooltip();
1464
+ } else {
1465
+ hideTooltip();
1466
+ }
1467
+ }).onNodeClick((node, event) => {
1468
+ handleNodeClick(node.id, Boolean(event?.shiftKey));
1469
+ }).onBackgroundClick(() => {
1470
+ if (selectedNodeIds.size === 0) return;
1471
+ selectedNodeIds = /* @__PURE__ */ new Set();
1472
+ armEmphasis();
1473
+ options?.onSelectionChange?.([]);
1474
+ }).onEngineTick(() => {
1475
+ if (destroyed) return;
1476
+ anchorTooltip();
1477
+ if (autoFit && options?.fitOnLoad !== false) {
1478
+ const now = performance.now();
1479
+ if (now - lastAutoFit > AUTO_FIT_INTERVAL_MS) {
1480
+ lastAutoFit = now;
1481
+ fitNow({ duration: 0 });
1482
+ refreshLabels();
1483
+ }
1484
+ }
1485
+ }).onEngineStop(() => {
1486
+ if (destroyed) return;
1487
+ if (autoFit && options?.fitOnLoad !== false) {
1488
+ autoFit = false;
1489
+ fitNow({ duration: 0 });
1490
+ }
1491
+ refreshLabels();
1492
+ });
1493
+ controlsListener = () => {
1494
+ if (destroyed) return;
1495
+ scheduleLabelRank();
1496
+ anchorTooltip();
1497
+ queueCameraChange();
1498
+ };
1499
+ graph.controls().addEventListener?.(
1500
+ "change",
1501
+ controlsListener
1502
+ );
1503
+ const canvasEl = graph.renderer()?.domElement;
1504
+ canvasEl?.addEventListener("dblclick", onCanvasDblClick);
1505
+ canvasEl?.addEventListener("pointerdown", onCanvasCameraInput);
1506
+ canvasEl?.addEventListener("wheel", onCanvasCameraInput, { passive: true });
1507
+ canvasEl?.addEventListener("webglcontextlost", onContextLost);
1508
+ const disconnectResize = shell.observeResize(() => doResize());
1509
+ return {
1510
+ update,
1511
+ // `update()` already routes an id-set-preserving change through the
1512
+ // material-mutation path, which never touches `graphData()`.
1513
+ updateVisuals: update,
1514
+ search,
1515
+ clearSearch,
1516
+ zoomToFit,
1517
+ zoomToNode,
1518
+ flyTo,
1519
+ centerAt: (x, y, opts) => flyTo({ x, y }, opts),
1520
+ getCamera,
1521
+ selectNode,
1522
+ getSelectedNodes: () => [...selectedNodeIds],
1523
+ getSearchMatches: () => [...searchManager.getMatches() ?? []],
1524
+ highlight,
1525
+ clearHighlight,
1526
+ getHighlight: () => highlightSet ? [...highlightSet] : null,
1527
+ getLegend,
1528
+ setActiveCategories,
1529
+ getActiveCategories: () => [...activeCategories],
1530
+ resize: doResize,
1531
+ destroy
1532
+ };
1533
+ }
1534
+ function themeBackground(theme) {
1535
+ const colors = theme?.colors;
1536
+ if (!colors || Array.isArray(colors)) return void 0;
1537
+ return typeof colors.background === "string" ? colors.background : void 0;
1538
+ }
1539
+
1540
+ // src/graph-3d/index.ts
1541
+ registerGraphRenderer(3, createGraph3DRenderer);
1542
+ function createGraph3D(container, spec, options) {
1543
+ return createGraph(container, { ...spec, dimensions: 3 }, options);
1544
+ }
1545
+ export {
1546
+ FIT_DISTANCE,
1547
+ LABEL_BUDGET_3D,
1548
+ LINK_WIDTH_MAX_EDGES,
1549
+ NODE_FOCUS_DISTANCE,
1550
+ createGraph3D,
1551
+ createGraph3DRenderer,
1552
+ forceCluster3D,
1553
+ resolveEmphasis,
1554
+ resolveVisibleLabels
1555
+ };
1556
+ //# sourceMappingURL=index.js.map