@fxhash/open-form-graph 0.0.2 → 0.0.3
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/dist/OpenFormGraph-CdcWNN86.d.ts +24 -0
- package/dist/OpenFormGraph-i8VSTamk.js +2170 -0
- package/dist/OpenFormGraph-i8VSTamk.js.map +1 -0
- package/dist/_types-CjgCTzqc.d.ts +82 -0
- package/dist/components.d.ts +2 -1
- package/dist/components.js +2 -2
- package/dist/constants-BNPHdbBA.d.ts +313 -0
- package/dist/index.d.ts +21 -7
- package/dist/index.js +3 -3
- package/dist/{provider-CTDz6ZQd.js → provider-PqOen2FE.js} +9 -6
- package/dist/provider-PqOen2FE.js.map +1 -0
- package/dist/provider.d.ts +3 -2
- package/dist/provider.js +2 -2
- package/package.json +3 -5
- package/dist/OpenFormGraph-0EqYrhjv.d.ts +0 -17
- package/dist/OpenFormGraph-wo_Y20C6.js +0 -1277
- package/dist/OpenFormGraph-wo_Y20C6.js.map +0 -1
- package/dist/constants-DU_wYtaU.d.ts +0 -242
- package/dist/provider-CTDz6ZQd.js.map +0 -1
@@ -0,0 +1,2170 @@
|
|
1
|
+
import { DEFAULT_GRAPH_CONFIG, VOID_DETACH_ID, VOID_ROOT_ID, useOpenFormGraph } from "./provider-PqOen2FE.js";
|
2
|
+
import { useEffect, useRef } from "react";
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
4
|
+
import { forceCenter, forceCollide, forceManyBody, forceRadial, forceSimulation } from "d3-force";
|
5
|
+
import { scaleLinear, scaleLog } from "d3-scale";
|
6
|
+
import { EventEmitter, float2hex, xorshiftString } from "@fxhash/utils";
|
7
|
+
|
8
|
+
//#region src/util/types.ts
|
9
|
+
function isSimNode(node) {
|
10
|
+
return typeof node === "object" && "id" in node;
|
11
|
+
}
|
12
|
+
function isSimLink(link) {
|
13
|
+
return typeof link === "object" && "source" in link && typeof link.source !== "string";
|
14
|
+
}
|
15
|
+
function isCustomHighlight(highlight) {
|
16
|
+
if (typeof highlight === "string") return false;
|
17
|
+
return true;
|
18
|
+
}
|
19
|
+
|
20
|
+
//#endregion
|
21
|
+
//#region src/util/graph.ts
|
22
|
+
function getNodeId(n) {
|
23
|
+
return typeof n === "object" && n !== null && "id" in n ? n.id : n;
|
24
|
+
}
|
25
|
+
function getParents(id, links) {
|
26
|
+
return links.filter((l) => {
|
27
|
+
const targetId = isSimNode(l.target) ? l.target.id : l.target;
|
28
|
+
return targetId === id;
|
29
|
+
}).map((link) => isSimNode(link.source) ? link.source.id : link.source.toString());
|
30
|
+
}
|
31
|
+
function getAllParentsUntil(nodeId, links, stopAtId) {
|
32
|
+
const parent = getParents(nodeId, links)[0];
|
33
|
+
if (!parent || parent === stopAtId) return [];
|
34
|
+
return [parent, ...getAllParentsUntil(parent, links, stopAtId)];
|
35
|
+
}
|
36
|
+
function getChildren(id, links) {
|
37
|
+
return links.filter((l) => {
|
38
|
+
const sourceId = isSimNode(l.source) ? l.source.id : l.source;
|
39
|
+
return sourceId === id;
|
40
|
+
}).map((link) => link.target.toString());
|
41
|
+
}
|
42
|
+
function getClusterSize(id, links) {
|
43
|
+
const children = getChildren(id, links);
|
44
|
+
return children.reduce((acc, childId) => {
|
45
|
+
return acc + getClusterSize(childId, links);
|
46
|
+
}, children.length || 0);
|
47
|
+
}
|
48
|
+
function getNodeDepth(id, links) {
|
49
|
+
function getDepth(id$1, depth) {
|
50
|
+
const parents = getParents(id$1, links);
|
51
|
+
if (parents.length === 0) return depth;
|
52
|
+
return getDepth(parents[0], depth + 1);
|
53
|
+
}
|
54
|
+
return getDepth(id, 0);
|
55
|
+
}
|
56
|
+
function getRootParent(id, links, stop) {
|
57
|
+
let currentId = id;
|
58
|
+
while (true) {
|
59
|
+
const parents = getParents(currentId, links);
|
60
|
+
if (stop && parents.includes(stop)) return currentId;
|
61
|
+
if (parents.length === 0) return currentId;
|
62
|
+
currentId = parents[0];
|
63
|
+
}
|
64
|
+
}
|
65
|
+
function hasOnlyLeafs(id, links) {
|
66
|
+
const children = getChildren(id, links);
|
67
|
+
return children.every((childId) => getChildren(childId, links).length === 0);
|
68
|
+
}
|
69
|
+
function getNodeSubgraph(nodeId, nodes, links, rootId) {
|
70
|
+
const nodesById = Object.fromEntries(nodes.map((n) => [n.id, n]));
|
71
|
+
const parentSet = new Set();
|
72
|
+
const childSet = new Set();
|
73
|
+
const subLinks = new Set();
|
74
|
+
let currentId = nodeId;
|
75
|
+
while (currentId !== rootId) {
|
76
|
+
const parentLink = links.find((l) => {
|
77
|
+
const targetId = isSimNode(l.target) ? l.target.id : l.target;
|
78
|
+
return targetId === currentId;
|
79
|
+
});
|
80
|
+
if (!parentLink) break;
|
81
|
+
const parentId = isSimNode(parentLink.source) ? parentLink.source.id : parentLink.source;
|
82
|
+
if (parentSet.has(parentId.toString())) break;
|
83
|
+
parentSet.add(parentId.toString());
|
84
|
+
subLinks.add(parentLink);
|
85
|
+
currentId = parentId.toString();
|
86
|
+
}
|
87
|
+
function collectChildren(id) {
|
88
|
+
for (const link of links) {
|
89
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
90
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
91
|
+
if (sourceId === id && !childSet.has(targetId.toString())) {
|
92
|
+
childSet.add(targetId.toString());
|
93
|
+
subLinks.add(link);
|
94
|
+
collectChildren(targetId.toString());
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
collectChildren(nodeId);
|
99
|
+
const validIds = new Set([
|
100
|
+
...parentSet,
|
101
|
+
nodeId,
|
102
|
+
...childSet
|
103
|
+
]);
|
104
|
+
const filteredLinks = Array.from(subLinks).filter((link) => {
|
105
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
106
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
107
|
+
return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
|
108
|
+
});
|
109
|
+
const allNodeIds = Array.from(validIds);
|
110
|
+
const subNodes = allNodeIds.map((id) => nodesById[id]).filter(Boolean);
|
111
|
+
return {
|
112
|
+
nodes: subNodes,
|
113
|
+
links: filteredLinks
|
114
|
+
};
|
115
|
+
}
|
116
|
+
|
117
|
+
//#endregion
|
118
|
+
//#region src/util/img.ts
|
119
|
+
function loadHTMLImageElement(src) {
|
120
|
+
return new Promise((resolve, reject) => {
|
121
|
+
const img$1 = new Image();
|
122
|
+
img$1.onload = () => resolve(img$1);
|
123
|
+
img$1.onerror = reject;
|
124
|
+
img$1.src = src;
|
125
|
+
});
|
126
|
+
}
|
127
|
+
|
128
|
+
//#endregion
|
129
|
+
//#region src/sim/TransformCanvas.ts
|
130
|
+
const MIN_ZOOM = .1;
|
131
|
+
const MAX_ZOOM = 10;
|
132
|
+
const CLICK_THRESHOLD = 5;
|
133
|
+
const ANIMATION_SPEED = .07;
|
134
|
+
const DRAG_ANIMATION_SPEED = .5;
|
135
|
+
const ANIMATION_THRESHOLD = {
|
136
|
+
x: .5,
|
137
|
+
y: .5,
|
138
|
+
scale: .001
|
139
|
+
};
|
140
|
+
const MOMENTUM_DAMPING = .91;
|
141
|
+
const MIN_VELOCITY = .5;
|
142
|
+
var TransformCanvas = class {
|
143
|
+
canvas;
|
144
|
+
transform = {
|
145
|
+
x: 0,
|
146
|
+
y: 0,
|
147
|
+
scale: 1
|
148
|
+
};
|
149
|
+
targetTransform = {
|
150
|
+
x: 0,
|
151
|
+
y: 0,
|
152
|
+
scale: 1
|
153
|
+
};
|
154
|
+
isAnimating = false;
|
155
|
+
animationFrame = null;
|
156
|
+
focus = null;
|
157
|
+
offset = {
|
158
|
+
x: 0,
|
159
|
+
y: 0
|
160
|
+
};
|
161
|
+
isDragging = false;
|
162
|
+
dragStart = null;
|
163
|
+
hasMoved = false;
|
164
|
+
lastPointerPos = {
|
165
|
+
x: 0,
|
166
|
+
y: 0
|
167
|
+
};
|
168
|
+
lastMovePos = {
|
169
|
+
x: 0,
|
170
|
+
y: 0
|
171
|
+
};
|
172
|
+
velocity = {
|
173
|
+
x: 0,
|
174
|
+
y: 0
|
175
|
+
};
|
176
|
+
lastDragTime = 0;
|
177
|
+
momentumFrame = null;
|
178
|
+
touchStart = null;
|
179
|
+
pinchStartDist = null;
|
180
|
+
pinchStartScale = 1;
|
181
|
+
noInteraction = false;
|
182
|
+
dpr = 1;
|
183
|
+
resizeObserver = null;
|
184
|
+
mediaQueryList = null;
|
185
|
+
onUpdate;
|
186
|
+
onClick;
|
187
|
+
onMove;
|
188
|
+
constructor(canvas, options) {
|
189
|
+
this.canvas = canvas;
|
190
|
+
this.onUpdate = options?.onUpdate;
|
191
|
+
this.onClick = options?.onClick;
|
192
|
+
this.onMove = options?.onMove;
|
193
|
+
this.offset = options?.offset || {
|
194
|
+
x: 0,
|
195
|
+
y: 0
|
196
|
+
};
|
197
|
+
this.dpr = window.devicePixelRatio || 1;
|
198
|
+
this.bindEventHandlers();
|
199
|
+
this.attachEventListeners();
|
200
|
+
this.setupDPRMonitoring();
|
201
|
+
}
|
202
|
+
bindEventHandlers() {
|
203
|
+
this.handleWheel = this.handleWheel.bind(this);
|
204
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
205
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
206
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
207
|
+
this.handleCanvasClick = this.handleCanvasClick.bind(this);
|
208
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
209
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
210
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
211
|
+
}
|
212
|
+
attachEventListeners() {
|
213
|
+
this.canvas.addEventListener("wheel", this.handleWheel, { passive: false });
|
214
|
+
this.canvas.addEventListener("mousedown", this.handleMouseDown);
|
215
|
+
this.canvas.addEventListener("click", this.handleCanvasClick);
|
216
|
+
window.addEventListener("mousemove", this.handleMouseMove);
|
217
|
+
window.addEventListener("mouseup", this.handleMouseUp);
|
218
|
+
this.canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
|
219
|
+
this.canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
|
220
|
+
this.canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
|
221
|
+
this.canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
|
222
|
+
}
|
223
|
+
setupDPRMonitoring() {
|
224
|
+
if (typeof ResizeObserver !== "undefined") {
|
225
|
+
this.resizeObserver = new ResizeObserver(() => {
|
226
|
+
this.updateDPR();
|
227
|
+
});
|
228
|
+
this.resizeObserver.observe(this.canvas);
|
229
|
+
}
|
230
|
+
const updateDPRFromMediaQuery = () => {
|
231
|
+
this.updateDPR();
|
232
|
+
};
|
233
|
+
const dpr = window.devicePixelRatio || 1;
|
234
|
+
this.mediaQueryList = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
235
|
+
if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
|
236
|
+
}
|
237
|
+
updateDPR() {
|
238
|
+
const newDPR = window.devicePixelRatio || 1;
|
239
|
+
if (newDPR !== this.dpr) {
|
240
|
+
const oldDPR = this.dpr;
|
241
|
+
this.dpr = newDPR;
|
242
|
+
const scale = newDPR / oldDPR;
|
243
|
+
this.transform.x *= scale;
|
244
|
+
this.transform.y *= scale;
|
245
|
+
this.targetTransform.x *= scale;
|
246
|
+
this.targetTransform.y *= scale;
|
247
|
+
this.onUpdate?.(this.transform);
|
248
|
+
if (this.mediaQueryList) {
|
249
|
+
const updateDPRFromMediaQuery = () => {
|
250
|
+
this.updateDPR();
|
251
|
+
};
|
252
|
+
if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
|
253
|
+
this.mediaQueryList = window.matchMedia(`(resolution: ${newDPR}dppx)`);
|
254
|
+
if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
toCanvasCoords(clientX, clientY) {
|
259
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
260
|
+
return {
|
261
|
+
x: (clientX - rect$1.left) * this.dpr,
|
262
|
+
y: (clientY - rect$1.top) * this.dpr
|
263
|
+
};
|
264
|
+
}
|
265
|
+
toCSSCoords(clientX, clientY) {
|
266
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
267
|
+
return {
|
268
|
+
x: clientX - rect$1.left,
|
269
|
+
y: clientY - rect$1.top
|
270
|
+
};
|
271
|
+
}
|
272
|
+
startMomentum() {
|
273
|
+
if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
|
274
|
+
if (!this.momentumFrame) this.momentumFrame = requestAnimationFrame(this.applyMomentum);
|
275
|
+
}
|
276
|
+
}
|
277
|
+
applyMomentum = () => {
|
278
|
+
this.targetTransform.x += this.velocity.x * this.dpr;
|
279
|
+
this.targetTransform.y += this.velocity.y * this.dpr;
|
280
|
+
this.velocity.x *= MOMENTUM_DAMPING;
|
281
|
+
this.velocity.y *= MOMENTUM_DAMPING;
|
282
|
+
if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
|
283
|
+
this.startAnimation();
|
284
|
+
this.momentumFrame = requestAnimationFrame(this.applyMomentum);
|
285
|
+
} else {
|
286
|
+
this.velocity = {
|
287
|
+
x: 0,
|
288
|
+
y: 0
|
289
|
+
};
|
290
|
+
this.momentumFrame = null;
|
291
|
+
}
|
292
|
+
};
|
293
|
+
lerp(a, b, t) {
|
294
|
+
return a + (b - a) * t;
|
295
|
+
}
|
296
|
+
animateTransform = () => {
|
297
|
+
const target = this.focus?.() || this.targetTransform;
|
298
|
+
const prev = this.transform;
|
299
|
+
const animationSpeed = this.isDragging ? DRAG_ANIMATION_SPEED : ANIMATION_SPEED;
|
300
|
+
const next = {
|
301
|
+
x: this.lerp(prev.x, target.x, animationSpeed / this.dpr),
|
302
|
+
y: this.lerp(prev.y, target.y, animationSpeed / this.dpr),
|
303
|
+
scale: this.lerp(prev.scale, target.scale, animationSpeed / this.dpr)
|
304
|
+
};
|
305
|
+
const done = Math.abs(next.x - target.x) < ANIMATION_THRESHOLD.x && Math.abs(next.y - target.y) < ANIMATION_THRESHOLD.y && Math.abs(next.scale - target.scale) < ANIMATION_THRESHOLD.scale;
|
306
|
+
if (done) {
|
307
|
+
this.transform = { ...target };
|
308
|
+
this.stopAnimation();
|
309
|
+
} else {
|
310
|
+
this.transform = next;
|
311
|
+
this.onUpdate?.(this.transform);
|
312
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
313
|
+
}
|
314
|
+
};
|
315
|
+
startAnimation() {
|
316
|
+
if (!this.isAnimating) {
|
317
|
+
this.isAnimating = true;
|
318
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
319
|
+
}
|
320
|
+
}
|
321
|
+
stopAnimation() {
|
322
|
+
this.isAnimating = false;
|
323
|
+
this.focus = null;
|
324
|
+
if (this.animationFrame) {
|
325
|
+
cancelAnimationFrame(this.animationFrame);
|
326
|
+
this.animationFrame = null;
|
327
|
+
}
|
328
|
+
}
|
329
|
+
interruptAnimation() {
|
330
|
+
this.targetTransform = { ...this.transform };
|
331
|
+
if (this.isAnimating) this.stopAnimation();
|
332
|
+
if (this.momentumFrame) {
|
333
|
+
cancelAnimationFrame(this.momentumFrame);
|
334
|
+
this.momentumFrame = null;
|
335
|
+
this.velocity = {
|
336
|
+
x: 0,
|
337
|
+
y: 0
|
338
|
+
};
|
339
|
+
}
|
340
|
+
}
|
341
|
+
handleWheel(e) {
|
342
|
+
e.preventDefault();
|
343
|
+
e.stopPropagation();
|
344
|
+
if (this.noInteraction) return;
|
345
|
+
if (this.momentumFrame) {
|
346
|
+
cancelAnimationFrame(this.momentumFrame);
|
347
|
+
this.momentumFrame = null;
|
348
|
+
this.velocity = {
|
349
|
+
x: 0,
|
350
|
+
y: 0
|
351
|
+
};
|
352
|
+
}
|
353
|
+
if (this.focus) {
|
354
|
+
this.interruptAnimation();
|
355
|
+
this.focus = null;
|
356
|
+
}
|
357
|
+
const canvasCoords = this.toCanvasCoords(e.clientX, e.clientY);
|
358
|
+
const scaleFactor = e.deltaY > 0 ? .95 : 1.05;
|
359
|
+
const newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.targetTransform.scale * scaleFactor));
|
360
|
+
const { x: currentX, y: currentY, scale: currentScale } = this.transform;
|
361
|
+
const worldX = (canvasCoords.x - currentX - this.offset.x * currentScale) / currentScale;
|
362
|
+
const worldY = (canvasCoords.y - currentY - this.offset.y * currentScale) / currentScale;
|
363
|
+
const newX = canvasCoords.x - worldX * newScale - this.offset.x * newScale;
|
364
|
+
const newY = canvasCoords.y - worldY * newScale - this.offset.y * newScale;
|
365
|
+
this.targetTransform = {
|
366
|
+
x: newX,
|
367
|
+
y: newY,
|
368
|
+
scale: newScale
|
369
|
+
};
|
370
|
+
this.startAnimation();
|
371
|
+
}
|
372
|
+
handleMouseDown(e) {
|
373
|
+
if (this.noInteraction) return;
|
374
|
+
this.interruptAnimation();
|
375
|
+
this.isDragging = true;
|
376
|
+
this.hasMoved = false;
|
377
|
+
this.dragStart = {
|
378
|
+
x: e.clientX,
|
379
|
+
y: e.clientY
|
380
|
+
};
|
381
|
+
this.lastPointerPos = {
|
382
|
+
x: e.clientX,
|
383
|
+
y: e.clientY
|
384
|
+
};
|
385
|
+
}
|
386
|
+
handleMouseMove(e) {
|
387
|
+
if (this.noInteraction) return;
|
388
|
+
this.lastMovePos = {
|
389
|
+
x: e.clientX,
|
390
|
+
y: e.clientY
|
391
|
+
};
|
392
|
+
const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
|
393
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
394
|
+
if (!this.isDragging || !this.dragStart) return;
|
395
|
+
const dx = e.clientX - this.dragStart.x;
|
396
|
+
const dy = e.clientY - this.dragStart.y;
|
397
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
398
|
+
this.hasMoved = true;
|
399
|
+
const deltaX = (e.clientX - this.lastPointerPos.x) * this.dpr;
|
400
|
+
const deltaY = (e.clientY - this.lastPointerPos.y) * this.dpr;
|
401
|
+
this.targetTransform.x += deltaX;
|
402
|
+
this.targetTransform.y += deltaY;
|
403
|
+
const now = Date.now();
|
404
|
+
const dt = now - this.lastDragTime;
|
405
|
+
if (dt > 0 && dt < 100) {
|
406
|
+
this.velocity.x = deltaX / dt * 16;
|
407
|
+
this.velocity.y = deltaY / dt * 16;
|
408
|
+
}
|
409
|
+
this.lastPointerPos = {
|
410
|
+
x: e.clientX,
|
411
|
+
y: e.clientY
|
412
|
+
};
|
413
|
+
this.lastDragTime = now;
|
414
|
+
this.startAnimation();
|
415
|
+
}
|
416
|
+
}
|
417
|
+
handleMouseUp(e) {
|
418
|
+
if (this.isDragging && this.hasMoved) this.startMomentum();
|
419
|
+
this.isDragging = false;
|
420
|
+
this.dragStart = null;
|
421
|
+
}
|
422
|
+
handleCanvasClick(e) {
|
423
|
+
if (this.noInteraction || this.hasMoved) return;
|
424
|
+
const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
|
425
|
+
this.onClick?.(cssCoords.x, cssCoords.y);
|
426
|
+
}
|
427
|
+
handleTouchStart(e) {
|
428
|
+
if (this.noInteraction) return;
|
429
|
+
e.preventDefault();
|
430
|
+
this.interruptAnimation();
|
431
|
+
if (e.touches.length === 1) {
|
432
|
+
const touch = e.touches[0];
|
433
|
+
this.isDragging = true;
|
434
|
+
this.hasMoved = false;
|
435
|
+
this.touchStart = {
|
436
|
+
x: touch.clientX,
|
437
|
+
y: touch.clientY
|
438
|
+
};
|
439
|
+
this.lastPointerPos = {
|
440
|
+
x: touch.clientX,
|
441
|
+
y: touch.clientY
|
442
|
+
};
|
443
|
+
this.lastMovePos = {
|
444
|
+
x: touch.clientX,
|
445
|
+
y: touch.clientY
|
446
|
+
};
|
447
|
+
} else if (e.touches.length === 2) {
|
448
|
+
this.isDragging = false;
|
449
|
+
const [t1, t2] = Array.from(e.touches);
|
450
|
+
this.pinchStartDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
451
|
+
this.pinchStartScale = this.targetTransform.scale;
|
452
|
+
}
|
453
|
+
}
|
454
|
+
handleTouchMove(e) {
|
455
|
+
if (this.noInteraction) return;
|
456
|
+
e.preventDefault();
|
457
|
+
if (e.touches.length === 1 && this.isDragging && this.touchStart) {
|
458
|
+
const touch = e.touches[0];
|
459
|
+
const dx = touch.clientX - this.touchStart.x;
|
460
|
+
const dy = touch.clientY - this.touchStart.y;
|
461
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
462
|
+
this.hasMoved = true;
|
463
|
+
const deltaX = touch.clientX - this.lastPointerPos.x;
|
464
|
+
const deltaY = touch.clientY - this.lastPointerPos.y;
|
465
|
+
this.targetTransform.x += deltaX * this.dpr;
|
466
|
+
this.targetTransform.y += deltaY * this.dpr;
|
467
|
+
const now = Date.now();
|
468
|
+
const dt = now - this.lastDragTime;
|
469
|
+
if (dt > 0 && dt < 100) {
|
470
|
+
this.velocity.x = deltaX / dt * 16;
|
471
|
+
this.velocity.y = deltaY / dt * 16;
|
472
|
+
}
|
473
|
+
this.lastPointerPos = {
|
474
|
+
x: touch.clientX,
|
475
|
+
y: touch.clientY
|
476
|
+
};
|
477
|
+
this.lastMovePos = {
|
478
|
+
x: touch.clientX,
|
479
|
+
y: touch.clientY
|
480
|
+
};
|
481
|
+
this.lastDragTime = now;
|
482
|
+
this.startAnimation();
|
483
|
+
const cssCoords = this.toCSSCoords(touch.clientX, touch.clientY);
|
484
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
485
|
+
}
|
486
|
+
} else if (e.touches.length === 2 && this.pinchStartDist != null) {
|
487
|
+
if (this.momentumFrame) {
|
488
|
+
cancelAnimationFrame(this.momentumFrame);
|
489
|
+
this.momentumFrame = null;
|
490
|
+
this.velocity = {
|
491
|
+
x: 0,
|
492
|
+
y: 0
|
493
|
+
};
|
494
|
+
}
|
495
|
+
const [t1, t2] = Array.from(e.touches);
|
496
|
+
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
497
|
+
let newScale = dist / this.pinchStartDist * this.pinchStartScale;
|
498
|
+
newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newScale));
|
499
|
+
const centerX = (t1.clientX + t2.clientX) / 2;
|
500
|
+
const centerY = (t1.clientY + t2.clientY) / 2;
|
501
|
+
const canvasCenter = this.toCanvasCoords(centerX, centerY);
|
502
|
+
const { x: currentX, y: currentY, scale: currentScale } = this.transform;
|
503
|
+
const worldX = (canvasCenter.x - currentX - this.offset.x * currentScale) / currentScale;
|
504
|
+
const worldY = (canvasCenter.y - currentY - this.offset.y * currentScale) / currentScale;
|
505
|
+
const newX = canvasCenter.x - worldX * newScale - this.offset.x * newScale;
|
506
|
+
const newY = canvasCenter.y - worldY * newScale - this.offset.y * newScale;
|
507
|
+
this.targetTransform = {
|
508
|
+
x: newX,
|
509
|
+
y: newY,
|
510
|
+
scale: newScale
|
511
|
+
};
|
512
|
+
this.startAnimation();
|
513
|
+
}
|
514
|
+
}
|
515
|
+
handleTouchEnd(e) {
|
516
|
+
if (this.noInteraction) return;
|
517
|
+
if (e.touches.length === 0) {
|
518
|
+
if (this.isDragging && this.hasMoved) this.startMomentum();
|
519
|
+
else if (this.isDragging && !this.hasMoved && this.onClick && this.touchStart) {
|
520
|
+
const cssCoords = this.toCSSCoords(this.touchStart.x, this.touchStart.y);
|
521
|
+
this.onClick(cssCoords.x, cssCoords.y);
|
522
|
+
}
|
523
|
+
this.isDragging = false;
|
524
|
+
this.touchStart = null;
|
525
|
+
this.hasMoved = false;
|
526
|
+
this.pinchStartDist = null;
|
527
|
+
} else if (e.touches.length === 1) {
|
528
|
+
const touch = e.touches[0];
|
529
|
+
this.isDragging = true;
|
530
|
+
this.hasMoved = false;
|
531
|
+
this.touchStart = {
|
532
|
+
x: touch.clientX,
|
533
|
+
y: touch.clientY
|
534
|
+
};
|
535
|
+
this.lastPointerPos = {
|
536
|
+
x: touch.clientX,
|
537
|
+
y: touch.clientY
|
538
|
+
};
|
539
|
+
this.pinchStartDist = null;
|
540
|
+
this.velocity = {
|
541
|
+
x: 0,
|
542
|
+
y: 0
|
543
|
+
};
|
544
|
+
this.lastDragTime = Date.now();
|
545
|
+
}
|
546
|
+
}
|
547
|
+
resetZoom() {
|
548
|
+
this.targetTransform = {
|
549
|
+
x: 0,
|
550
|
+
y: 0,
|
551
|
+
scale: 1
|
552
|
+
};
|
553
|
+
this.startAnimation();
|
554
|
+
}
|
555
|
+
transformTo(update) {
|
556
|
+
this.targetTransform = {
|
557
|
+
...this.targetTransform,
|
558
|
+
...update
|
559
|
+
};
|
560
|
+
this.startAnimation();
|
561
|
+
}
|
562
|
+
getTransformationFromWorld(worldX, worldY, newScale) {
|
563
|
+
const scale = newScale ?? this.transform.scale;
|
564
|
+
const x = this.canvas.width / 2 + this.offset.x * this.dpr - worldX * scale - (this.canvas.width / 2 + this.offset.x * this.dpr) * scale;
|
565
|
+
const y = this.canvas.height / 2 + this.offset.y * this.dpr - worldY * scale - (this.canvas.height / 2 + this.offset.y * this.dpr) * scale;
|
566
|
+
return {
|
567
|
+
x,
|
568
|
+
y,
|
569
|
+
scale
|
570
|
+
};
|
571
|
+
}
|
572
|
+
transformToWorld(worldX, worldY, newScale) {
|
573
|
+
const transform = this.getTransformationFromWorld(worldX, worldY, newScale);
|
574
|
+
this.transformTo(transform);
|
575
|
+
}
|
576
|
+
trackCursor() {
|
577
|
+
const cssCoords = this.toCSSCoords(this.lastMovePos.x, this.lastMovePos.y);
|
578
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
579
|
+
}
|
580
|
+
setNoInteraction(noInteraction) {
|
581
|
+
this.noInteraction = noInteraction;
|
582
|
+
if (noInteraction) {
|
583
|
+
this.isDragging = false;
|
584
|
+
this.dragStart = null;
|
585
|
+
}
|
586
|
+
}
|
587
|
+
focusOn(focus) {
|
588
|
+
this.focus = focus;
|
589
|
+
if (focus) {
|
590
|
+
const _focus = focus;
|
591
|
+
this.focus = () => {
|
592
|
+
const worldFocus = _focus();
|
593
|
+
if (!worldFocus) return null;
|
594
|
+
const transform = this.getTransformationFromWorld(worldFocus.x, worldFocus.y, worldFocus.scale);
|
595
|
+
return transform;
|
596
|
+
};
|
597
|
+
this.startAnimation();
|
598
|
+
}
|
599
|
+
}
|
600
|
+
resetFocus() {
|
601
|
+
this.focus = null;
|
602
|
+
}
|
603
|
+
destroy() {
|
604
|
+
this.stopAnimation();
|
605
|
+
if (this.momentumFrame) {
|
606
|
+
cancelAnimationFrame(this.momentumFrame);
|
607
|
+
this.momentumFrame = null;
|
608
|
+
}
|
609
|
+
this.canvas.removeEventListener("wheel", this.handleWheel);
|
610
|
+
this.canvas.removeEventListener("mousedown", this.handleMouseDown);
|
611
|
+
this.canvas.removeEventListener("click", this.handleCanvasClick);
|
612
|
+
this.canvas.removeEventListener("touchstart", this.handleTouchStart);
|
613
|
+
this.canvas.removeEventListener("touchmove", this.handleTouchMove);
|
614
|
+
this.canvas.removeEventListener("touchend", this.handleTouchEnd);
|
615
|
+
this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
|
616
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
617
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
618
|
+
if (this.resizeObserver) {
|
619
|
+
this.resizeObserver.disconnect();
|
620
|
+
this.resizeObserver = null;
|
621
|
+
}
|
622
|
+
if (this.mediaQueryList) {
|
623
|
+
const updateDPRFromMediaQuery = () => {
|
624
|
+
this.updateDPR();
|
625
|
+
};
|
626
|
+
if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
|
627
|
+
this.mediaQueryList = null;
|
628
|
+
}
|
629
|
+
}
|
630
|
+
getTransform() {
|
631
|
+
return { ...this.transform };
|
632
|
+
}
|
633
|
+
getTargetTransform() {
|
634
|
+
return { ...this.targetTransform };
|
635
|
+
}
|
636
|
+
getFocus() {
|
637
|
+
return this.focus ? { ...this.focus } : null;
|
638
|
+
}
|
639
|
+
setOffset(offset) {
|
640
|
+
this.offset = offset;
|
641
|
+
}
|
642
|
+
};
|
643
|
+
|
644
|
+
//#endregion
|
645
|
+
//#region src/util/canvas.ts
|
646
|
+
/**
|
647
|
+
* draws a circle on the canvas
|
648
|
+
* @param ctx - The canvas rendering context
|
649
|
+
* @param x - The x-coordinate of the circle's center
|
650
|
+
* @param y - The y-coordinate of the circle's center
|
651
|
+
* @param radius - The radius of the circle (default is 5)
|
652
|
+
* @param options - Optional parameters for styling the circle
|
653
|
+
* @param options.fill - Whether to fill the circle (default is true)
|
654
|
+
* @param options.fillStyle - The fill color of the circle
|
655
|
+
* @param options.stroke - Whether to stroke the circle (default is false)
|
656
|
+
* @param options.strokeStyle - The stroke color of the circle
|
657
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
658
|
+
* @returns void
|
659
|
+
*/
|
660
|
+
function circle(ctx, x, y, radius = 5, options) {
|
661
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2 } = options || {};
|
662
|
+
ctx.save();
|
663
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
664
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
665
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
666
|
+
ctx.beginPath();
|
667
|
+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
668
|
+
ctx.closePath();
|
669
|
+
if (fill) ctx.fill();
|
670
|
+
if (stroke) ctx.stroke();
|
671
|
+
ctx.restore();
|
672
|
+
}
|
673
|
+
/**
|
674
|
+
* draws a rectangle on the canvas
|
675
|
+
* @param ctx - The canvas rendering context
|
676
|
+
* @param x - The x-coordinate of the rectangle's top-left corner
|
677
|
+
* @param y - The y-coordinate of the rectangle's top-left corner
|
678
|
+
* @param width - The width of the rectangle
|
679
|
+
* @param height - The height of the rectangle
|
680
|
+
* @param options - Optional parameters for styling the rectangle
|
681
|
+
* @param options.fill - Whether to fill the rectangle (default is true)
|
682
|
+
* @param options.fillStyle - The fill color of the rectangle
|
683
|
+
* @param options.stroke - Whether to stroke the rectangle (default is false)
|
684
|
+
* @param options.strokeStyle - The stroke color of the rectangle
|
685
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
686
|
+
* @param options.borderRadius - The radius of the corners (default is 0)
|
687
|
+
* @returns void
|
688
|
+
*/
|
689
|
+
function rect(ctx, x, y, width, height, options) {
|
690
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, borderRadius = 0 } = options || {};
|
691
|
+
ctx.save();
|
692
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
693
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
694
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
695
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
696
|
+
ctx.beginPath();
|
697
|
+
if (r > 0) {
|
698
|
+
ctx.moveTo(x + r, y);
|
699
|
+
ctx.lineTo(x + width - r, y);
|
700
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
701
|
+
ctx.lineTo(x + width, y + height - r);
|
702
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
703
|
+
ctx.lineTo(x + r, y + height);
|
704
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
705
|
+
ctx.lineTo(x, y + r);
|
706
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
707
|
+
} else ctx.rect(x, y, width, height);
|
708
|
+
ctx.closePath();
|
709
|
+
if (fill) ctx.fill();
|
710
|
+
if (stroke) ctx.stroke();
|
711
|
+
ctx.restore();
|
712
|
+
}
|
713
|
+
/**
|
714
|
+
* draws an image on the canvas with optional border radius and opacity
|
715
|
+
* @param ctx - The canvas rendering context
|
716
|
+
* @param image - The HTMLImageElement to draw
|
717
|
+
* @param x - The x-coordinate of the image's top-left corner
|
718
|
+
* @param y - The y-coordinate of the image's top-left corner
|
719
|
+
* @param width - The width of the image
|
720
|
+
* @param height - The height of the image
|
721
|
+
* @param borderRadius - The radius of the corners (default is 0)
|
722
|
+
* @param opacity - The opacity of the image (default is 1.0)
|
723
|
+
* @param bgColor - Optional background color to fill the clipped area
|
724
|
+
* @returns void
|
725
|
+
*/
|
726
|
+
function img(ctx, image, x, y, width, height, borderRadius = 0, opacity = 1, bgColor) {
|
727
|
+
ctx.save();
|
728
|
+
ctx.beginPath();
|
729
|
+
ctx.globalAlpha = 1;
|
730
|
+
if (borderRadius > 0) {
|
731
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
732
|
+
ctx.moveTo(x + r, y);
|
733
|
+
ctx.lineTo(x + width - r, y);
|
734
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
735
|
+
ctx.lineTo(x + width, y + height - r);
|
736
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
737
|
+
ctx.lineTo(x + r, y + height);
|
738
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
739
|
+
ctx.lineTo(x, y + r);
|
740
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
741
|
+
} else ctx.rect(x, y, width, height);
|
742
|
+
ctx.closePath();
|
743
|
+
if (bgColor && opacity < 1) {
|
744
|
+
ctx.save();
|
745
|
+
ctx.fillStyle = bgColor;
|
746
|
+
ctx.fill();
|
747
|
+
ctx.restore();
|
748
|
+
}
|
749
|
+
ctx.clip();
|
750
|
+
ctx.globalAlpha = opacity;
|
751
|
+
ctx.drawImage(image, x, y, width, height);
|
752
|
+
ctx.restore();
|
753
|
+
}
|
754
|
+
function hexagon(ctx, x, y, radius, options) {
|
755
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, rotation = 0, borderRadius = 0 } = options || {};
|
756
|
+
ctx.save();
|
757
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
758
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
759
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
760
|
+
const sides = 6;
|
761
|
+
const angleStep = Math.PI * 2 / sides;
|
762
|
+
ctx.beginPath();
|
763
|
+
const points = [];
|
764
|
+
for (let i = 0; i < sides; i++) {
|
765
|
+
const angle = rotation + i * angleStep;
|
766
|
+
points.push({
|
767
|
+
x: x + radius * Math.cos(angle),
|
768
|
+
y: y + radius * Math.sin(angle)
|
769
|
+
});
|
770
|
+
}
|
771
|
+
if (borderRadius > 0) {
|
772
|
+
const maxBorderRadius = Math.min(borderRadius, radius / 3);
|
773
|
+
for (let i = 0; i < sides; i++) {
|
774
|
+
const current = points[i];
|
775
|
+
const next = points[(i + 1) % sides];
|
776
|
+
const prev = points[(i - 1 + sides) % sides];
|
777
|
+
const toPrev = {
|
778
|
+
x: prev.x - current.x,
|
779
|
+
y: prev.y - current.y
|
780
|
+
};
|
781
|
+
const toNext = {
|
782
|
+
x: next.x - current.x,
|
783
|
+
y: next.y - current.y
|
784
|
+
};
|
785
|
+
const lenPrev = Math.sqrt(toPrev.x * toPrev.x + toPrev.y * toPrev.y);
|
786
|
+
const lenNext = Math.sqrt(toNext.x * toNext.x + toNext.y * toNext.y);
|
787
|
+
const normPrev = {
|
788
|
+
x: toPrev.x / lenPrev,
|
789
|
+
y: toPrev.y / lenPrev
|
790
|
+
};
|
791
|
+
const normNext = {
|
792
|
+
x: toNext.x / lenNext,
|
793
|
+
y: toNext.y / lenNext
|
794
|
+
};
|
795
|
+
const cpPrev = {
|
796
|
+
x: current.x + normPrev.x * maxBorderRadius,
|
797
|
+
y: current.y + normPrev.y * maxBorderRadius
|
798
|
+
};
|
799
|
+
const cpNext = {
|
800
|
+
x: current.x + normNext.x * maxBorderRadius,
|
801
|
+
y: current.y + normNext.y * maxBorderRadius
|
802
|
+
};
|
803
|
+
if (i === 0) ctx.moveTo(cpPrev.x, cpPrev.y);
|
804
|
+
else ctx.lineTo(cpPrev.x, cpPrev.y);
|
805
|
+
ctx.quadraticCurveTo(current.x, current.y, cpNext.x, cpNext.y);
|
806
|
+
}
|
807
|
+
} else {
|
808
|
+
ctx.moveTo(points[0].x, points[0].y);
|
809
|
+
for (let i = 1; i < sides; i++) ctx.lineTo(points[i].x, points[i].y);
|
810
|
+
}
|
811
|
+
ctx.closePath();
|
812
|
+
if (fill) ctx.fill();
|
813
|
+
if (stroke) ctx.stroke();
|
814
|
+
ctx.restore();
|
815
|
+
}
|
816
|
+
|
817
|
+
//#endregion
|
818
|
+
//#region src/util/color.ts
|
819
|
+
/**
|
820
|
+
* Some utility functions to handle colors
|
821
|
+
*/
|
822
|
+
function color(rgb) {
|
823
|
+
const colorHandler = function(arg) {
|
824
|
+
if (typeof arg === "number") return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${arg})`;
|
825
|
+
if (typeof arg === "function") {
|
826
|
+
const transformedRGB = arg(rgb);
|
827
|
+
return color(transformedRGB);
|
828
|
+
}
|
829
|
+
return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
830
|
+
};
|
831
|
+
colorHandler.rgb = rgb;
|
832
|
+
return colorHandler;
|
833
|
+
}
|
834
|
+
/**
|
835
|
+
* Dims a color to white or black
|
836
|
+
* @param color An array of 3 numbers representing RGB values (0-255)
|
837
|
+
* @param dimFactor A value between 0 and 1 where 0 is completely black and 1 is the original color
|
838
|
+
* @returns A new array with the dimmed RGB values
|
839
|
+
*/
|
840
|
+
function dim(factor = .6, white = true) {
|
841
|
+
const base = white ? 255 : 0;
|
842
|
+
return function(rgb) {
|
843
|
+
return [
|
844
|
+
Math.round(rgb[0] + (base - rgb[0]) * (1 - factor)),
|
845
|
+
Math.round(rgb[1] + (base - rgb[1]) * (1 - factor)),
|
846
|
+
Math.round(rgb[2] + (base - rgb[2]) * (1 - factor))
|
847
|
+
];
|
848
|
+
};
|
849
|
+
}
|
850
|
+
|
851
|
+
//#endregion
|
852
|
+
//#region src/util/data.ts
|
853
|
+
const images = [];
|
854
|
+
function generateTree(maxNodes, maxChildren) {
|
855
|
+
const nodes = [];
|
856
|
+
const links = [];
|
857
|
+
let index$1 = 0;
|
858
|
+
function createNode(label) {
|
859
|
+
const isRoot = label === VOID_ROOT_ID;
|
860
|
+
const idx = isRoot ? VOID_ROOT_ID : index$1++;
|
861
|
+
return {
|
862
|
+
id: idx.toString(),
|
863
|
+
label,
|
864
|
+
imgSrc: isRoot ? void 0 : images[index$1 % images.length]
|
865
|
+
};
|
866
|
+
}
|
867
|
+
const root = createNode(VOID_ROOT_ID);
|
868
|
+
nodes.push(root);
|
869
|
+
const queue = [root];
|
870
|
+
while (queue.length > 0 && nodes.length < maxNodes) {
|
871
|
+
const parent = queue.shift();
|
872
|
+
const rand = Math.random();
|
873
|
+
const biased = Math.floor(Math.pow(rand, 2) * (maxChildren + 1));
|
874
|
+
const childrenCount = Math.min(biased, maxChildren);
|
875
|
+
for (let i = 0; i < childrenCount; i++) {
|
876
|
+
if (nodes.length >= maxNodes) break;
|
877
|
+
const child = createNode(`Node ${nodes.length}`);
|
878
|
+
nodes.push(child);
|
879
|
+
links.push({
|
880
|
+
source: parent.id,
|
881
|
+
target: child.id
|
882
|
+
});
|
883
|
+
queue.push(child);
|
884
|
+
}
|
885
|
+
}
|
886
|
+
return {
|
887
|
+
nodes,
|
888
|
+
links
|
889
|
+
};
|
890
|
+
}
|
891
|
+
function getPrunedData(startId, nodes, links, highlights = []) {
|
892
|
+
const nodesById = Object.fromEntries(nodes.map((node) => [node.id, node]));
|
893
|
+
const visibleNodes = [];
|
894
|
+
const visibleLinks = [];
|
895
|
+
const visited = new Set();
|
896
|
+
(function traverseTree(node = nodesById[startId]) {
|
897
|
+
if (!node || visited.has(node.id)) return;
|
898
|
+
visited.add(node.id);
|
899
|
+
if (node.status === "LIQUIDATED" && !highlights.find((h) => h.id === node.id)) return;
|
900
|
+
visibleNodes.push(node);
|
901
|
+
if (node?.state?.collapsed) return;
|
902
|
+
const childLinks = links.filter((l) => (isSimNode(l.source) ? l.source.id : l.source) === node.id);
|
903
|
+
for (const link of childLinks) {
|
904
|
+
const targetNode = isSimNode(link.target) ? link.target : nodesById[link.target.toString()];
|
905
|
+
if (targetNode?.status === "LIQUIDATED" && !highlights.find((h) => h.id === targetNode.id)) continue;
|
906
|
+
visibleLinks.push(link);
|
907
|
+
traverseTree(targetNode);
|
908
|
+
}
|
909
|
+
})();
|
910
|
+
return {
|
911
|
+
nodes: visibleNodes,
|
912
|
+
links: visibleLinks
|
913
|
+
};
|
914
|
+
}
|
915
|
+
/**
|
916
|
+
* Automatically identifies root nodes and builds a nested structure
|
917
|
+
* @param nodes Array of raw nodes
|
918
|
+
* @param links Array of links between nodes
|
919
|
+
* @returns Array of nested nodes starting from identified roots
|
920
|
+
*/
|
921
|
+
function buildTreeFromGraphData(nodes, links) {
|
922
|
+
const nodeMap = new Map();
|
923
|
+
nodes.forEach((node) => nodeMap.set(node.id, node));
|
924
|
+
const childrenMap = new Map();
|
925
|
+
const parentMap = new Map();
|
926
|
+
nodes.forEach((node) => {
|
927
|
+
childrenMap.set(node.id, []);
|
928
|
+
parentMap.set(node.id, []);
|
929
|
+
});
|
930
|
+
links.forEach((link) => {
|
931
|
+
if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
|
932
|
+
const children = childrenMap.get(link.source) || [];
|
933
|
+
if (!children.includes(link.target)) {
|
934
|
+
children.push(link.target);
|
935
|
+
childrenMap.set(link.source, children);
|
936
|
+
}
|
937
|
+
const parents = parentMap.get(link.target) || [];
|
938
|
+
if (!parents.includes(link.source)) {
|
939
|
+
parents.push(link.source);
|
940
|
+
parentMap.set(link.target, parents);
|
941
|
+
}
|
942
|
+
}
|
943
|
+
});
|
944
|
+
const rootHashes = [];
|
945
|
+
nodeMap.forEach((_, hash) => {
|
946
|
+
const parents = parentMap.get(hash) || [];
|
947
|
+
if (parents.length === 0) rootHashes.push(hash);
|
948
|
+
});
|
949
|
+
const buildNode = (hash, visited = new Set()) => {
|
950
|
+
if (visited.has(hash)) return null;
|
951
|
+
visited.add(hash);
|
952
|
+
const node = nodeMap.get(hash);
|
953
|
+
if (!node) return null;
|
954
|
+
const childHashes = childrenMap.get(hash) || [];
|
955
|
+
const nestedChildren = [];
|
956
|
+
childHashes.forEach((childHash) => {
|
957
|
+
const childNode = buildNode(childHash, new Set([...visited]));
|
958
|
+
if (childNode) nestedChildren.push(childNode);
|
959
|
+
});
|
960
|
+
return {
|
961
|
+
...node,
|
962
|
+
children: nestedChildren
|
963
|
+
};
|
964
|
+
};
|
965
|
+
const result = [];
|
966
|
+
rootHashes.forEach((rootHash) => {
|
967
|
+
const nestedRoot = buildNode(rootHash);
|
968
|
+
if (nestedRoot) result.push(nestedRoot);
|
969
|
+
});
|
970
|
+
const processedNodes = new Set();
|
971
|
+
const markProcessed = (node) => {
|
972
|
+
processedNodes.add(node.id);
|
973
|
+
node.children.forEach(markProcessed);
|
974
|
+
};
|
975
|
+
result.forEach(markProcessed);
|
976
|
+
nodeMap.forEach((_, hash) => {
|
977
|
+
if (!processedNodes.has(hash)) {
|
978
|
+
const orphanedRoot = buildNode(hash);
|
979
|
+
if (orphanedRoot) result.push(orphanedRoot);
|
980
|
+
}
|
981
|
+
});
|
982
|
+
return result;
|
983
|
+
}
|
984
|
+
/**
|
985
|
+
* Recursively retrieves all parents of a node from a graph data structure
|
986
|
+
* @param {string} nodeHash - The hash of the node to find parents for
|
987
|
+
* @param {RawNode[]} nodes - Array of nodes in the graph
|
988
|
+
* @param {RawLink[]} links - Array of links connecting the nodes
|
989
|
+
* @returns {RawNode[]} - Array of parent nodes
|
990
|
+
*/
|
991
|
+
function searchParents(nodeHash, nodes, links) {
|
992
|
+
const visited = new Set();
|
993
|
+
function findParents(hash) {
|
994
|
+
if (visited.has(hash)) return [];
|
995
|
+
visited.add(hash);
|
996
|
+
const immediateParents = links.filter((link) => link.target === hash).map((link) => link.source);
|
997
|
+
const parentNodes = nodes.filter((node) => immediateParents.includes(node.id));
|
998
|
+
const ancestorNodes = immediateParents.flatMap((parentHash) => findParents(parentHash));
|
999
|
+
return [...parentNodes, ...ancestorNodes];
|
1000
|
+
}
|
1001
|
+
return findParents(nodeHash);
|
1002
|
+
}
|
1003
|
+
|
1004
|
+
//#endregion
|
1005
|
+
//#region src/sim/_interfaces.ts
|
1006
|
+
var OpenGraphEventEmitter = class extends EventEmitter {};
|
1007
|
+
|
1008
|
+
//#endregion
|
1009
|
+
//#region src/util/math.ts
|
1010
|
+
function getRadialPoint(r, cx, cy, angle = Math.random() * 2 * Math.PI) {
|
1011
|
+
return {
|
1012
|
+
x: cx + r * Math.cos(angle),
|
1013
|
+
y: cy + r * Math.sin(angle)
|
1014
|
+
};
|
1015
|
+
}
|
1016
|
+
function getAngle(cx, cy, x, y) {
|
1017
|
+
return Math.atan2(y - cy, x - cx);
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
//#endregion
|
1021
|
+
//#region src/util/highlights.ts
|
1022
|
+
const red = [
|
1023
|
+
238,
|
1024
|
+
125,
|
1025
|
+
121
|
1026
|
+
];
|
1027
|
+
const redred = [
|
1028
|
+
255,
|
1029
|
+
0,
|
1030
|
+
0
|
1031
|
+
];
|
1032
|
+
var Highlight = class {
|
1033
|
+
static owner = (id) => {
|
1034
|
+
return {
|
1035
|
+
id,
|
1036
|
+
strokeColor: red
|
1037
|
+
};
|
1038
|
+
};
|
1039
|
+
static primary = (id) => {
|
1040
|
+
return {
|
1041
|
+
id,
|
1042
|
+
linkTo: id,
|
1043
|
+
scale: 4,
|
1044
|
+
strokeColor: redred,
|
1045
|
+
linkColor: redred,
|
1046
|
+
onTop: true,
|
1047
|
+
isDetached: true
|
1048
|
+
};
|
1049
|
+
};
|
1050
|
+
static evolved = (id) => {
|
1051
|
+
return {
|
1052
|
+
id,
|
1053
|
+
linkTo: id,
|
1054
|
+
scale: 2.1,
|
1055
|
+
strokeColor: redred,
|
1056
|
+
linkColor: redred,
|
1057
|
+
onTop: true,
|
1058
|
+
isDetached: true
|
1059
|
+
};
|
1060
|
+
};
|
1061
|
+
static minted = (id) => {
|
1062
|
+
return {
|
1063
|
+
id,
|
1064
|
+
linkTo: id,
|
1065
|
+
scale: 1.5,
|
1066
|
+
strokeColor: redred,
|
1067
|
+
linkColor: redred,
|
1068
|
+
isDetached: true,
|
1069
|
+
onTop: true
|
1070
|
+
};
|
1071
|
+
};
|
1072
|
+
};
|
1073
|
+
|
1074
|
+
//#endregion
|
1075
|
+
//#region src/sim/asymmetric-link.ts
|
1076
|
+
function constant(x) {
|
1077
|
+
return function() {
|
1078
|
+
return x;
|
1079
|
+
};
|
1080
|
+
}
|
1081
|
+
function jiggle(random) {
|
1082
|
+
return (random() - .5) * 1e-6;
|
1083
|
+
}
|
1084
|
+
function index(d) {
|
1085
|
+
return d.index;
|
1086
|
+
}
|
1087
|
+
function find(nodeById, nodeId) {
|
1088
|
+
const node = nodeById.get(nodeId);
|
1089
|
+
if (!node) throw new Error("node not found: " + nodeId);
|
1090
|
+
return node;
|
1091
|
+
}
|
1092
|
+
function asymmetricLinks(links) {
|
1093
|
+
let id = index;
|
1094
|
+
let strength = defaultStrength;
|
1095
|
+
let strengths;
|
1096
|
+
let distance = constant(30);
|
1097
|
+
let distances;
|
1098
|
+
let nodes;
|
1099
|
+
let count;
|
1100
|
+
let bias;
|
1101
|
+
let random;
|
1102
|
+
let iterations = 1;
|
1103
|
+
if (links == null) links = [];
|
1104
|
+
function defaultStrength(link) {
|
1105
|
+
return 1 / Math.min(count[link.source.index], count[link.target.index]);
|
1106
|
+
}
|
1107
|
+
function force(alpha) {
|
1108
|
+
for (let k = 0, n = links.length; k < iterations; ++k) for (let i = 0; i < n; ++i) {
|
1109
|
+
const link = links[i];
|
1110
|
+
const source = link.source;
|
1111
|
+
const target = link.target;
|
1112
|
+
let x = target.x + target.vx - source.x - source.vx || jiggle(random);
|
1113
|
+
let y = target.y + target.vy - source.y - source.vy || jiggle(random);
|
1114
|
+
let l = Math.sqrt(x * x + y * y);
|
1115
|
+
l = (l - distances[i]) / l * alpha;
|
1116
|
+
x *= l;
|
1117
|
+
y *= l;
|
1118
|
+
const b = bias[i];
|
1119
|
+
const strengthValue = strengths[i];
|
1120
|
+
let s0, s1;
|
1121
|
+
if (Array.isArray(strengthValue)) [s0, s1] = strengthValue;
|
1122
|
+
else s0 = s1 = strengthValue;
|
1123
|
+
target.vx -= x * b * s0;
|
1124
|
+
target.vy -= y * b * s0;
|
1125
|
+
source.vx += x * (1 - b) * s1;
|
1126
|
+
source.vy += y * (1 - b) * s1;
|
1127
|
+
}
|
1128
|
+
}
|
1129
|
+
function initialize() {
|
1130
|
+
if (!nodes) return;
|
1131
|
+
const n = nodes.length;
|
1132
|
+
const m = links.length;
|
1133
|
+
const nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d]));
|
1134
|
+
count = new Array(n).fill(0);
|
1135
|
+
for (let i = 0; i < m; ++i) {
|
1136
|
+
const link = links[i];
|
1137
|
+
link.index = i;
|
1138
|
+
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
|
1139
|
+
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
|
1140
|
+
count[link.source.index]++;
|
1141
|
+
count[link.target.index]++;
|
1142
|
+
}
|
1143
|
+
bias = new Array(m);
|
1144
|
+
for (let i = 0; i < m; ++i) {
|
1145
|
+
const link = links[i];
|
1146
|
+
const sourceCount = count[link.source.index];
|
1147
|
+
const targetCount = count[link.target.index];
|
1148
|
+
bias[i] = sourceCount / (sourceCount + targetCount);
|
1149
|
+
}
|
1150
|
+
strengths = new Array(m);
|
1151
|
+
initializeStrength();
|
1152
|
+
distances = new Array(m);
|
1153
|
+
initializeDistance();
|
1154
|
+
}
|
1155
|
+
function initializeStrength() {
|
1156
|
+
if (!nodes) return;
|
1157
|
+
for (let i = 0, n = links.length; i < n; ++i) strengths[i] = strength(links[i], i, links);
|
1158
|
+
}
|
1159
|
+
function initializeDistance() {
|
1160
|
+
if (!nodes) return;
|
1161
|
+
for (let i = 0, n = links.length; i < n; ++i) distances[i] = +distance(links[i], i, links);
|
1162
|
+
}
|
1163
|
+
force.initialize = function(_nodes, _random) {
|
1164
|
+
nodes = _nodes;
|
1165
|
+
random = _random;
|
1166
|
+
initialize();
|
1167
|
+
};
|
1168
|
+
force.links = function(_) {
|
1169
|
+
return arguments.length ? (links = _, initialize(), force) : links;
|
1170
|
+
};
|
1171
|
+
force.id = function(_) {
|
1172
|
+
return arguments.length ? (id = _, force) : id;
|
1173
|
+
};
|
1174
|
+
force.iterations = function(_) {
|
1175
|
+
return arguments.length ? (iterations = +_, force) : iterations;
|
1176
|
+
};
|
1177
|
+
force.strength = function(_) {
|
1178
|
+
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
|
1179
|
+
};
|
1180
|
+
force.distance = function(_) {
|
1181
|
+
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
|
1182
|
+
};
|
1183
|
+
return force;
|
1184
|
+
}
|
1185
|
+
|
1186
|
+
//#endregion
|
1187
|
+
//#region src/util/hash.ts
|
1188
|
+
function quickHash(data) {
|
1189
|
+
return float2hex(xorshiftString(data));
|
1190
|
+
}
|
1191
|
+
|
1192
|
+
//#endregion
|
1193
|
+
//#region src/sim/OpenGraphSimulation.ts
|
1194
|
+
const RADIAL_FORCES = false;
|
1195
|
+
const INITIAL_RADIUS = 300;
|
1196
|
+
const INCREMENTAL = 200;
|
1197
|
+
function getRadius(depth) {
|
1198
|
+
if (depth === 0) return INITIAL_RADIUS;
|
1199
|
+
return INCREMENTAL * depth + INITIAL_RADIUS;
|
1200
|
+
}
|
1201
|
+
var OpenGraphSimulation = class {
|
1202
|
+
width;
|
1203
|
+
height;
|
1204
|
+
config;
|
1205
|
+
rootImageSources;
|
1206
|
+
canvas;
|
1207
|
+
transformCanvas;
|
1208
|
+
theme;
|
1209
|
+
rawData;
|
1210
|
+
emitter;
|
1211
|
+
translate = {
|
1212
|
+
x: 0,
|
1213
|
+
y: 0
|
1214
|
+
};
|
1215
|
+
data = {
|
1216
|
+
nodes: [],
|
1217
|
+
links: []
|
1218
|
+
};
|
1219
|
+
prunedData = {
|
1220
|
+
nodes: [],
|
1221
|
+
links: []
|
1222
|
+
};
|
1223
|
+
subGraph = {
|
1224
|
+
nodes: [],
|
1225
|
+
links: []
|
1226
|
+
};
|
1227
|
+
rootId = "";
|
1228
|
+
simulation = null;
|
1229
|
+
clusterSizeRange = [0, 1];
|
1230
|
+
maxDepth = 0;
|
1231
|
+
isTicking = false;
|
1232
|
+
tickCount = 0;
|
1233
|
+
loadNodeImage;
|
1234
|
+
imageCache = new Map();
|
1235
|
+
rootImages = [];
|
1236
|
+
hideThumbnails = false;
|
1237
|
+
noInteraction = false;
|
1238
|
+
lockedNodeId;
|
1239
|
+
selectedNode = null;
|
1240
|
+
hoveredNode = null;
|
1241
|
+
highlights = [];
|
1242
|
+
renderLayers = {
|
1243
|
+
links: {
|
1244
|
+
regular: [],
|
1245
|
+
highlighted: []
|
1246
|
+
},
|
1247
|
+
nodes: {
|
1248
|
+
regular: [],
|
1249
|
+
highlighted: []
|
1250
|
+
}
|
1251
|
+
};
|
1252
|
+
constructor(props) {
|
1253
|
+
this.emitter = new OpenGraphEventEmitter();
|
1254
|
+
this.theme = props.theme || "light";
|
1255
|
+
this.width = props.width;
|
1256
|
+
this.height = props.height;
|
1257
|
+
this.config = props.config || DEFAULT_GRAPH_CONFIG;
|
1258
|
+
this.rootImageSources = props.rootImageSources || [];
|
1259
|
+
this.canvas = props.canvas;
|
1260
|
+
this.lockedNodeId = props.lockedNodeId;
|
1261
|
+
this.loadNodeImage = props.loadNodeImage;
|
1262
|
+
this.translate = props.translate || {
|
1263
|
+
x: 0,
|
1264
|
+
y: 0
|
1265
|
+
};
|
1266
|
+
this.transformCanvas = new TransformCanvas(this.canvas, {
|
1267
|
+
onUpdate: this.handleTransform,
|
1268
|
+
onClick: this.handleClick,
|
1269
|
+
onMove: this.handleMove
|
1270
|
+
});
|
1271
|
+
this.rootImageSources.forEach((src, idx) => {
|
1272
|
+
if (src && !this.imageCache.get(src)) loadHTMLImageElement(src).then((img$1) => {
|
1273
|
+
this.imageCache.set(src, img$1);
|
1274
|
+
this.rootImages[idx] = img$1;
|
1275
|
+
});
|
1276
|
+
});
|
1277
|
+
}
|
1278
|
+
get center() {
|
1279
|
+
return {
|
1280
|
+
x: 0,
|
1281
|
+
y: 0
|
1282
|
+
};
|
1283
|
+
}
|
1284
|
+
get origin() {
|
1285
|
+
const dpi = devicePixelRatio || 1;
|
1286
|
+
return {
|
1287
|
+
x: (this.translate.x + this.width / 2) * dpi,
|
1288
|
+
y: (this.translate.y + this.height / 2) * dpi
|
1289
|
+
};
|
1290
|
+
}
|
1291
|
+
getNodeAtPosition = (cx, cy) => {
|
1292
|
+
const transform = this.transformCanvas.getTransform();
|
1293
|
+
const { x: tx, y: ty, scale } = transform;
|
1294
|
+
const dpi = devicePixelRatio || 1;
|
1295
|
+
const canvasX = cx * dpi;
|
1296
|
+
const canvasY = cy * dpi;
|
1297
|
+
const scaledX = (canvasX - tx) / scale;
|
1298
|
+
const scaledY = (canvasY - ty) / scale;
|
1299
|
+
const graphX = scaledX - this.origin.x;
|
1300
|
+
const graphY = scaledY - this.origin.y;
|
1301
|
+
const candidates = [];
|
1302
|
+
for (let node of this.data.nodes) {
|
1303
|
+
const r = this.getNodeSize(node.id) / 2;
|
1304
|
+
if (node.x == null || node.y == null) continue;
|
1305
|
+
const dx = node.x - graphX;
|
1306
|
+
const dy = node.y - graphY;
|
1307
|
+
if (dx * dx + dy * dy < r * r) {
|
1308
|
+
if (!this.prunedData.nodes.find((n) => n.id === node.id)) continue;
|
1309
|
+
candidates.push(node);
|
1310
|
+
}
|
1311
|
+
}
|
1312
|
+
if (candidates.length === 0) return null;
|
1313
|
+
if (candidates.length === 1) return candidates[0];
|
1314
|
+
for (let i = this.renderLayers.nodes.highlighted.length - 1; i >= 0; i--) {
|
1315
|
+
const node = this.renderLayers.nodes.highlighted[i];
|
1316
|
+
if (candidates.find((c) => c.id === node.id)) return node;
|
1317
|
+
}
|
1318
|
+
for (let node of this.renderLayers.nodes.regular) if (candidates.find((c) => c.id === node.id)) return node;
|
1319
|
+
return candidates[0];
|
1320
|
+
};
|
1321
|
+
getNodeScreenPosition = (node) => {
|
1322
|
+
const transform = this.transformCanvas.getTransform();
|
1323
|
+
const x = transform.x + (node.x || 0 + this.origin.x) * transform.scale;
|
1324
|
+
const y = transform.y + (node.y || 0 + this.origin.y) * transform.scale;
|
1325
|
+
return {
|
1326
|
+
x,
|
1327
|
+
y
|
1328
|
+
};
|
1329
|
+
};
|
1330
|
+
getNodeCanvasPosition = (node) => {
|
1331
|
+
const _x = node.x || 0;
|
1332
|
+
const _y = node.y || 0;
|
1333
|
+
const transform = this.transformCanvas.getTransform();
|
1334
|
+
const x = this.origin.x - _x * transform.scale;
|
1335
|
+
const y = this.origin.y - _y * transform.scale;
|
1336
|
+
return {
|
1337
|
+
x,
|
1338
|
+
y
|
1339
|
+
};
|
1340
|
+
};
|
1341
|
+
screenToWorld(_x, _y) {
|
1342
|
+
const transform = this.transformCanvas.getTransform();
|
1343
|
+
const { x: tx, y: ty, scale } = transform;
|
1344
|
+
const dpi = devicePixelRatio || 1;
|
1345
|
+
const canvasX = _x * dpi;
|
1346
|
+
const canvasY = _y * dpi;
|
1347
|
+
const scaledX = (canvasX - tx) / scale;
|
1348
|
+
const scaledY = (canvasY - ty) / scale;
|
1349
|
+
const x = scaledX - this.origin.x;
|
1350
|
+
const y = scaledY - this.origin.y;
|
1351
|
+
return {
|
1352
|
+
x,
|
1353
|
+
y
|
1354
|
+
};
|
1355
|
+
}
|
1356
|
+
worldToScreen(worldX, worldY) {
|
1357
|
+
const transform = this.transformCanvas.getTransform();
|
1358
|
+
const { x: tx, y: ty, scale } = transform;
|
1359
|
+
const dpi = devicePixelRatio || 1;
|
1360
|
+
const scaledX = (worldX + this.origin.x) * scale;
|
1361
|
+
const scaledY = (worldY + this.origin.y) * scale;
|
1362
|
+
const canvasX = scaledX + tx;
|
1363
|
+
const canvasY = scaledY + ty;
|
1364
|
+
const screenX = canvasX / dpi;
|
1365
|
+
const screenY = canvasY / dpi;
|
1366
|
+
return {
|
1367
|
+
x: screenX,
|
1368
|
+
y: screenY
|
1369
|
+
};
|
1370
|
+
}
|
1371
|
+
handleClick = (x, y) => {
|
1372
|
+
let node = this.getNodeAtPosition(x, y);
|
1373
|
+
if (node?.state?.sessionNode) return;
|
1374
|
+
if (this.lockedNodeId && !node) node = this.getNodeById(this.lockedNodeId);
|
1375
|
+
this.handleClickNode(node);
|
1376
|
+
};
|
1377
|
+
handleClickNode = (node, options = {
|
1378
|
+
noToggle: false,
|
1379
|
+
triggerFocus: false
|
1380
|
+
}) => {
|
1381
|
+
let wasOpened = false;
|
1382
|
+
if (node) {
|
1383
|
+
if (node.id === this.rootId) {
|
1384
|
+
this.selectedNode = null;
|
1385
|
+
this.emitter.emit("selected-node-changed", null);
|
1386
|
+
this.subGraph = {
|
1387
|
+
nodes: [],
|
1388
|
+
links: []
|
1389
|
+
};
|
1390
|
+
this.updateRenderLayers();
|
1391
|
+
this.updateScene();
|
1392
|
+
return;
|
1393
|
+
}
|
1394
|
+
if (node.state) {
|
1395
|
+
const children = getChildren(node.id, this.data.links);
|
1396
|
+
if (children.length > 0 && !options?.noToggle) {
|
1397
|
+
if (this.selectedNode?.id !== node.id) {
|
1398
|
+
if (node.state.collapsed) wasOpened = true;
|
1399
|
+
node.state.collapsed = false;
|
1400
|
+
} else node.state.collapsed = !node.state.collapsed;
|
1401
|
+
if (!node.state.collapsed) {
|
1402
|
+
const clusterDistance = 100;
|
1403
|
+
const clusterRadius = 50;
|
1404
|
+
const parentX = node.x || this.center.x;
|
1405
|
+
const parentY = node.y || this.center.y;
|
1406
|
+
const dirX = parentX - this.center.x;
|
1407
|
+
const dirY = parentY - this.center.y;
|
1408
|
+
const length = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
|
1409
|
+
const normX = dirX / length;
|
1410
|
+
const normY = dirY / length;
|
1411
|
+
const clusterX = parentX + normX * clusterDistance;
|
1412
|
+
const clusterY = parentY + normY * clusterDistance;
|
1413
|
+
children.forEach((childId) => {
|
1414
|
+
const childNode = this.data.nodes.find((n) => n.id === childId);
|
1415
|
+
if (childNode && isSimNode(childNode)) {
|
1416
|
+
const angle = Math.random() * 2 * Math.PI;
|
1417
|
+
const radius = Math.random() * clusterRadius;
|
1418
|
+
childNode.x = clusterX + Math.cos(angle) * radius;
|
1419
|
+
childNode.y = clusterY + Math.sin(angle) * radius;
|
1420
|
+
}
|
1421
|
+
});
|
1422
|
+
}
|
1423
|
+
}
|
1424
|
+
}
|
1425
|
+
if (!node.state?.collapsed) {
|
1426
|
+
const parents = getParents(node.id, this.data.links);
|
1427
|
+
parents.forEach((parentId) => {
|
1428
|
+
const parentNode = this.data.nodes.find((n) => n.id === parentId);
|
1429
|
+
if (parentNode && isSimNode(parentNode) && parentNode.state) parentNode.state.collapsed = false;
|
1430
|
+
});
|
1431
|
+
}
|
1432
|
+
this.subGraph = getNodeSubgraph(node.id, this.data.nodes, this.data.links, this.rootId);
|
1433
|
+
}
|
1434
|
+
if (this.selectedNode?.id !== node?.id) {
|
1435
|
+
this.selectedNode = node;
|
1436
|
+
this.emitter.emit("selected-node-changed", node);
|
1437
|
+
this.updateRenderLayers();
|
1438
|
+
}
|
1439
|
+
if (node) {
|
1440
|
+
this.restart(wasOpened ? .05 : 0);
|
1441
|
+
if (wasOpened || options?.triggerFocus) this.transformCanvas.focusOn(() => {
|
1442
|
+
const t = this.transformCanvas.getTransform();
|
1443
|
+
const _node = this.getNodeById(node.id);
|
1444
|
+
return {
|
1445
|
+
x: _node?.x,
|
1446
|
+
y: _node?.y,
|
1447
|
+
scale: t.scale
|
1448
|
+
};
|
1449
|
+
});
|
1450
|
+
} else if (!node && this.selectedNode) {
|
1451
|
+
this.selectedNode = null;
|
1452
|
+
this.emitter.emit("selected-node-changed", null);
|
1453
|
+
this.subGraph = {
|
1454
|
+
nodes: [],
|
1455
|
+
links: []
|
1456
|
+
};
|
1457
|
+
this.updateRenderLayers();
|
1458
|
+
this.updateScene();
|
1459
|
+
}
|
1460
|
+
};
|
1461
|
+
updateScene = () => {
|
1462
|
+
if (this.isTicking) return;
|
1463
|
+
this.onDraw();
|
1464
|
+
};
|
1465
|
+
handleMove = (x, y) => {
|
1466
|
+
const world = this.screenToWorld(x, y);
|
1467
|
+
const node = this.simulation?.find(world.x, world.y, 10) || null;
|
1468
|
+
if (node?.state?.sessionNode) return;
|
1469
|
+
if (this.hoveredNode === node) return;
|
1470
|
+
this.hoveredNode = node;
|
1471
|
+
this.emitter.emit("hovered-node-changed", node);
|
1472
|
+
this.canvas.style.cursor = node ? "pointer" : "default";
|
1473
|
+
this.updateScene();
|
1474
|
+
};
|
1475
|
+
handleTransform = (t) => {
|
1476
|
+
this.emitter.emit("transform-changed", t);
|
1477
|
+
this.updateScene();
|
1478
|
+
};
|
1479
|
+
updateHighlights = () => {
|
1480
|
+
const detachedHighlights = this.highlights.filter((h) => h.isDetached);
|
1481
|
+
const validSessionIds = new Set();
|
1482
|
+
let focusSessionNode = null;
|
1483
|
+
detachedHighlights.forEach((h) => {
|
1484
|
+
const highlightedNode = this.data.nodes.find((n) => n.id === h.id);
|
1485
|
+
if (!highlightedNode) return;
|
1486
|
+
const existingLink = this.data.links.find((l) => {
|
1487
|
+
return isSimNode(l.target) ? l.target.id === h.id : l.target === h.id;
|
1488
|
+
});
|
1489
|
+
if (!existingLink) return;
|
1490
|
+
const id = isSimNode(existingLink.source) ? existingLink.source.id : existingLink.source.toString();
|
1491
|
+
const parentNode = this.getNodeById(id);
|
1492
|
+
if (!parentNode) return;
|
1493
|
+
const _sessionId = h.sessionId || parentNode.id;
|
1494
|
+
const sessionId = parentNode.state?.sessionNode ? parentNode.id : `${VOID_DETACH_ID}-${_sessionId}`;
|
1495
|
+
validSessionIds.add(sessionId);
|
1496
|
+
let sessionNode = this.data.nodes.find((n) => n.id === sessionId);
|
1497
|
+
if (!sessionNode) {
|
1498
|
+
const depth = (highlightedNode?.depth || 1) + 1;
|
1499
|
+
const angle = getAngle(this.center.x, this.center.y, parentNode?.x, parentNode?.y);
|
1500
|
+
const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y, angle);
|
1501
|
+
sessionNode = {
|
1502
|
+
id: sessionId,
|
1503
|
+
state: {
|
1504
|
+
collapsed: false,
|
1505
|
+
image: void 0,
|
1506
|
+
sessionNode: true
|
1507
|
+
},
|
1508
|
+
depth,
|
1509
|
+
clusterSize: 1,
|
1510
|
+
x: circlePos.x,
|
1511
|
+
y: circlePos.y
|
1512
|
+
};
|
1513
|
+
this.data.nodes.push(sessionNode);
|
1514
|
+
this.data.links.push({
|
1515
|
+
target: sessionNode,
|
1516
|
+
source: parentNode
|
1517
|
+
});
|
1518
|
+
}
|
1519
|
+
const existingLinkIndex = this.data.links.findIndex((l) => l === existingLink);
|
1520
|
+
this.data.links.splice(existingLinkIndex, 1);
|
1521
|
+
this.data.links.push({
|
1522
|
+
target: highlightedNode,
|
1523
|
+
source: sessionNode
|
1524
|
+
});
|
1525
|
+
const parents = getAllParentsUntil(highlightedNode.id, this.data.links, this.rootId);
|
1526
|
+
parents.forEach((parentId) => {
|
1527
|
+
const node = this.data.nodes.find((n) => n.id === parentId);
|
1528
|
+
if (node?.state) node.state.collapsed = false;
|
1529
|
+
});
|
1530
|
+
if (highlightedNode.state) highlightedNode.state.collapsed = false;
|
1531
|
+
if (!focusSessionNode) focusSessionNode = sessionNode;
|
1532
|
+
});
|
1533
|
+
const sessionNodesToRemove = this.data.nodes.filter((n) => n.state?.sessionNode && !validSessionIds.has(n.id));
|
1534
|
+
sessionNodesToRemove.forEach((sessionNode) => {
|
1535
|
+
const childLinks = this.data.links.filter((l) => isSimNode(l.source) && l.source.id === sessionNode.id);
|
1536
|
+
const incomingLink = this.data.links.find((l) => isSimNode(l.target) && l.target.id === sessionNode.id);
|
1537
|
+
const parentNode = incomingLink ? isSimNode(incomingLink.source) ? incomingLink.source : this.getNodeById(incomingLink.source.toString()) : null;
|
1538
|
+
this.data.links = this.data.links.filter((l) => !(isSimNode(l.source) && l.source.id === sessionNode.id) && !(isSimNode(l.target) && l.target.id === sessionNode.id));
|
1539
|
+
if (parentNode) childLinks.forEach((link) => {
|
1540
|
+
const targetNode = isSimNode(link.target) ? link.target : this.getNodeById(link.target.toString());
|
1541
|
+
if (targetNode) this.data.links.push({
|
1542
|
+
source: parentNode,
|
1543
|
+
target: targetNode
|
1544
|
+
});
|
1545
|
+
});
|
1546
|
+
const index$1 = this.data.nodes.findIndex((n) => n.id === sessionNode.id);
|
1547
|
+
if (index$1 !== -1) this.data.nodes.splice(index$1, 1);
|
1548
|
+
});
|
1549
|
+
if (this.selectedNode) this.subGraph = getNodeSubgraph(this.selectedNode.id, this.data.nodes, this.data.links, this.rootId);
|
1550
|
+
this.restart();
|
1551
|
+
if (focusSessionNode) this.transformCanvas.focusOn(() => {
|
1552
|
+
if (!focusSessionNode) return null;
|
1553
|
+
const t = this.transformCanvas.getTransform();
|
1554
|
+
const _node = this.getNodeById(focusSessionNode.id);
|
1555
|
+
if (!_node) return null;
|
1556
|
+
return {
|
1557
|
+
x: _node?.x,
|
1558
|
+
y: _node?.y,
|
1559
|
+
scale: t.scale
|
1560
|
+
};
|
1561
|
+
});
|
1562
|
+
};
|
1563
|
+
initialize = (data, rootId) => {
|
1564
|
+
this.rawData = data;
|
1565
|
+
this.rootId = rootId;
|
1566
|
+
const _links = data.links.map((l) => ({ ...l }));
|
1567
|
+
const _nodes = data.nodes.map((n) => {
|
1568
|
+
const existingData = this.data.nodes.find((x$1) => x$1.id === n.id);
|
1569
|
+
const parents = getParents(n.id, _links);
|
1570
|
+
const parentNode = this.data.nodes.find((p) => p.id === parents[0]);
|
1571
|
+
const clusterSize = getClusterSize(n.id, _links);
|
1572
|
+
const depth = getNodeDepth(n.id, _links);
|
1573
|
+
const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y);
|
1574
|
+
const x = depth > 0 ? null : existingData?.x || parentNode?.x || circlePos.x;
|
1575
|
+
const y = depth > 0 ? null : existingData?.y || parentNode?.y || circlePos.y;
|
1576
|
+
return {
|
1577
|
+
...n,
|
1578
|
+
state: {
|
1579
|
+
collapsed: hasOnlyLeafs(n.id, _links) && getChildren(n.id, _links).length > 1,
|
1580
|
+
...existingData?.state
|
1581
|
+
},
|
1582
|
+
clusterSize,
|
1583
|
+
depth,
|
1584
|
+
x,
|
1585
|
+
y
|
1586
|
+
};
|
1587
|
+
}).sort((a, b) => a.depth - b.depth);
|
1588
|
+
_nodes.forEach((n, i, arr) => {
|
1589
|
+
const existingData = _nodes.find((x$1) => x$1.id === n.id);
|
1590
|
+
const parents = getParents(n.id, _links);
|
1591
|
+
const parentNode = _nodes.find((p) => p.id === parents[0]);
|
1592
|
+
const depth = n.depth;
|
1593
|
+
const parentAngle = depth > 0 ? getAngle(this.center.x, this.center.y, parentNode?.x, parentNode?.y) : void 0;
|
1594
|
+
const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y, parentAngle);
|
1595
|
+
const x = depth > 0 ? existingData?.x || circlePos.x : existingData?.x || circlePos.x;
|
1596
|
+
const y = depth > 0 ? existingData?.y || circlePos.y : existingData?.y || circlePos.y;
|
1597
|
+
_nodes[i].x = x;
|
1598
|
+
_nodes[i].y = y;
|
1599
|
+
});
|
1600
|
+
if (!data.nodes.find((n) => n.id === this.rootId)) _nodes.push({
|
1601
|
+
id: this.rootId,
|
1602
|
+
state: {
|
1603
|
+
collapsed: false,
|
1604
|
+
image: void 0
|
1605
|
+
},
|
1606
|
+
depth: -1,
|
1607
|
+
clusterSize: 1,
|
1608
|
+
x: this.center.x,
|
1609
|
+
y: this.center.y
|
1610
|
+
});
|
1611
|
+
const targetIds = new Set(_links.map((link) => link.target));
|
1612
|
+
const rootNodes = _nodes.filter((node) => !targetIds.has(node.id));
|
1613
|
+
for (const node of rootNodes) _links.push({
|
1614
|
+
source: this.rootId,
|
1615
|
+
target: node.id
|
1616
|
+
});
|
1617
|
+
this.maxDepth = Math.max(..._nodes.map((n) => n.depth || 0));
|
1618
|
+
this.data = {
|
1619
|
+
nodes: _nodes,
|
1620
|
+
links: _links
|
1621
|
+
};
|
1622
|
+
this.loadNodeImages();
|
1623
|
+
this.updateHighlights();
|
1624
|
+
this.triggerSelected(true);
|
1625
|
+
};
|
1626
|
+
get lockedNode() {
|
1627
|
+
return this.lockedNodeId ? this.getNodeById(this.lockedNodeId) : null;
|
1628
|
+
}
|
1629
|
+
triggerSelected = (triggerFocus = false) => {
|
1630
|
+
if (this.selectedNode) this.handleClickNode(this.selectedNode, {
|
1631
|
+
noToggle: true,
|
1632
|
+
triggerFocus
|
1633
|
+
});
|
1634
|
+
else if (this.lockedNode) this.handleClickNode(this.lockedNode, {
|
1635
|
+
noToggle: true,
|
1636
|
+
triggerFocus
|
1637
|
+
});
|
1638
|
+
else this.setSelectedNode(null);
|
1639
|
+
};
|
1640
|
+
restart = (alpha = .1) => {
|
1641
|
+
this.tickCount = 0;
|
1642
|
+
this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links, this.highlights);
|
1643
|
+
this.updateRenderLayers();
|
1644
|
+
this.clusterSizeRange = this.prunedData.nodes.filter((n) => n.state?.collapsed).reduce((acc, node) => [Math.min(acc[0], node.clusterSize || 1), Math.max(acc[1], node.clusterSize || 1)], [Infinity, -Infinity]);
|
1645
|
+
if (this.simulation) {
|
1646
|
+
this.simulation.stop();
|
1647
|
+
this.simulation.on("tick", null);
|
1648
|
+
this.simulation.on("end", null);
|
1649
|
+
}
|
1650
|
+
this.simulation = forceSimulation(this.prunedData.nodes).alpha(this.simulation ? alpha : .5).force("collide", forceCollide((n) => this.getNodeSize(n.id) / 2)).force("link", asymmetricLinks(this.prunedData.links).id((d) => d.id).distance((l) => {
|
1651
|
+
const size = this.getNodeSize(isSimNode(l.target) ? l.target.id : l.target.toString());
|
1652
|
+
if (isSimNode(l.target)) {
|
1653
|
+
const state = l.target?.state;
|
1654
|
+
if (!state?.collapsed) return size;
|
1655
|
+
}
|
1656
|
+
return size * 3;
|
1657
|
+
}).strength((l) => {
|
1658
|
+
return [.66, .08];
|
1659
|
+
})).force("charge", forceManyBody().strength((node) => {
|
1660
|
+
return -150;
|
1661
|
+
})).force("center", forceCenter(this.center.x, this.center.y).strength(.1)).restart();
|
1662
|
+
if (RADIAL_FORCES) for (let i = 0; i < this.maxDepth; i++) {
|
1663
|
+
const depth = i;
|
1664
|
+
const r = getRadius(depth);
|
1665
|
+
const x = this.center.x;
|
1666
|
+
const y = this.center.y;
|
1667
|
+
console.log("Adding radial force for depth", depth, "with radius", r, x, y);
|
1668
|
+
this.simulation.force(`radial-${depth}`, forceRadial(r, x, y).strength((n) => {
|
1669
|
+
if (n.id === this.rootId) return 0;
|
1670
|
+
if (n.depth === 0) return 0;
|
1671
|
+
if (n.depth === depth) return .01;
|
1672
|
+
return 0;
|
1673
|
+
}));
|
1674
|
+
}
|
1675
|
+
this.simulation.on("tick", this.handleTick);
|
1676
|
+
this.simulation.on("end", this.onEnd);
|
1677
|
+
};
|
1678
|
+
get rootNode() {
|
1679
|
+
return this.data.nodes.find((n) => n.id === this.rootId) || null;
|
1680
|
+
}
|
1681
|
+
handleTick = () => {
|
1682
|
+
this.isTicking = true;
|
1683
|
+
this.onDraw();
|
1684
|
+
this.tickCount++;
|
1685
|
+
};
|
1686
|
+
setTranslate({ x, y }) {
|
1687
|
+
this.translate = {
|
1688
|
+
x,
|
1689
|
+
y
|
1690
|
+
};
|
1691
|
+
this.transformCanvas.setOffset(this.translate);
|
1692
|
+
}
|
1693
|
+
get visiblityScale() {
|
1694
|
+
return scaleLog().domain(this.clusterSizeRange).range([1.5, .9]).clamp(true);
|
1695
|
+
}
|
1696
|
+
get color() {
|
1697
|
+
return color(this.theme === "light" ? this.config.theme.dark : this.config.theme.light);
|
1698
|
+
}
|
1699
|
+
get colorContrast() {
|
1700
|
+
return color(this.theme === "light" ? this.config.theme.light : this.config.theme.dark);
|
1701
|
+
}
|
1702
|
+
getNodeSize = (nodeId) => {
|
1703
|
+
const { nodeSize } = this.config;
|
1704
|
+
const highlight = this.highlights.find((h) => h.id === nodeId);
|
1705
|
+
const sizeScale = highlight?.scale || 1;
|
1706
|
+
if (nodeId === this.rootId) return nodeSize * 2 * sizeScale;
|
1707
|
+
const node = this.data.nodes.find((n) => n.id === nodeId);
|
1708
|
+
if (node?.state?.sessionNode) return 5;
|
1709
|
+
const isCollapsed = !!node?.state?.collapsed;
|
1710
|
+
if (isCollapsed) {
|
1711
|
+
const scale = scaleLinear().domain(this.clusterSizeRange).range([nodeSize, nodeSize * 3]);
|
1712
|
+
return scale(node.clusterSize || 1);
|
1713
|
+
}
|
1714
|
+
const isSelected = this.selectedNode?.id === nodeId;
|
1715
|
+
const isLiquidated = node?.status === "LIQUIDATED" || node?.status === "REGENERATED";
|
1716
|
+
const _size = isLiquidated ? nodeSize * .2 : nodeSize;
|
1717
|
+
return isSelected ? _size * 4 : _size * sizeScale;
|
1718
|
+
};
|
1719
|
+
updateRenderLayers() {
|
1720
|
+
const isHighlighted = (id) => {
|
1721
|
+
const highlight = this.highlights.find((h) => h.id === id);
|
1722
|
+
return !this.selectedNode && highlight?.onTop || this.selectedNode?.id === id || this.subGraph.nodes.find((n) => n.id === id);
|
1723
|
+
};
|
1724
|
+
this.renderLayers.nodes.regular = [];
|
1725
|
+
this.renderLayers.nodes.highlighted = [];
|
1726
|
+
this.prunedData.nodes.forEach((node) => {
|
1727
|
+
if (isHighlighted(node.id)) this.renderLayers.nodes.highlighted.push(node);
|
1728
|
+
else this.renderLayers.nodes.regular.push(node);
|
1729
|
+
});
|
1730
|
+
this.renderLayers.links.regular = [];
|
1731
|
+
this.renderLayers.links.highlighted = [];
|
1732
|
+
const isLinkInSubgraph = (link) => {
|
1733
|
+
if (!this.selectedNode) return false;
|
1734
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
1735
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
1736
|
+
return this.subGraph.links.some((l) => {
|
1737
|
+
const lSourceId = getNodeId(l.source);
|
1738
|
+
const lTargetId = getNodeId(l.target);
|
1739
|
+
return lSourceId === sourceId && lTargetId === targetId || lSourceId === targetId && lTargetId === sourceId;
|
1740
|
+
});
|
1741
|
+
};
|
1742
|
+
const isLinkInHighlights = (link) => {
|
1743
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
1744
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
1745
|
+
return !this.selectedNode && !!this.highlights.find((h) => h.isDetached && (h.id === sourceId || h.id === targetId));
|
1746
|
+
};
|
1747
|
+
this.prunedData.links.forEach((link) => {
|
1748
|
+
const inSubgraph = isLinkInSubgraph(link);
|
1749
|
+
const inHighlights = isLinkInHighlights(link);
|
1750
|
+
if (inSubgraph || inHighlights) this.renderLayers.links.highlighted.push(link);
|
1751
|
+
else this.renderLayers.links.regular.push(link);
|
1752
|
+
});
|
1753
|
+
this.renderLayers.links.highlighted.sort((a, b) => {
|
1754
|
+
const inSubgraphA = isLinkInSubgraph(a);
|
1755
|
+
const inSubgraphB = isLinkInSubgraph(b);
|
1756
|
+
if (inSubgraphA || inSubgraphB) return 2;
|
1757
|
+
return -1;
|
1758
|
+
});
|
1759
|
+
this.renderLayers.nodes.highlighted.sort((a, b) => {
|
1760
|
+
const highlightA = this.highlights.find((h) => h.id === a.id);
|
1761
|
+
const highlightB = this.highlights.find((h) => h.id === b.id);
|
1762
|
+
if (this.subGraph.nodes.find((n) => n.id === a.id) || this.subGraph.nodes.find((n) => n.id === b.id)) return 2;
|
1763
|
+
if (a.id === this.selectedNode?.id || b.id === this.selectedNode?.id) return 2;
|
1764
|
+
if (highlightA?.onTop || highlightB?.onTop) return 1;
|
1765
|
+
return -1;
|
1766
|
+
});
|
1767
|
+
}
|
1768
|
+
renderLink(ctx, link, options) {
|
1769
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
1770
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
1771
|
+
let sourceNode = this.data.nodes.find((n) => n.id === sourceId);
|
1772
|
+
const isLight = this.theme === "light";
|
1773
|
+
const { dim: _dim, hasSelection, highlight } = options;
|
1774
|
+
let stroke = _dim ? this.color(dim(.09, isLight))() : hasSelection ? this.color(dim(.4, isLight))() : this.color(dim(.18, isLight))();
|
1775
|
+
ctx.globalAlpha = highlight ? 1 : .5;
|
1776
|
+
const sx = sourceNode && sourceNode.x || 0;
|
1777
|
+
const sy = sourceNode && sourceNode.y || 0;
|
1778
|
+
const tx = isSimNode(link.target) && link.target.x || 0;
|
1779
|
+
const ty = isSimNode(link.target) && link.target.y || 0;
|
1780
|
+
if (highlight?.linkColor) {
|
1781
|
+
const gradient = ctx.createLinearGradient(sx, sy, tx, ty);
|
1782
|
+
gradient.addColorStop(0, stroke);
|
1783
|
+
gradient.addColorStop(1, color(highlight.linkColor)());
|
1784
|
+
ctx.strokeStyle = gradient;
|
1785
|
+
} else ctx.strokeStyle = stroke;
|
1786
|
+
ctx.lineWidth = _dim ? .3 : .8;
|
1787
|
+
ctx.beginPath();
|
1788
|
+
ctx.moveTo(sx, sy);
|
1789
|
+
ctx.lineTo(tx, ty);
|
1790
|
+
ctx.stroke();
|
1791
|
+
ctx.closePath();
|
1792
|
+
}
|
1793
|
+
renderNode(ctx, node, options) {
|
1794
|
+
const x = node.x || 0;
|
1795
|
+
const y = node.y || 0;
|
1796
|
+
const isSelected = this.selectedNode?.id === node.id;
|
1797
|
+
const isHovered = this.hoveredNode?.id === node.id;
|
1798
|
+
const isCollapsed = !!node.state?.collapsed;
|
1799
|
+
const isLiquidated = node.status === "LIQUIDATED" || node.status === "REGENERATED";
|
1800
|
+
const isLight = this.theme === "light";
|
1801
|
+
const { dim: _dim, transform } = options;
|
1802
|
+
const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
|
1803
|
+
const stroke = this.colorContrast();
|
1804
|
+
const nodeSize = this.getNodeSize(node.id);
|
1805
|
+
const highlight = this.highlights.find((h) => h.id === node.id);
|
1806
|
+
const highlighted = !!highlight;
|
1807
|
+
let highlightedStroke = _dim ? color(highlight?.strokeColor || red)(dim(.4, isLight))() : color(highlight?.strokeColor || red)();
|
1808
|
+
if (node.id === this.rootId) this.renderRootNode(ctx, x, y, nodeSize, _dim, isLight);
|
1809
|
+
else if (node.state?.sessionNode) this.renderSessionNode(ctx, x, y, nodeSize);
|
1810
|
+
else if (isCollapsed) this.renderCollapsedNode(ctx, x, y, nodeSize, {
|
1811
|
+
fill,
|
1812
|
+
stroke,
|
1813
|
+
highlighted,
|
1814
|
+
highlightedStroke,
|
1815
|
+
isSelected,
|
1816
|
+
dim: _dim,
|
1817
|
+
transform,
|
1818
|
+
clusterSize: node.clusterSize || 1,
|
1819
|
+
isLight
|
1820
|
+
});
|
1821
|
+
else this.renderExpandedNode(ctx, x, y, nodeSize, {
|
1822
|
+
fill,
|
1823
|
+
stroke,
|
1824
|
+
highlighted,
|
1825
|
+
highlightedStroke,
|
1826
|
+
isHovered,
|
1827
|
+
isLiquidated,
|
1828
|
+
dim: _dim,
|
1829
|
+
image: node.state?.image
|
1830
|
+
});
|
1831
|
+
}
|
1832
|
+
renderSessionNode(ctx, x, y, size) {
|
1833
|
+
const isLight = this.theme === "light";
|
1834
|
+
circle(ctx, x, y, size / 2, {
|
1835
|
+
stroke: true,
|
1836
|
+
strokeStyle: this.colorContrast(),
|
1837
|
+
lineWidth: .2,
|
1838
|
+
fill: true,
|
1839
|
+
fillStyle: this.color(dim(.18, isLight))()
|
1840
|
+
});
|
1841
|
+
}
|
1842
|
+
renderRootNode(ctx, x, y, size, dimmed, isLight) {
|
1843
|
+
circle(ctx, x, y, size / 2, {
|
1844
|
+
stroke: false,
|
1845
|
+
strokeStyle: this.colorContrast(),
|
1846
|
+
lineWidth: .2,
|
1847
|
+
fill: true,
|
1848
|
+
fillStyle: this.color(dim(.18, isLight))()
|
1849
|
+
});
|
1850
|
+
if (this.rootImages) {
|
1851
|
+
const _idx = Math.min(isLight ? 0 : 1, this.rootImages.length - 1);
|
1852
|
+
const _img = this.rootImages[_idx];
|
1853
|
+
const _imgSize = size * .55;
|
1854
|
+
if (_img) img(ctx, _img, x - _imgSize / 2, y - _imgSize / 2, _imgSize, _imgSize, 0, dimmed ? .1 : 1);
|
1855
|
+
}
|
1856
|
+
}
|
1857
|
+
renderCollapsedNode(ctx, x, y, size, options) {
|
1858
|
+
const { fill, stroke, highlighted, highlightedStroke, isSelected, dim: _dim, transform, clusterSize, isLight } = options;
|
1859
|
+
if (highlighted) {
|
1860
|
+
const _size = size + 4;
|
1861
|
+
circle(ctx, x, y, _size / 2, {
|
1862
|
+
stroke: true,
|
1863
|
+
strokeStyle: highlightedStroke,
|
1864
|
+
lineWidth: 1,
|
1865
|
+
fill: false,
|
1866
|
+
fillStyle: fill
|
1867
|
+
});
|
1868
|
+
}
|
1869
|
+
circle(ctx, x, y, size / 2, {
|
1870
|
+
stroke: true,
|
1871
|
+
strokeStyle: stroke,
|
1872
|
+
lineWidth: isSelected ? 1 : .2,
|
1873
|
+
fill: true,
|
1874
|
+
fillStyle: fill
|
1875
|
+
});
|
1876
|
+
const showLabel = transform.scale >= this.visiblityScale(clusterSize) ? 1 : 0;
|
1877
|
+
if (showLabel) {
|
1878
|
+
ctx.font = `${14 / transform.scale}px Sans-Serif`;
|
1879
|
+
ctx.textAlign = "center";
|
1880
|
+
ctx.textBaseline = "middle";
|
1881
|
+
ctx.fillStyle = this.color(dim(_dim ? .2 : .5, isLight))();
|
1882
|
+
ctx.fillText(clusterSize.toString(), x, y);
|
1883
|
+
}
|
1884
|
+
}
|
1885
|
+
renderExpandedNode(ctx, x, y, size, options) {
|
1886
|
+
const { fill, highlighted, highlightedStroke, isHovered, isLiquidated, dim: _dim, image } = options;
|
1887
|
+
const _size = size + 1;
|
1888
|
+
rect(ctx, x - _size / 2, y - _size / 2, _size, _size, {
|
1889
|
+
stroke: highlighted || isHovered,
|
1890
|
+
strokeStyle: isHovered ? fill : highlightedStroke,
|
1891
|
+
lineWidth: .5,
|
1892
|
+
fill: this.hideThumbnails || isLiquidated || !image,
|
1893
|
+
fillStyle: fill,
|
1894
|
+
borderRadius: 1
|
1895
|
+
});
|
1896
|
+
if (image && !this.hideThumbnails && !isLiquidated) img(ctx, image, x - size / 2, y - size / 2, size, size, 1, _dim ? .1 : 1, fill);
|
1897
|
+
}
|
1898
|
+
onDraw = () => {
|
1899
|
+
const context = this.canvas?.getContext("2d");
|
1900
|
+
const transform = this.transformCanvas.getTransform();
|
1901
|
+
if (!context) return;
|
1902
|
+
const dpi = devicePixelRatio || 1;
|
1903
|
+
context.save();
|
1904
|
+
context.scale(dpi, dpi);
|
1905
|
+
context.clearRect(0, 0, this.width, this.height);
|
1906
|
+
context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
|
1907
|
+
context.translate(this.origin.x, this.origin.y);
|
1908
|
+
context.save();
|
1909
|
+
const hasSelection = !!this.selectedNode;
|
1910
|
+
this.renderLayers.links.regular.forEach((link) => {
|
1911
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
1912
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
1913
|
+
const _dim = hasSelection && !this.subGraph.links.find((sl) => getNodeId(sl.source) === sourceId && getNodeId(sl.target) === targetId);
|
1914
|
+
this.renderLink(context, link, {
|
1915
|
+
dim: _dim,
|
1916
|
+
hasSelection
|
1917
|
+
});
|
1918
|
+
});
|
1919
|
+
context.globalAlpha = 1;
|
1920
|
+
this.renderLayers.nodes.regular.forEach((node) => {
|
1921
|
+
const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
|
1922
|
+
this.renderNode(context, node, {
|
1923
|
+
dim: _dim,
|
1924
|
+
transform
|
1925
|
+
});
|
1926
|
+
});
|
1927
|
+
this.renderLayers.links.highlighted.forEach((link) => {
|
1928
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
1929
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
1930
|
+
const _dim = hasSelection && !this.subGraph.links.find((sl) => getNodeId(sl.source) === sourceId && getNodeId(sl.target) === targetId);
|
1931
|
+
const highlight = this.highlights.find((h) => h.id === sourceId || h.id === targetId);
|
1932
|
+
this.renderLink(context, link, {
|
1933
|
+
dim: _dim,
|
1934
|
+
hasSelection,
|
1935
|
+
highlight
|
1936
|
+
});
|
1937
|
+
});
|
1938
|
+
context.globalAlpha = 1;
|
1939
|
+
this.renderLayers.nodes.highlighted.forEach((node) => {
|
1940
|
+
const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
|
1941
|
+
this.renderNode(context, node, {
|
1942
|
+
dim: _dim,
|
1943
|
+
transform
|
1944
|
+
});
|
1945
|
+
});
|
1946
|
+
context.restore();
|
1947
|
+
context.restore();
|
1948
|
+
this.transformCanvas.trackCursor();
|
1949
|
+
};
|
1950
|
+
drawDebug(context) {
|
1951
|
+
const transform = this.transformCanvas.getTransform();
|
1952
|
+
context.font = `14px Sans-Serif`;
|
1953
|
+
context.textAlign = "left";
|
1954
|
+
context.fillStyle = "#000000";
|
1955
|
+
context.fillText(`${transform.x}, ${transform.y}, ${transform.scale}`, 0, 15);
|
1956
|
+
const center = this.worldToScreen(this.rootNode?.x, this.rootNode?.y);
|
1957
|
+
context.fillText(`${center.x}, ${center.y}`, 0, 30);
|
1958
|
+
}
|
1959
|
+
drawDebugDepthCircles(context) {
|
1960
|
+
const transform = this.transformCanvas.getTransform();
|
1961
|
+
for (let i = 0; i < this.maxDepth; i++) {
|
1962
|
+
const depth = i;
|
1963
|
+
const r = getRadius(depth);
|
1964
|
+
const x = this.center.x;
|
1965
|
+
const y = this.center.y;
|
1966
|
+
circle(context, x, y, r, {
|
1967
|
+
fill: false,
|
1968
|
+
stroke: true,
|
1969
|
+
strokeStyle: "#00ff00"
|
1970
|
+
});
|
1971
|
+
context.font = `${40 / transform.scale}px Sans-Serif`;
|
1972
|
+
context.textAlign = "center";
|
1973
|
+
context.textBaseline = "middle";
|
1974
|
+
context.fillStyle = this.color();
|
1975
|
+
context.fillText(depth.toString(), x + r, y);
|
1976
|
+
}
|
1977
|
+
}
|
1978
|
+
onEnd = () => {
|
1979
|
+
this.isTicking = false;
|
1980
|
+
};
|
1981
|
+
loadNodeImages = () => {
|
1982
|
+
this.data.nodes.forEach((node) => {
|
1983
|
+
if (node.id === this.rootId) return;
|
1984
|
+
if (node.imgSrc && this.imageCache.get(node.imgSrc)) {
|
1985
|
+
node.state = node.state || {};
|
1986
|
+
node.state.image = this.imageCache.get(node.imgSrc);
|
1987
|
+
return;
|
1988
|
+
}
|
1989
|
+
const loadImage = async () => {
|
1990
|
+
const src = this.loadNodeImage ? await this.loadNodeImage(node) : node.imgSrc;
|
1991
|
+
if (!src) return;
|
1992
|
+
const html = this.imageCache.get(src) || await loadHTMLImageElement(src);
|
1993
|
+
this.imageCache.set(src, html);
|
1994
|
+
node.state = node.state || {};
|
1995
|
+
node.state.image = html;
|
1996
|
+
};
|
1997
|
+
loadImage();
|
1998
|
+
});
|
1999
|
+
};
|
2000
|
+
destroy = () => {
|
2001
|
+
this.simulation?.stop();
|
2002
|
+
this.simulation?.on("tick", null);
|
2003
|
+
this.simulation?.on("end", null);
|
2004
|
+
this.transformCanvas.destroy();
|
2005
|
+
};
|
2006
|
+
resize = (width, height) => {
|
2007
|
+
this.width = width;
|
2008
|
+
this.height = height;
|
2009
|
+
this.updateScene();
|
2010
|
+
};
|
2011
|
+
setTheme = (theme) => {
|
2012
|
+
this.theme = theme;
|
2013
|
+
this.updateScene();
|
2014
|
+
};
|
2015
|
+
setHideThumbnails = (hide) => {
|
2016
|
+
this.hideThumbnails = hide;
|
2017
|
+
this.updateScene();
|
2018
|
+
};
|
2019
|
+
setSelectedNode = (node) => {
|
2020
|
+
this.selectedNode = node;
|
2021
|
+
this.updateRenderLayers();
|
2022
|
+
this.updateScene();
|
2023
|
+
};
|
2024
|
+
_highlightHash;
|
2025
|
+
setHighlights = (highlights) => {
|
2026
|
+
const hash = quickHash(JSON.stringify(highlights));
|
2027
|
+
if (hash === this._highlightHash) return;
|
2028
|
+
this._highlightHash = hash;
|
2029
|
+
this.highlights = highlights;
|
2030
|
+
this.updateHighlights();
|
2031
|
+
};
|
2032
|
+
setNoInteraction = (noInteraction) => {
|
2033
|
+
this.noInteraction = noInteraction;
|
2034
|
+
this.transformCanvas.setNoInteraction(this.noInteraction);
|
2035
|
+
this.updateScene();
|
2036
|
+
};
|
2037
|
+
getNodeById = (nodeId) => {
|
2038
|
+
return this.data.nodes.find((n) => n.id === nodeId) || null;
|
2039
|
+
};
|
2040
|
+
setLockedNodeId = (nodeId) => {
|
2041
|
+
this.lockedNodeId = nodeId || void 0;
|
2042
|
+
};
|
2043
|
+
handleClickDebug = (x, y) => {
|
2044
|
+
const p = this.screenToWorld(x, y);
|
2045
|
+
this.circles.push(p);
|
2046
|
+
this.transformCanvas.focusOn(() => {
|
2047
|
+
const circle$1 = this.circles[this.circles.length - 1];
|
2048
|
+
return {
|
2049
|
+
x: circle$1.x,
|
2050
|
+
y: circle$1.y,
|
2051
|
+
scale: this.transformCanvas.getTransform().scale
|
2052
|
+
};
|
2053
|
+
});
|
2054
|
+
this.updateScene();
|
2055
|
+
};
|
2056
|
+
circles = [];
|
2057
|
+
onDrawDebug = () => {
|
2058
|
+
const context = this.canvas?.getContext("2d");
|
2059
|
+
const transform = this.transformCanvas.getTransform();
|
2060
|
+
if (!context) return;
|
2061
|
+
const dpi = devicePixelRatio || 1;
|
2062
|
+
context.save();
|
2063
|
+
context.scale(dpi, dpi);
|
2064
|
+
context.clearRect(0, 0, this.width, this.height);
|
2065
|
+
context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
|
2066
|
+
context.translate(this.origin.x, this.origin.y);
|
2067
|
+
context.save();
|
2068
|
+
this.circles.map((c) => {
|
2069
|
+
circle(context, c.x, c.y, 5, {
|
2070
|
+
fill: true,
|
2071
|
+
fillStyle: "#ff0000",
|
2072
|
+
stroke: false
|
2073
|
+
});
|
2074
|
+
});
|
2075
|
+
circle(context, 0, 0, 5, {
|
2076
|
+
fill: true,
|
2077
|
+
fillStyle: "#ff0000",
|
2078
|
+
stroke: false
|
2079
|
+
});
|
2080
|
+
context.restore();
|
2081
|
+
context.restore();
|
2082
|
+
};
|
2083
|
+
};
|
2084
|
+
|
2085
|
+
//#endregion
|
2086
|
+
//#region src/components/OpenFormGraph.tsx
|
2087
|
+
function OpenFormGraph(props) {
|
2088
|
+
const { width, height, highlights = [], className, noInteraction = false, loadNodeImage, translate, onTransform } = props;
|
2089
|
+
const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode, lockedNodeId } = useOpenFormGraph();
|
2090
|
+
const canvasRef = useRef(null);
|
2091
|
+
useEffect(() => {
|
2092
|
+
if (!canvasRef.current) return;
|
2093
|
+
simulation.current = new OpenGraphSimulation({
|
2094
|
+
width,
|
2095
|
+
height,
|
2096
|
+
canvas: canvasRef.current,
|
2097
|
+
rootImageSources,
|
2098
|
+
loadNodeImage,
|
2099
|
+
theme,
|
2100
|
+
translate,
|
2101
|
+
lockedNodeId
|
2102
|
+
});
|
2103
|
+
simulation.current.emitter.on("selected-node-changed", setSelectedNode);
|
2104
|
+
simulation.current.emitter.on("hovered-node-changed", setHoveredNode);
|
2105
|
+
return () => {
|
2106
|
+
simulation.current?.emitter.off("selected-node-changed", setSelectedNode);
|
2107
|
+
simulation.current?.emitter.off("hovered-node-changed", setHoveredNode);
|
2108
|
+
simulation.current?.destroy();
|
2109
|
+
};
|
2110
|
+
}, []);
|
2111
|
+
useEffect(() => {
|
2112
|
+
if (!simulation.current) return;
|
2113
|
+
if (!onTransform) return;
|
2114
|
+
simulation.current.emitter.on("transform-changed", onTransform);
|
2115
|
+
return () => {
|
2116
|
+
simulation.current?.emitter.off("transform-changed", onTransform);
|
2117
|
+
};
|
2118
|
+
}, [onTransform]);
|
2119
|
+
useEffect(() => {
|
2120
|
+
if (!simulation.current) return;
|
2121
|
+
simulation.current.resize(width, height);
|
2122
|
+
}, [width, height]);
|
2123
|
+
useEffect(() => {
|
2124
|
+
if (!simulation.current || !theme) return;
|
2125
|
+
simulation.current.setTheme(theme);
|
2126
|
+
}, [theme]);
|
2127
|
+
useEffect(() => {
|
2128
|
+
if (!simulation.current) return;
|
2129
|
+
simulation.current.setHideThumbnails(hideThumbnails);
|
2130
|
+
}, [hideThumbnails]);
|
2131
|
+
useEffect(() => {
|
2132
|
+
if (!simulation.current) return;
|
2133
|
+
simulation.current.setHighlights(highlights);
|
2134
|
+
}, [highlights]);
|
2135
|
+
useEffect(() => {
|
2136
|
+
if (!simulation.current) return;
|
2137
|
+
simulation.current.setNoInteraction(noInteraction);
|
2138
|
+
}, [noInteraction]);
|
2139
|
+
useEffect(() => {
|
2140
|
+
if (!simulation.current) return;
|
2141
|
+
simulation.current.initialize(data, rootId);
|
2142
|
+
}, [data]);
|
2143
|
+
useEffect(() => {
|
2144
|
+
if (!simulation.current) return;
|
2145
|
+
if (!translate) return;
|
2146
|
+
simulation.current.setTranslate(translate);
|
2147
|
+
}, [translate?.y, translate?.x]);
|
2148
|
+
useEffect(() => {
|
2149
|
+
if (!simulation.current) return;
|
2150
|
+
if (simulation.current.lockedNodeId === lockedNodeId) return;
|
2151
|
+
simulation.current.setLockedNodeId(lockedNodeId);
|
2152
|
+
}, [translate?.y, translate?.x]);
|
2153
|
+
const dpi = devicePixelRatio || 1;
|
2154
|
+
return /* @__PURE__ */ jsx("canvas", {
|
2155
|
+
onMouseEnter: props.onMouseEnter,
|
2156
|
+
onMouseLeave: props.onMouseLeave,
|
2157
|
+
ref: canvasRef,
|
2158
|
+
className,
|
2159
|
+
width: `${width * dpi}px`,
|
2160
|
+
height: `${height * dpi}px`,
|
2161
|
+
style: {
|
2162
|
+
width: `${width}px`,
|
2163
|
+
height: `${height}px`
|
2164
|
+
}
|
2165
|
+
});
|
2166
|
+
}
|
2167
|
+
|
2168
|
+
//#endregion
|
2169
|
+
export { Highlight, OpenFormGraph, OpenGraphEventEmitter, OpenGraphSimulation, TransformCanvas, buildTreeFromGraphData, circle, color, dim, generateTree, getAllParentsUntil, getChildren, getClusterSize, getNodeDepth, getNodeId, getNodeSubgraph, getParents, getPrunedData, getRootParent, hasOnlyLeafs, hexagon, img, isCustomHighlight, isSimLink, isSimNode, loadHTMLImageElement, rect, red, searchParents };
|
2170
|
+
//# sourceMappingURL=OpenFormGraph-i8VSTamk.js.map
|