@fxhash/open-form-graph 0.0.1 → 0.0.2
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-0EqYrhjv.d.ts +17 -0
- package/dist/OpenFormGraph-wo_Y20C6.js +1277 -0
- package/dist/OpenFormGraph-wo_Y20C6.js.map +1 -0
- package/dist/components.d.ts +2 -0
- package/dist/components.js +4 -0
- package/dist/constants-DU_wYtaU.d.ts +242 -0
- package/dist/index.d.ts +111 -133
- package/dist/index.js +3 -2098
- package/dist/provider-CTDz6ZQd.js +103 -0
- package/dist/provider-CTDz6ZQd.js.map +1 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +3 -0
- package/package.json +14 -7
- package/dist/index.js.map +0 -1
@@ -0,0 +1,1277 @@
|
|
1
|
+
import { DEFAULT_GRAPH_CONFIG, VOID_ROOT_ID, useOpenFormGraph } from "./provider-CTDz6ZQd.js";
|
2
|
+
import { useEffect, useRef } from "react";
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
4
|
+
import { forceCenter, forceLink, forceManyBody, forceSimulation } from "d3-force";
|
5
|
+
import { scaleLinear, scaleLog } from "d3-scale";
|
6
|
+
|
7
|
+
//#region src/util/types.ts
|
8
|
+
function isSimNode(node) {
|
9
|
+
return typeof node === "object" && "id" in node;
|
10
|
+
}
|
11
|
+
function isSimLink(link) {
|
12
|
+
return typeof link === "object" && "source" in link && typeof link.source !== "string";
|
13
|
+
}
|
14
|
+
|
15
|
+
//#endregion
|
16
|
+
//#region src/util/graph.ts
|
17
|
+
function getNodeId(n) {
|
18
|
+
return typeof n === "object" && n !== null && "id" in n ? n.id : n;
|
19
|
+
}
|
20
|
+
function getParents(id, links) {
|
21
|
+
return links.filter((l) => {
|
22
|
+
const targetId = isSimNode(l.target) ? l.target.id : l.target;
|
23
|
+
return targetId === id;
|
24
|
+
}).map((link) => isSimNode(link.source) ? link.source.id : link.source.toString());
|
25
|
+
}
|
26
|
+
function getChildren(id, links) {
|
27
|
+
return links.filter((l) => {
|
28
|
+
const sourceId = isSimNode(l.source) ? l.source.id : l.source;
|
29
|
+
return sourceId === id;
|
30
|
+
}).map((link) => link.target.toString());
|
31
|
+
}
|
32
|
+
function getClusterSize(id, links) {
|
33
|
+
const children = getChildren(id, links);
|
34
|
+
return children.reduce((acc, childId) => {
|
35
|
+
return acc + getClusterSize(childId, links);
|
36
|
+
}, children.length || 0);
|
37
|
+
}
|
38
|
+
function hasOnlyLeafs(id, links) {
|
39
|
+
const children = getChildren(id, links);
|
40
|
+
return children.every((childId) => getChildren(childId, links).length === 0);
|
41
|
+
}
|
42
|
+
function getNodeSubgraph(nodeId, nodes, links, rootId) {
|
43
|
+
const nodesById = Object.fromEntries(nodes.map((n) => [n.id, n]));
|
44
|
+
const parentSet = new Set();
|
45
|
+
const childSet = new Set();
|
46
|
+
const subLinks = new Set();
|
47
|
+
let currentId = nodeId;
|
48
|
+
while (currentId !== rootId) {
|
49
|
+
const parentLink = links.find((l) => {
|
50
|
+
const targetId = isSimNode(l.target) ? l.target.id : l.target;
|
51
|
+
return targetId === currentId;
|
52
|
+
});
|
53
|
+
if (!parentLink) break;
|
54
|
+
const parentId = isSimNode(parentLink.source) ? parentLink.source.id : parentLink.source;
|
55
|
+
if (parentSet.has(parentId.toString())) break;
|
56
|
+
parentSet.add(parentId.toString());
|
57
|
+
subLinks.add(parentLink);
|
58
|
+
currentId = parentId.toString();
|
59
|
+
}
|
60
|
+
function collectChildren(id) {
|
61
|
+
for (const link of links) {
|
62
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
63
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
64
|
+
if (sourceId === id && !childSet.has(targetId.toString())) {
|
65
|
+
childSet.add(targetId.toString());
|
66
|
+
subLinks.add(link);
|
67
|
+
collectChildren(targetId.toString());
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
collectChildren(nodeId);
|
72
|
+
const validIds = new Set([
|
73
|
+
...parentSet,
|
74
|
+
nodeId,
|
75
|
+
...childSet
|
76
|
+
]);
|
77
|
+
const filteredLinks = Array.from(subLinks).filter((link) => {
|
78
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
79
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
80
|
+
return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
|
81
|
+
});
|
82
|
+
const allNodeIds = Array.from(validIds);
|
83
|
+
const subNodes = allNodeIds.map((id) => nodesById[id]).filter(Boolean);
|
84
|
+
return {
|
85
|
+
nodes: subNodes,
|
86
|
+
links: filteredLinks
|
87
|
+
};
|
88
|
+
}
|
89
|
+
|
90
|
+
//#endregion
|
91
|
+
//#region src/util/img.ts
|
92
|
+
function loadImage(src) {
|
93
|
+
return new Promise((resolve, reject) => {
|
94
|
+
const img$1 = new Image();
|
95
|
+
img$1.onload = () => resolve(img$1);
|
96
|
+
img$1.onerror = reject;
|
97
|
+
img$1.src = src;
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
//#endregion
|
102
|
+
//#region src/sim/TransformCanvas.ts
|
103
|
+
const MIN_ZOOM = .1;
|
104
|
+
const MAX_ZOOM = 10;
|
105
|
+
const CLICK_THRESHOLD = 5;
|
106
|
+
var TransformCanvas = class {
|
107
|
+
canvas;
|
108
|
+
transform = {
|
109
|
+
x: 0,
|
110
|
+
y: 0,
|
111
|
+
scale: 1
|
112
|
+
};
|
113
|
+
targetTransform = {
|
114
|
+
x: 0,
|
115
|
+
y: 0,
|
116
|
+
scale: 1
|
117
|
+
};
|
118
|
+
isAnimating = false;
|
119
|
+
animationFrame = null;
|
120
|
+
isDragging = false;
|
121
|
+
dragStart = null;
|
122
|
+
moved = false;
|
123
|
+
lastMousePos = {
|
124
|
+
x: 0,
|
125
|
+
y: 0
|
126
|
+
};
|
127
|
+
lastMoveMousePos = {
|
128
|
+
x: 0,
|
129
|
+
y: 0
|
130
|
+
};
|
131
|
+
zoomFocus = {
|
132
|
+
x: 0,
|
133
|
+
y: 0
|
134
|
+
};
|
135
|
+
onUpdate;
|
136
|
+
onClick;
|
137
|
+
onMove;
|
138
|
+
noInteraction = false;
|
139
|
+
touchStart = null;
|
140
|
+
lastTouchPos = {
|
141
|
+
x: 0,
|
142
|
+
y: 0
|
143
|
+
};
|
144
|
+
pinchStartDist = null;
|
145
|
+
pinchStartScale = 1;
|
146
|
+
focus = null;
|
147
|
+
constructor(canvas, options) {
|
148
|
+
this.canvas = canvas;
|
149
|
+
this.onUpdate = options?.onUpdate;
|
150
|
+
this.onClick = options?.onClick;
|
151
|
+
this.onMove = options?.onMove;
|
152
|
+
this.handleWheel = this.handleWheel.bind(this);
|
153
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
154
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
155
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
156
|
+
this.handleMouseUpCanvas = this.handleMouseUpCanvas.bind(this);
|
157
|
+
canvas.addEventListener("wheel", this.handleWheel, { passive: false });
|
158
|
+
canvas.addEventListener("mousedown", this.handleMouseDown);
|
159
|
+
window.addEventListener("mousemove", this.handleMouseMove);
|
160
|
+
window.addEventListener("mouseup", this.handleMouseUp);
|
161
|
+
canvas.addEventListener("mouseup", this.handleMouseUpCanvas);
|
162
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
163
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
164
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
165
|
+
canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
|
166
|
+
canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
|
167
|
+
canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
|
168
|
+
canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
|
169
|
+
}
|
170
|
+
lerp(a, b, t) {
|
171
|
+
return a + (b - a) * t;
|
172
|
+
}
|
173
|
+
animateTransform = () => {
|
174
|
+
const speed = .05;
|
175
|
+
const prev = this.transform;
|
176
|
+
const target = this.focus?.() || this.targetTransform;
|
177
|
+
const next = {
|
178
|
+
x: this.lerp(prev.x, target.x, speed),
|
179
|
+
y: this.lerp(prev.y, target.y, speed),
|
180
|
+
scale: this.lerp(prev.scale, target.scale, speed)
|
181
|
+
};
|
182
|
+
const done = Math.abs(next.x - target.x) < .5 && Math.abs(next.y - target.y) < .5 && Math.abs(next.scale - target.scale) < .001;
|
183
|
+
if (done) {
|
184
|
+
this.transform = { ...target };
|
185
|
+
this.isAnimating = false;
|
186
|
+
this.focus = null;
|
187
|
+
} else {
|
188
|
+
this.transform = next;
|
189
|
+
this.isAnimating = true;
|
190
|
+
}
|
191
|
+
this.onUpdate?.(this.transform);
|
192
|
+
if (this.isAnimating) this.animationFrame = requestAnimationFrame(this.animateTransform);
|
193
|
+
else if (this.animationFrame) {
|
194
|
+
cancelAnimationFrame(this.animationFrame);
|
195
|
+
this.animationFrame = null;
|
196
|
+
}
|
197
|
+
};
|
198
|
+
handleWheel(e) {
|
199
|
+
if (this.noInteraction) return;
|
200
|
+
if (this.isAnimating) {
|
201
|
+
this.isAnimating = false;
|
202
|
+
this.focus = null;
|
203
|
+
if (this.animationFrame) {
|
204
|
+
cancelAnimationFrame(this.animationFrame);
|
205
|
+
this.animationFrame = null;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
e.preventDefault();
|
209
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
210
|
+
const mouseX = e.clientX - rect$1.left;
|
211
|
+
const mouseY = e.clientY - rect$1.top;
|
212
|
+
this.zoomFocus = {
|
213
|
+
x: mouseX,
|
214
|
+
y: mouseY
|
215
|
+
};
|
216
|
+
const scaleFactor = e.deltaY > 0 ? .95 : 1.05;
|
217
|
+
const newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.targetTransform.scale * scaleFactor));
|
218
|
+
const { x, y, scale } = this.transform;
|
219
|
+
const dx = mouseX - x;
|
220
|
+
const dy = mouseY - y;
|
221
|
+
const scaleChange = newScale / scale;
|
222
|
+
const newX = mouseX - dx * scaleChange;
|
223
|
+
const newY = mouseY - dy * scaleChange;
|
224
|
+
this.targetTransform = {
|
225
|
+
x: newX,
|
226
|
+
y: newY,
|
227
|
+
scale: newScale
|
228
|
+
};
|
229
|
+
if (!this.isAnimating) {
|
230
|
+
this.isAnimating = true;
|
231
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
232
|
+
}
|
233
|
+
}
|
234
|
+
handleMouseDown(e) {
|
235
|
+
if (this.noInteraction) return;
|
236
|
+
if (this.isAnimating) {
|
237
|
+
this.isAnimating = false;
|
238
|
+
this.focus = null;
|
239
|
+
if (this.animationFrame) {
|
240
|
+
cancelAnimationFrame(this.animationFrame);
|
241
|
+
this.animationFrame = null;
|
242
|
+
}
|
243
|
+
}
|
244
|
+
this.isDragging = true;
|
245
|
+
this.moved = false;
|
246
|
+
this.dragStart = {
|
247
|
+
x: e.clientX,
|
248
|
+
y: e.clientY
|
249
|
+
};
|
250
|
+
this.lastMousePos = {
|
251
|
+
x: e.clientX,
|
252
|
+
y: e.clientY
|
253
|
+
};
|
254
|
+
}
|
255
|
+
handleMouseMove(e) {
|
256
|
+
if (this.noInteraction) return;
|
257
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
258
|
+
const x = e.clientX - rect$1.left;
|
259
|
+
const y = e.clientY - rect$1.top;
|
260
|
+
this.lastMoveMousePos = {
|
261
|
+
x: e.clientX,
|
262
|
+
y: e.clientY
|
263
|
+
};
|
264
|
+
this.onMove?.(x, y);
|
265
|
+
if (!this.isDragging || !this.dragStart) return;
|
266
|
+
const dx = e.clientX - this.dragStart.x;
|
267
|
+
const dy = e.clientY - this.dragStart.y;
|
268
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
269
|
+
this.moved = true;
|
270
|
+
const deltaX = e.clientX - this.lastMousePos.x;
|
271
|
+
const deltaY = e.clientY - this.lastMousePos.y;
|
272
|
+
this.transform.x += deltaX;
|
273
|
+
this.transform.y += deltaY;
|
274
|
+
this.targetTransform.x += deltaX;
|
275
|
+
this.targetTransform.y += deltaY;
|
276
|
+
this.lastMousePos = {
|
277
|
+
x: e.clientX,
|
278
|
+
y: e.clientY
|
279
|
+
};
|
280
|
+
this.onUpdate?.(this.transform);
|
281
|
+
}
|
282
|
+
}
|
283
|
+
handleMouseUpCanvas(e) {
|
284
|
+
if (this.noInteraction) return;
|
285
|
+
if (!this.moved && this.onClick) {
|
286
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
287
|
+
const x = e.clientX - rect$1.left;
|
288
|
+
const y = e.clientY - rect$1.top;
|
289
|
+
this.onClick(x, y);
|
290
|
+
}
|
291
|
+
}
|
292
|
+
handleMouseUp(e) {
|
293
|
+
this.isDragging = false;
|
294
|
+
this.dragStart = null;
|
295
|
+
this.moved = false;
|
296
|
+
}
|
297
|
+
handleTouchStart(e) {
|
298
|
+
if (this.noInteraction) return;
|
299
|
+
if (this.isAnimating) {
|
300
|
+
this.isAnimating = false;
|
301
|
+
this.focus = null;
|
302
|
+
if (this.animationFrame) {
|
303
|
+
cancelAnimationFrame(this.animationFrame);
|
304
|
+
this.animationFrame = null;
|
305
|
+
}
|
306
|
+
}
|
307
|
+
if (e.touches.length === 1) {
|
308
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
309
|
+
const touch = e.touches[0];
|
310
|
+
this.isDragging = true;
|
311
|
+
this.moved = false;
|
312
|
+
this.touchStart = {
|
313
|
+
x: touch.clientX,
|
314
|
+
y: touch.clientY
|
315
|
+
};
|
316
|
+
this.lastTouchPos = {
|
317
|
+
x: touch.clientX,
|
318
|
+
y: touch.clientY
|
319
|
+
};
|
320
|
+
this.lastMoveMousePos = {
|
321
|
+
x: touch.clientX,
|
322
|
+
y: touch.clientY
|
323
|
+
};
|
324
|
+
} else if (e.touches.length === 2) {
|
325
|
+
this.isDragging = false;
|
326
|
+
const [t1, t2] = [e.touches[0], e.touches[1]];
|
327
|
+
this.pinchStartDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
328
|
+
this.pinchStartScale = this.targetTransform.scale;
|
329
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
330
|
+
this.zoomFocus = {
|
331
|
+
x: (t1.clientX + t2.clientX) / 2 - rect$1.left,
|
332
|
+
y: (t1.clientY + t2.clientY) / 2 - rect$1.top
|
333
|
+
};
|
334
|
+
}
|
335
|
+
}
|
336
|
+
handleTouchMove(e) {
|
337
|
+
if (this.noInteraction) return;
|
338
|
+
e.preventDefault();
|
339
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
340
|
+
if (e.touches.length === 1 && this.touchStart) {
|
341
|
+
const touch = e.touches[0];
|
342
|
+
const dx = touch.clientX - this.touchStart.x;
|
343
|
+
const dy = touch.clientY - this.touchStart.y;
|
344
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
345
|
+
this.moved = true;
|
346
|
+
const deltaX = touch.clientX - this.lastTouchPos.x;
|
347
|
+
const deltaY = touch.clientY - this.lastTouchPos.y;
|
348
|
+
this.transform.x += deltaX;
|
349
|
+
this.transform.y += deltaY;
|
350
|
+
this.targetTransform.x += deltaX;
|
351
|
+
this.targetTransform.y += deltaY;
|
352
|
+
this.lastTouchPos = {
|
353
|
+
x: touch.clientX,
|
354
|
+
y: touch.clientY
|
355
|
+
};
|
356
|
+
this.lastMoveMousePos = {
|
357
|
+
x: touch.clientX,
|
358
|
+
y: touch.clientY
|
359
|
+
};
|
360
|
+
this.onUpdate?.(this.transform);
|
361
|
+
this.onMove?.(touch.clientX - rect$1.left, touch.clientY - rect$1.top);
|
362
|
+
}
|
363
|
+
} else if (e.touches.length === 2 && this.pinchStartDist != null) {
|
364
|
+
const [t1, t2] = [e.touches[0], e.touches[1]];
|
365
|
+
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
366
|
+
let scale = dist / this.pinchStartDist * this.pinchStartScale;
|
367
|
+
scale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, scale));
|
368
|
+
const { x, y, scale: prevScale } = this.transform;
|
369
|
+
const dx = this.zoomFocus.x - x;
|
370
|
+
const dy = this.zoomFocus.y - y;
|
371
|
+
const scaleChange = scale / prevScale;
|
372
|
+
const newX = this.zoomFocus.x - dx * scaleChange;
|
373
|
+
const newY = this.zoomFocus.y - dy * scaleChange;
|
374
|
+
this.targetTransform = {
|
375
|
+
x: newX,
|
376
|
+
y: newY,
|
377
|
+
scale
|
378
|
+
};
|
379
|
+
if (!this.isAnimating) {
|
380
|
+
this.isAnimating = true;
|
381
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
382
|
+
}
|
383
|
+
}
|
384
|
+
}
|
385
|
+
handleTouchEnd(e) {
|
386
|
+
if (this.noInteraction) return;
|
387
|
+
if (e.touches.length === 0) {
|
388
|
+
if (this.isDragging && !this.moved && this.onClick && this.touchStart) {
|
389
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
390
|
+
this.onClick(this.touchStart.x - rect$1.left, this.touchStart.y - rect$1.top);
|
391
|
+
}
|
392
|
+
this.isDragging = false;
|
393
|
+
this.touchStart = null;
|
394
|
+
this.moved = false;
|
395
|
+
this.pinchStartDist = null;
|
396
|
+
} else if (e.touches.length === 1) {
|
397
|
+
const touch = e.touches[0];
|
398
|
+
this.isDragging = true;
|
399
|
+
this.touchStart = {
|
400
|
+
x: touch.clientX,
|
401
|
+
y: touch.clientY
|
402
|
+
};
|
403
|
+
this.lastTouchPos = {
|
404
|
+
x: touch.clientX,
|
405
|
+
y: touch.clientY
|
406
|
+
};
|
407
|
+
this.pinchStartDist = null;
|
408
|
+
}
|
409
|
+
}
|
410
|
+
resetZoom() {
|
411
|
+
this.transform = {
|
412
|
+
x: 0,
|
413
|
+
y: 0,
|
414
|
+
scale: 1
|
415
|
+
};
|
416
|
+
this.targetTransform = {
|
417
|
+
x: 0,
|
418
|
+
y: 0,
|
419
|
+
scale: 1
|
420
|
+
};
|
421
|
+
this.onUpdate?.(this.transform);
|
422
|
+
}
|
423
|
+
transformTo(update) {
|
424
|
+
this.targetTransform = {
|
425
|
+
...this.transform,
|
426
|
+
...update
|
427
|
+
};
|
428
|
+
if (!this.isAnimating) {
|
429
|
+
this.isAnimating = true;
|
430
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
431
|
+
}
|
432
|
+
}
|
433
|
+
trackCursor() {
|
434
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
435
|
+
const x = this.lastMoveMousePos.x - rect$1.left;
|
436
|
+
const y = this.lastMoveMousePos.y - rect$1.top;
|
437
|
+
this.onMove?.(x, y);
|
438
|
+
}
|
439
|
+
destroy() {
|
440
|
+
this.canvas.removeEventListener("wheel", this.handleWheel);
|
441
|
+
this.canvas.removeEventListener("mousedown", this.handleMouseDown);
|
442
|
+
this.canvas.removeEventListener("touchstart", this.handleTouchStart);
|
443
|
+
this.canvas.removeEventListener("touchmove", this.handleTouchMove);
|
444
|
+
this.canvas.removeEventListener("touchend", this.handleTouchEnd);
|
445
|
+
this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
|
446
|
+
this.canvas.removeEventListener("mouseup", this.handleMouseUpCanvas);
|
447
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
448
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
449
|
+
this.isAnimating = false;
|
450
|
+
this.focus = null;
|
451
|
+
if (this.animationFrame) {
|
452
|
+
cancelAnimationFrame(this.animationFrame);
|
453
|
+
this.animationFrame = null;
|
454
|
+
}
|
455
|
+
}
|
456
|
+
setNoInteraction = (noInteraction) => {
|
457
|
+
this.noInteraction = noInteraction;
|
458
|
+
};
|
459
|
+
focusOn = (focus) => {
|
460
|
+
this.focus = focus;
|
461
|
+
};
|
462
|
+
};
|
463
|
+
|
464
|
+
//#endregion
|
465
|
+
//#region src/util/canvas.ts
|
466
|
+
/**
|
467
|
+
* draws a circle on the canvas
|
468
|
+
* @param ctx - The canvas rendering context
|
469
|
+
* @param x - The x-coordinate of the circle's center
|
470
|
+
* @param y - The y-coordinate of the circle's center
|
471
|
+
* @param radius - The radius of the circle (default is 5)
|
472
|
+
* @param options - Optional parameters for styling the circle
|
473
|
+
* @param options.fill - Whether to fill the circle (default is true)
|
474
|
+
* @param options.fillStyle - The fill color of the circle
|
475
|
+
* @param options.stroke - Whether to stroke the circle (default is false)
|
476
|
+
* @param options.strokeStyle - The stroke color of the circle
|
477
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
478
|
+
* @returns void
|
479
|
+
*/
|
480
|
+
function circle(ctx, x, y, radius = 5, options) {
|
481
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2 } = options || {};
|
482
|
+
ctx.save();
|
483
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
484
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
485
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
486
|
+
ctx.beginPath();
|
487
|
+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
488
|
+
ctx.closePath();
|
489
|
+
if (fill) ctx.fill();
|
490
|
+
if (stroke) ctx.stroke();
|
491
|
+
ctx.restore();
|
492
|
+
}
|
493
|
+
/**
|
494
|
+
* draws a rectangle on the canvas
|
495
|
+
* @param ctx - The canvas rendering context
|
496
|
+
* @param x - The x-coordinate of the rectangle's top-left corner
|
497
|
+
* @param y - The y-coordinate of the rectangle's top-left corner
|
498
|
+
* @param width - The width of the rectangle
|
499
|
+
* @param height - The height of the rectangle
|
500
|
+
* @param options - Optional parameters for styling the rectangle
|
501
|
+
* @param options.fill - Whether to fill the rectangle (default is true)
|
502
|
+
* @param options.fillStyle - The fill color of the rectangle
|
503
|
+
* @param options.stroke - Whether to stroke the rectangle (default is false)
|
504
|
+
* @param options.strokeStyle - The stroke color of the rectangle
|
505
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
506
|
+
* @param options.borderRadius - The radius of the corners (default is 0)
|
507
|
+
* @returns void
|
508
|
+
*/
|
509
|
+
function rect(ctx, x, y, width, height, options) {
|
510
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, borderRadius = 0 } = options || {};
|
511
|
+
ctx.save();
|
512
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
513
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
514
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
515
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
516
|
+
ctx.beginPath();
|
517
|
+
if (r > 0) {
|
518
|
+
ctx.moveTo(x + r, y);
|
519
|
+
ctx.lineTo(x + width - r, y);
|
520
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
521
|
+
ctx.lineTo(x + width, y + height - r);
|
522
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
523
|
+
ctx.lineTo(x + r, y + height);
|
524
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
525
|
+
ctx.lineTo(x, y + r);
|
526
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
527
|
+
} else ctx.rect(x, y, width, height);
|
528
|
+
ctx.closePath();
|
529
|
+
if (fill) ctx.fill();
|
530
|
+
if (stroke) ctx.stroke();
|
531
|
+
ctx.restore();
|
532
|
+
}
|
533
|
+
/**
|
534
|
+
* draws an image on the canvas with optional border radius and opacity
|
535
|
+
* @param ctx - The canvas rendering context
|
536
|
+
* @param image - The HTMLImageElement to draw
|
537
|
+
* @param x - The x-coordinate of the image's top-left corner
|
538
|
+
* @param y - The y-coordinate of the image's top-left corner
|
539
|
+
* @param width - The width of the image
|
540
|
+
* @param height - The height of the image
|
541
|
+
* @param borderRadius - The radius of the corners (default is 0)
|
542
|
+
* @param opacity - The opacity of the image (default is 1.0)
|
543
|
+
* @param bgColor - Optional background color to fill the clipped area
|
544
|
+
* @returns void
|
545
|
+
*/
|
546
|
+
function img(ctx, image, x, y, width, height, borderRadius = 0, opacity = 1, bgColor) {
|
547
|
+
ctx.save();
|
548
|
+
ctx.beginPath();
|
549
|
+
if (borderRadius > 0) {
|
550
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
551
|
+
ctx.moveTo(x + r, y);
|
552
|
+
ctx.lineTo(x + width - r, y);
|
553
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
554
|
+
ctx.lineTo(x + width, y + height - r);
|
555
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
556
|
+
ctx.lineTo(x + r, y + height);
|
557
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
558
|
+
ctx.lineTo(x, y + r);
|
559
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
560
|
+
} else ctx.rect(x, y, width, height);
|
561
|
+
ctx.closePath();
|
562
|
+
if (bgColor) {
|
563
|
+
ctx.save();
|
564
|
+
ctx.fillStyle = bgColor;
|
565
|
+
ctx.globalAlpha = 1;
|
566
|
+
ctx.fill();
|
567
|
+
ctx.restore();
|
568
|
+
}
|
569
|
+
ctx.clip();
|
570
|
+
ctx.globalAlpha = opacity;
|
571
|
+
ctx.drawImage(image, x, y, width, height);
|
572
|
+
ctx.restore();
|
573
|
+
}
|
574
|
+
function hexagon(ctx, x, y, radius, options) {
|
575
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, rotation = 0, borderRadius = 0 } = options || {};
|
576
|
+
ctx.save();
|
577
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
578
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
579
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
580
|
+
const sides = 6;
|
581
|
+
const angleStep = Math.PI * 2 / sides;
|
582
|
+
ctx.beginPath();
|
583
|
+
const points = [];
|
584
|
+
for (let i = 0; i < sides; i++) {
|
585
|
+
const angle = rotation + i * angleStep;
|
586
|
+
points.push({
|
587
|
+
x: x + radius * Math.cos(angle),
|
588
|
+
y: y + radius * Math.sin(angle)
|
589
|
+
});
|
590
|
+
}
|
591
|
+
if (borderRadius > 0) {
|
592
|
+
const maxBorderRadius = Math.min(borderRadius, radius / 3);
|
593
|
+
for (let i = 0; i < sides; i++) {
|
594
|
+
const current = points[i];
|
595
|
+
const next = points[(i + 1) % sides];
|
596
|
+
const prev = points[(i - 1 + sides) % sides];
|
597
|
+
const toPrev = {
|
598
|
+
x: prev.x - current.x,
|
599
|
+
y: prev.y - current.y
|
600
|
+
};
|
601
|
+
const toNext = {
|
602
|
+
x: next.x - current.x,
|
603
|
+
y: next.y - current.y
|
604
|
+
};
|
605
|
+
const lenPrev = Math.sqrt(toPrev.x * toPrev.x + toPrev.y * toPrev.y);
|
606
|
+
const lenNext = Math.sqrt(toNext.x * toNext.x + toNext.y * toNext.y);
|
607
|
+
const normPrev = {
|
608
|
+
x: toPrev.x / lenPrev,
|
609
|
+
y: toPrev.y / lenPrev
|
610
|
+
};
|
611
|
+
const normNext = {
|
612
|
+
x: toNext.x / lenNext,
|
613
|
+
y: toNext.y / lenNext
|
614
|
+
};
|
615
|
+
const cpPrev = {
|
616
|
+
x: current.x + normPrev.x * maxBorderRadius,
|
617
|
+
y: current.y + normPrev.y * maxBorderRadius
|
618
|
+
};
|
619
|
+
const cpNext = {
|
620
|
+
x: current.x + normNext.x * maxBorderRadius,
|
621
|
+
y: current.y + normNext.y * maxBorderRadius
|
622
|
+
};
|
623
|
+
if (i === 0) ctx.moveTo(cpPrev.x, cpPrev.y);
|
624
|
+
else ctx.lineTo(cpPrev.x, cpPrev.y);
|
625
|
+
ctx.quadraticCurveTo(current.x, current.y, cpNext.x, cpNext.y);
|
626
|
+
}
|
627
|
+
} else {
|
628
|
+
ctx.moveTo(points[0].x, points[0].y);
|
629
|
+
for (let i = 1; i < sides; i++) ctx.lineTo(points[i].x, points[i].y);
|
630
|
+
}
|
631
|
+
ctx.closePath();
|
632
|
+
if (fill) ctx.fill();
|
633
|
+
if (stroke) ctx.stroke();
|
634
|
+
ctx.restore();
|
635
|
+
}
|
636
|
+
|
637
|
+
//#endregion
|
638
|
+
//#region src/util/color.ts
|
639
|
+
/**
|
640
|
+
* Some utility functions to handle colors
|
641
|
+
*/
|
642
|
+
function color(rgb) {
|
643
|
+
const colorHandler = function(arg) {
|
644
|
+
if (typeof arg === "number") return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${arg})`;
|
645
|
+
if (typeof arg === "function") {
|
646
|
+
const transformedRGB = arg(rgb);
|
647
|
+
return color(transformedRGB);
|
648
|
+
}
|
649
|
+
return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
650
|
+
};
|
651
|
+
colorHandler.rgb = rgb;
|
652
|
+
return colorHandler;
|
653
|
+
}
|
654
|
+
/**
|
655
|
+
* Dims a color to white or black
|
656
|
+
* @param color An array of 3 numbers representing RGB values (0-255)
|
657
|
+
* @param dimFactor A value between 0 and 1 where 0 is completely black and 1 is the original color
|
658
|
+
* @returns A new array with the dimmed RGB values
|
659
|
+
*/
|
660
|
+
function dim(factor = .6, white = true) {
|
661
|
+
const base = white ? 255 : 0;
|
662
|
+
return function(rgb) {
|
663
|
+
return [
|
664
|
+
Math.round(rgb[0] + (base - rgb[0]) * (1 - factor)),
|
665
|
+
Math.round(rgb[1] + (base - rgb[1]) * (1 - factor)),
|
666
|
+
Math.round(rgb[2] + (base - rgb[2]) * (1 - factor))
|
667
|
+
];
|
668
|
+
};
|
669
|
+
}
|
670
|
+
|
671
|
+
//#endregion
|
672
|
+
//#region src/util/data.ts
|
673
|
+
const images = [];
|
674
|
+
function generateTree(maxNodes, maxChildren) {
|
675
|
+
const nodes = [];
|
676
|
+
const links = [];
|
677
|
+
let index = 0;
|
678
|
+
function createNode(label) {
|
679
|
+
const isRoot = label === VOID_ROOT_ID;
|
680
|
+
const idx = isRoot ? VOID_ROOT_ID : index++;
|
681
|
+
return {
|
682
|
+
id: idx.toString(),
|
683
|
+
label,
|
684
|
+
imgSrc: isRoot ? void 0 : images[index % images.length]
|
685
|
+
};
|
686
|
+
}
|
687
|
+
const root = createNode(VOID_ROOT_ID);
|
688
|
+
nodes.push(root);
|
689
|
+
const queue = [root];
|
690
|
+
while (queue.length > 0 && nodes.length < maxNodes) {
|
691
|
+
const parent = queue.shift();
|
692
|
+
const rand = Math.random();
|
693
|
+
const biased = Math.floor(Math.pow(rand, 2) * (maxChildren + 1));
|
694
|
+
const childrenCount = Math.min(biased, maxChildren);
|
695
|
+
for (let i = 0; i < childrenCount; i++) {
|
696
|
+
if (nodes.length >= maxNodes) break;
|
697
|
+
const child = createNode(`Node ${nodes.length}`);
|
698
|
+
nodes.push(child);
|
699
|
+
links.push({
|
700
|
+
source: parent.id,
|
701
|
+
target: child.id
|
702
|
+
});
|
703
|
+
queue.push(child);
|
704
|
+
}
|
705
|
+
}
|
706
|
+
return {
|
707
|
+
nodes,
|
708
|
+
links
|
709
|
+
};
|
710
|
+
}
|
711
|
+
function getPrunedData(startId, nodes, links) {
|
712
|
+
const nodesById = Object.fromEntries(nodes.map((node) => [node.id, node]));
|
713
|
+
const visibleNodes = [];
|
714
|
+
const visibleLinks = [];
|
715
|
+
const visited = new Set();
|
716
|
+
(function traverseTree(node = nodesById[startId]) {
|
717
|
+
if (!node || visited.has(node.id)) return;
|
718
|
+
visited.add(node.id);
|
719
|
+
visibleNodes.push(node);
|
720
|
+
if (node?.state?.collapsed) return;
|
721
|
+
const childLinks = links.filter((l) => (isSimNode(l.source) ? l.source.id : l.source) === node.id);
|
722
|
+
visibleLinks.push(...childLinks);
|
723
|
+
childLinks.map((link) => link.target).forEach((n) => traverseTree(isSimNode(n) ? n : nodesById[n.toString()]));
|
724
|
+
})();
|
725
|
+
return {
|
726
|
+
nodes: visibleNodes,
|
727
|
+
links: visibleLinks
|
728
|
+
};
|
729
|
+
}
|
730
|
+
/**
|
731
|
+
* Automatically identifies root nodes and builds a nested structure
|
732
|
+
* @param nodes Array of raw nodes
|
733
|
+
* @param links Array of links between nodes
|
734
|
+
* @returns Array of nested nodes starting from identified roots
|
735
|
+
*/
|
736
|
+
function buildTreeFromGraphData(nodes, links) {
|
737
|
+
const nodeMap = new Map();
|
738
|
+
nodes.forEach((node) => nodeMap.set(node.id, node));
|
739
|
+
const childrenMap = new Map();
|
740
|
+
const parentMap = new Map();
|
741
|
+
nodes.forEach((node) => {
|
742
|
+
childrenMap.set(node.id, []);
|
743
|
+
parentMap.set(node.id, []);
|
744
|
+
});
|
745
|
+
links.forEach((link) => {
|
746
|
+
if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
|
747
|
+
const children = childrenMap.get(link.source) || [];
|
748
|
+
if (!children.includes(link.target)) {
|
749
|
+
children.push(link.target);
|
750
|
+
childrenMap.set(link.source, children);
|
751
|
+
}
|
752
|
+
const parents = parentMap.get(link.target) || [];
|
753
|
+
if (!parents.includes(link.source)) {
|
754
|
+
parents.push(link.source);
|
755
|
+
parentMap.set(link.target, parents);
|
756
|
+
}
|
757
|
+
}
|
758
|
+
});
|
759
|
+
const rootHashes = [];
|
760
|
+
nodeMap.forEach((_, hash) => {
|
761
|
+
const parents = parentMap.get(hash) || [];
|
762
|
+
if (parents.length === 0) rootHashes.push(hash);
|
763
|
+
});
|
764
|
+
const buildNode = (hash, visited = new Set()) => {
|
765
|
+
if (visited.has(hash)) return null;
|
766
|
+
visited.add(hash);
|
767
|
+
const node = nodeMap.get(hash);
|
768
|
+
if (!node) return null;
|
769
|
+
const childHashes = childrenMap.get(hash) || [];
|
770
|
+
const nestedChildren = [];
|
771
|
+
childHashes.forEach((childHash) => {
|
772
|
+
const childNode = buildNode(childHash, new Set([...visited]));
|
773
|
+
if (childNode) nestedChildren.push(childNode);
|
774
|
+
});
|
775
|
+
return {
|
776
|
+
...node,
|
777
|
+
children: nestedChildren
|
778
|
+
};
|
779
|
+
};
|
780
|
+
const result = [];
|
781
|
+
rootHashes.forEach((rootHash) => {
|
782
|
+
const nestedRoot = buildNode(rootHash);
|
783
|
+
if (nestedRoot) result.push(nestedRoot);
|
784
|
+
});
|
785
|
+
const processedNodes = new Set();
|
786
|
+
const markProcessed = (node) => {
|
787
|
+
processedNodes.add(node.id);
|
788
|
+
node.children.forEach(markProcessed);
|
789
|
+
};
|
790
|
+
result.forEach(markProcessed);
|
791
|
+
nodeMap.forEach((_, hash) => {
|
792
|
+
if (!processedNodes.has(hash)) {
|
793
|
+
const orphanedRoot = buildNode(hash);
|
794
|
+
if (orphanedRoot) result.push(orphanedRoot);
|
795
|
+
}
|
796
|
+
});
|
797
|
+
return result;
|
798
|
+
}
|
799
|
+
/**
|
800
|
+
* Recursively retrieves all parents of a node from a graph data structure
|
801
|
+
* @param {string} nodeHash - The hash of the node to find parents for
|
802
|
+
* @param {RawNode[]} nodes - Array of nodes in the graph
|
803
|
+
* @param {RawLink[]} links - Array of links connecting the nodes
|
804
|
+
* @returns {RawNode[]} - Array of parent nodes
|
805
|
+
*/
|
806
|
+
function searchParents(nodeHash, nodes, links) {
|
807
|
+
const visited = new Set();
|
808
|
+
function findParents(hash) {
|
809
|
+
if (visited.has(hash)) return [];
|
810
|
+
visited.add(hash);
|
811
|
+
const immediateParents = links.filter((link) => link.target === hash).map((link) => link.source);
|
812
|
+
const parentNodes = nodes.filter((node) => immediateParents.includes(node.id));
|
813
|
+
const ancestorNodes = immediateParents.flatMap((parentHash) => findParents(parentHash));
|
814
|
+
return [...parentNodes, ...ancestorNodes];
|
815
|
+
}
|
816
|
+
return findParents(nodeHash);
|
817
|
+
}
|
818
|
+
|
819
|
+
//#endregion
|
820
|
+
//#region src/sim/OpenGraphSimulation.ts
|
821
|
+
var OpenGraphSimulation = class {
|
822
|
+
width;
|
823
|
+
height;
|
824
|
+
config;
|
825
|
+
rootImageSources;
|
826
|
+
canvas;
|
827
|
+
transformCanvas;
|
828
|
+
theme;
|
829
|
+
data = {
|
830
|
+
nodes: [],
|
831
|
+
links: []
|
832
|
+
};
|
833
|
+
prunedData = {
|
834
|
+
nodes: [],
|
835
|
+
links: []
|
836
|
+
};
|
837
|
+
subGraph = {
|
838
|
+
nodes: [],
|
839
|
+
links: []
|
840
|
+
};
|
841
|
+
rootId = "";
|
842
|
+
simulation = null;
|
843
|
+
clusterSizeRange = [0, 1];
|
844
|
+
imageCache = new Map();
|
845
|
+
rootImages = [];
|
846
|
+
hideThumbnails = false;
|
847
|
+
noInteraction = false;
|
848
|
+
selectedNode = null;
|
849
|
+
onSelectedNodeChange;
|
850
|
+
hoveredNode = null;
|
851
|
+
onHoveredNodeChange;
|
852
|
+
highlights = [];
|
853
|
+
constructor(props) {
|
854
|
+
this.theme = props.theme || "light";
|
855
|
+
this.width = props.width;
|
856
|
+
this.height = props.height;
|
857
|
+
this.config = props.config || DEFAULT_GRAPH_CONFIG;
|
858
|
+
this.rootImageSources = props.rootImageSources || [];
|
859
|
+
this.canvas = props.canvas;
|
860
|
+
this.onHoveredNodeChange = props.onHoveredNodeChange;
|
861
|
+
this.onSelectedNodeChange = props.onSelectedNodeChange;
|
862
|
+
this.transformCanvas = new TransformCanvas(this.canvas, {
|
863
|
+
onUpdate: this.onDraw,
|
864
|
+
onClick: this.handleClick,
|
865
|
+
onMove: this.handleMove
|
866
|
+
});
|
867
|
+
this.rootImageSources.forEach((src, idx) => {
|
868
|
+
if (src && !this.imageCache.get(src)) loadImage(src).then((img$1) => {
|
869
|
+
this.imageCache.set(src, img$1);
|
870
|
+
this.rootImages[idx] = img$1;
|
871
|
+
});
|
872
|
+
});
|
873
|
+
}
|
874
|
+
getNodeAtPosition = (cx, cy) => {
|
875
|
+
const transform = this.transformCanvas.transform;
|
876
|
+
const { x: tx, y: ty, scale } = transform;
|
877
|
+
const x = (cx - tx) / scale;
|
878
|
+
const y = (cy - ty) / scale;
|
879
|
+
for (let node of this.data.nodes) {
|
880
|
+
const r = this.getNodeSize(node.id) / 2;
|
881
|
+
if (node.x == null || node.y == null) continue;
|
882
|
+
const dx = node.x - x;
|
883
|
+
const dy = node.y - y;
|
884
|
+
if (dx * dx + dy * dy < r * r) return node;
|
885
|
+
}
|
886
|
+
return null;
|
887
|
+
};
|
888
|
+
getNodeScreenPosition = (node) => {
|
889
|
+
const _x = node.x || 0;
|
890
|
+
const _y = node.y || 0;
|
891
|
+
const transform = this.transformCanvas.transform;
|
892
|
+
const x = this.width / 2 - _x * transform.scale;
|
893
|
+
const y = this.height / 2 - _y * transform.scale;
|
894
|
+
return {
|
895
|
+
x,
|
896
|
+
y
|
897
|
+
};
|
898
|
+
};
|
899
|
+
handleClick = (x, y) => {
|
900
|
+
const node = this.getNodeAtPosition(x, y);
|
901
|
+
this.handleClickNode(node);
|
902
|
+
};
|
903
|
+
handleClickNode = (node) => {
|
904
|
+
if (node) {
|
905
|
+
if (node.id === this.rootId) {
|
906
|
+
this.selectedNode = null;
|
907
|
+
this.onSelectedNodeChange?.(null);
|
908
|
+
this.subGraph = {
|
909
|
+
nodes: [],
|
910
|
+
links: []
|
911
|
+
};
|
912
|
+
return;
|
913
|
+
}
|
914
|
+
if (node.state) {
|
915
|
+
const children = getChildren(node.id, this.data.links);
|
916
|
+
if (children.length > 0) {
|
917
|
+
if (this.selectedNode?.id !== node.id) node.state.collapsed = false;
|
918
|
+
else node.state.collapsed = !node.state.collapsed;
|
919
|
+
if (!node.state.collapsed) children.forEach((childId) => {
|
920
|
+
const childNode = this.data.nodes.find((n) => n.id === childId);
|
921
|
+
if (childNode && isSimNode(childNode)) {
|
922
|
+
if (!childNode.x || childNode.x === 0) childNode.x = (node.x || this.width / 2) + Math.random() * 50 - 5;
|
923
|
+
if (!childNode.y || childNode.y === 0) childNode.y = (node.y || this.height / 2) + Math.random() * 50 - 5;
|
924
|
+
}
|
925
|
+
});
|
926
|
+
}
|
927
|
+
}
|
928
|
+
if (!node.state?.collapsed) {
|
929
|
+
const parents = getParents(node.id, this.data.links);
|
930
|
+
parents.forEach((parentId) => {
|
931
|
+
const parentNode = this.data.nodes.find((n) => n.id === parentId);
|
932
|
+
if (parentNode && isSimNode(parentNode) && parentNode.state) parentNode.state.collapsed = false;
|
933
|
+
});
|
934
|
+
}
|
935
|
+
this.restart();
|
936
|
+
this.subGraph = getNodeSubgraph(node.id, this.data.nodes, this.data.links, this.rootId);
|
937
|
+
const nodePos = this.getNodeScreenPosition(node);
|
938
|
+
this.transformCanvas.transformTo({
|
939
|
+
x: nodePos.x,
|
940
|
+
y: nodePos.y
|
941
|
+
});
|
942
|
+
this.transformCanvas.focusOn(() => {
|
943
|
+
const nodePos$1 = this.getNodeScreenPosition(node);
|
944
|
+
return {
|
945
|
+
x: nodePos$1.x,
|
946
|
+
y: nodePos$1.y,
|
947
|
+
scale: this.transformCanvas.transform.scale
|
948
|
+
};
|
949
|
+
});
|
950
|
+
}
|
951
|
+
if (this.selectedNode?.id !== node?.id) {
|
952
|
+
this.selectedNode = node;
|
953
|
+
this.onSelectedNodeChange?.(node);
|
954
|
+
}
|
955
|
+
};
|
956
|
+
handleMove = (x, y) => {
|
957
|
+
const node = this.getNodeAtPosition(x, y);
|
958
|
+
if (this.hoveredNode === node) return;
|
959
|
+
this.hoveredNode = node;
|
960
|
+
this.onHoveredNodeChange?.(node);
|
961
|
+
this.canvas.style.cursor = node ? "pointer" : "default";
|
962
|
+
this.onDraw();
|
963
|
+
};
|
964
|
+
initialize = (data, rootId) => {
|
965
|
+
this.rootId = rootId;
|
966
|
+
const _links = data.links.map((l) => ({ ...l }));
|
967
|
+
const _nodes = data.nodes.map((n) => {
|
968
|
+
const existingData = this.data.nodes.find((x) => x.id === n.id);
|
969
|
+
return {
|
970
|
+
...n,
|
971
|
+
state: { collapsed: hasOnlyLeafs(n.id, _links) && getChildren(n.id, _links).length > 1 },
|
972
|
+
clusterSize: getClusterSize(n.id, _links),
|
973
|
+
x: existingData?.x || this.width / 2 + Math.random() * 200 - 5,
|
974
|
+
y: existingData?.y || this.height / 2 + Math.random() * 200 - 5
|
975
|
+
};
|
976
|
+
});
|
977
|
+
if (!data.nodes.find((n) => n.id === rootId)) {
|
978
|
+
_nodes.push({
|
979
|
+
id: this.rootId,
|
980
|
+
state: {
|
981
|
+
collapsed: false,
|
982
|
+
image: void 0
|
983
|
+
},
|
984
|
+
clusterSize: 1,
|
985
|
+
x: this.width / 2,
|
986
|
+
y: this.height / 2
|
987
|
+
});
|
988
|
+
const targetIds = new Set(_links.map((link) => link.target));
|
989
|
+
const rootNodes = _nodes.filter((node) => !targetIds.has(node.id));
|
990
|
+
for (const node of rootNodes) _links.push({
|
991
|
+
source: rootId,
|
992
|
+
target: node.id
|
993
|
+
});
|
994
|
+
}
|
995
|
+
this.data = {
|
996
|
+
nodes: _nodes,
|
997
|
+
links: _links
|
998
|
+
};
|
999
|
+
this.loadNodeImages();
|
1000
|
+
this.restart();
|
1001
|
+
};
|
1002
|
+
restart = () => {
|
1003
|
+
this.setSelectedNode(null);
|
1004
|
+
this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links);
|
1005
|
+
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]);
|
1006
|
+
this.simulation = forceSimulation(this.prunedData.nodes).force("link", forceLink(this.prunedData.links).id((d) => d.id).distance((l) => {
|
1007
|
+
if (isSimNode(l.target) && !l.target?.state?.collapsed) return this.config.nodeSize;
|
1008
|
+
return this.config.nodeSize * 3;
|
1009
|
+
}).strength((l) => {
|
1010
|
+
return .6;
|
1011
|
+
})).force("charge", forceManyBody().strength(() => {
|
1012
|
+
return -130;
|
1013
|
+
})).force("center", forceCenter(this.width / 2, this.height / 2).strength(.01));
|
1014
|
+
this.simulation.on("tick", this.onDraw);
|
1015
|
+
this.simulation.on("end", this.onEnd);
|
1016
|
+
};
|
1017
|
+
get visiblityScale() {
|
1018
|
+
return scaleLog().domain(this.clusterSizeRange).range([3, 1.5]).clamp(true);
|
1019
|
+
}
|
1020
|
+
get color() {
|
1021
|
+
return color(this.theme === "light" ? this.config.theme.dark : this.config.theme.light);
|
1022
|
+
}
|
1023
|
+
get colorContrast() {
|
1024
|
+
return color(this.theme === "light" ? this.config.theme.light : this.config.theme.dark);
|
1025
|
+
}
|
1026
|
+
getNodeSize = (nodeId) => {
|
1027
|
+
const { nodeSize } = this.config;
|
1028
|
+
if (nodeId === this.rootId) return nodeSize * 2;
|
1029
|
+
const node = this.data.nodes.find((n) => n.id === nodeId);
|
1030
|
+
const isCollapsed = !!node?.state?.collapsed;
|
1031
|
+
if (isCollapsed) {
|
1032
|
+
const scale = scaleLinear().domain(this.clusterSizeRange).range([nodeSize, nodeSize * 3]);
|
1033
|
+
return scale(node.clusterSize || 1);
|
1034
|
+
}
|
1035
|
+
const isSelected = this.selectedNode?.id === nodeId;
|
1036
|
+
return isSelected ? nodeSize * 2 : nodeSize;
|
1037
|
+
};
|
1038
|
+
onDraw = () => {
|
1039
|
+
const context = this.canvas?.getContext("2d");
|
1040
|
+
const transform = this.transformCanvas.transform;
|
1041
|
+
if (!context) return;
|
1042
|
+
const dpi = devicePixelRatio || 1;
|
1043
|
+
context.save();
|
1044
|
+
context.scale(dpi, dpi);
|
1045
|
+
context.clearRect(0, 0, this.width, this.height);
|
1046
|
+
context.translate(transform.x, transform.y);
|
1047
|
+
context.scale(transform.scale, transform.scale);
|
1048
|
+
context.save();
|
1049
|
+
const isLight = this.theme === "light";
|
1050
|
+
this.prunedData.links.forEach((l) => {
|
1051
|
+
const _dim = !!this.selectedNode && !this.subGraph.links.find((sl) => getNodeId(sl.source) === getNodeId(l.source) && getNodeId(sl.target) === getNodeId(l.target));
|
1052
|
+
const stroke = _dim ? this.color(dim(.09, isLight))() : this.color(dim(.18, isLight))();
|
1053
|
+
context.globalAlpha = .5;
|
1054
|
+
context.strokeStyle = stroke;
|
1055
|
+
context.lineWidth = _dim ? .3 : .8;
|
1056
|
+
context.beginPath();
|
1057
|
+
const sx = isSimNode(l.source) && l.source.x || 0;
|
1058
|
+
const sy = isSimNode(l.source) && l.source.y || 0;
|
1059
|
+
context.moveTo(sx, sy);
|
1060
|
+
const tx = isSimNode(l.target) && l.target.x || 0;
|
1061
|
+
const ty = isSimNode(l.target) && l.target.y || 0;
|
1062
|
+
context.lineTo(tx, ty);
|
1063
|
+
context.stroke();
|
1064
|
+
context.closePath();
|
1065
|
+
});
|
1066
|
+
context.globalAlpha = 1;
|
1067
|
+
this.prunedData.nodes.forEach((node) => {
|
1068
|
+
const x = node.x || 0;
|
1069
|
+
const y = node.y || 0;
|
1070
|
+
const isSelected = this.selectedNode?.id === node.id;
|
1071
|
+
const isHovered = this.hoveredNode?.id === node.id;
|
1072
|
+
const isCollapsed = !!node.state?.collapsed;
|
1073
|
+
const _dim = !!this.selectedNode && !this.subGraph?.nodes.some((n) => n.id === node.id);
|
1074
|
+
const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
|
1075
|
+
const stroke = this.colorContrast();
|
1076
|
+
const blue = [
|
1077
|
+
94,
|
1078
|
+
112,
|
1079
|
+
235
|
1080
|
+
];
|
1081
|
+
const red = [
|
1082
|
+
238,
|
1083
|
+
125,
|
1084
|
+
121
|
1085
|
+
];
|
1086
|
+
const highlightedStroke = _dim ? color(red)(dim(.4, isLight))() : color(red)();
|
1087
|
+
const size = this.getNodeSize(node.id);
|
1088
|
+
const highlighted = this.highlights.includes(node.id);
|
1089
|
+
if (node.id === this.rootId) {
|
1090
|
+
circle(context, x, y, size / 2, {
|
1091
|
+
stroke: false,
|
1092
|
+
strokeStyle: stroke,
|
1093
|
+
lineWidth: isSelected ? 1 : .2,
|
1094
|
+
fill: true,
|
1095
|
+
fillStyle: this.color(dim(.18, isLight))()
|
1096
|
+
});
|
1097
|
+
if (this.rootImages) {
|
1098
|
+
const _idx = Math.min(isLight ? 0 : 1, this.rootImages.length - 1);
|
1099
|
+
const _img = this.rootImages[_idx];
|
1100
|
+
const _imgSize = size * .55;
|
1101
|
+
if (_img) img(context, _img, x - _imgSize / 2, y - _imgSize / 2, _imgSize, _imgSize, 0, _dim ? .1 : 1);
|
1102
|
+
}
|
1103
|
+
} else if (isCollapsed) {
|
1104
|
+
if (highlighted) {
|
1105
|
+
const _size = size + 4;
|
1106
|
+
circle(context, x, y, _size / 2, {
|
1107
|
+
stroke: true,
|
1108
|
+
strokeStyle: highlightedStroke,
|
1109
|
+
lineWidth: 1,
|
1110
|
+
fill: false,
|
1111
|
+
fillStyle: fill
|
1112
|
+
});
|
1113
|
+
}
|
1114
|
+
circle(context, x, y, size / 2, {
|
1115
|
+
stroke: true,
|
1116
|
+
strokeStyle: stroke,
|
1117
|
+
lineWidth: isSelected ? 1 : .2,
|
1118
|
+
fill: true,
|
1119
|
+
fillStyle: fill
|
1120
|
+
});
|
1121
|
+
const showLabel = transform.scale >= this.visiblityScale(node.clusterSize || 1) ? 1 : 0;
|
1122
|
+
if (showLabel) {
|
1123
|
+
context.font = `${14 / transform.scale}px Sans-Serif`;
|
1124
|
+
context.textAlign = "center";
|
1125
|
+
context.textBaseline = "middle";
|
1126
|
+
context.fillStyle = this.color(dim(_dim ? .2 : .5, isLight))();
|
1127
|
+
context.fillText((node.clusterSize || 1).toString(), x, y);
|
1128
|
+
}
|
1129
|
+
} else {
|
1130
|
+
if (highlighted) {
|
1131
|
+
const _size = size + 4;
|
1132
|
+
rect(context, x - _size / 2, y - _size / 2, _size, _size, {
|
1133
|
+
stroke: true,
|
1134
|
+
strokeStyle: highlightedStroke,
|
1135
|
+
lineWidth: 1,
|
1136
|
+
fill: false,
|
1137
|
+
fillStyle: fill,
|
1138
|
+
borderRadius: 1
|
1139
|
+
});
|
1140
|
+
}
|
1141
|
+
rect(context, x - size / 2, y - size / 2, size, size, {
|
1142
|
+
stroke: true,
|
1143
|
+
strokeStyle: stroke,
|
1144
|
+
lineWidth: isSelected ? .3 : .2,
|
1145
|
+
fill: true,
|
1146
|
+
fillStyle: fill,
|
1147
|
+
borderRadius: 1
|
1148
|
+
});
|
1149
|
+
if (node.state?.image && !this.hideThumbnails) {
|
1150
|
+
const _size = size - 1;
|
1151
|
+
img(context, node.state?.image, x - _size / 2, y - _size / 2, _size, _size, 1, _dim ? .1 : 1);
|
1152
|
+
}
|
1153
|
+
}
|
1154
|
+
});
|
1155
|
+
context.restore();
|
1156
|
+
context.restore();
|
1157
|
+
this.transformCanvas.trackCursor();
|
1158
|
+
};
|
1159
|
+
onEnd = () => {};
|
1160
|
+
loadNodeImages = () => {
|
1161
|
+
this.data.nodes.forEach((node) => {
|
1162
|
+
if (node.imgSrc && !this.imageCache.get(node.imgSrc)) loadImage(node.imgSrc).then((img$1) => {
|
1163
|
+
this.imageCache.set(node.imgSrc, img$1);
|
1164
|
+
node.state = node.state || {};
|
1165
|
+
node.state.image = img$1;
|
1166
|
+
});
|
1167
|
+
});
|
1168
|
+
};
|
1169
|
+
destroy = () => {
|
1170
|
+
this.simulation?.stop();
|
1171
|
+
this.simulation?.on("tick", null);
|
1172
|
+
this.simulation?.on("end", null);
|
1173
|
+
this.transformCanvas.destroy();
|
1174
|
+
};
|
1175
|
+
resize = (width, height) => {
|
1176
|
+
this.width = width;
|
1177
|
+
this.height = height;
|
1178
|
+
this.onDraw();
|
1179
|
+
};
|
1180
|
+
setTheme = (theme) => {
|
1181
|
+
this.theme = theme;
|
1182
|
+
this.onDraw();
|
1183
|
+
};
|
1184
|
+
setHideThumbnails = (hide) => {
|
1185
|
+
this.hideThumbnails = hide;
|
1186
|
+
this.onDraw();
|
1187
|
+
};
|
1188
|
+
setSelectedNode = (node) => {
|
1189
|
+
this.selectedNode = node;
|
1190
|
+
this.prunedData.nodes.sort((a, b) => {
|
1191
|
+
if (a.id === this.selectedNode?.id) return 1;
|
1192
|
+
if (b.id === this.selectedNode?.id) return -1;
|
1193
|
+
return 0;
|
1194
|
+
});
|
1195
|
+
this.onDraw();
|
1196
|
+
};
|
1197
|
+
setHighlights = (highlights) => {
|
1198
|
+
this.highlights = highlights;
|
1199
|
+
this.onDraw();
|
1200
|
+
};
|
1201
|
+
setNoInteraction = (noInteraction) => {
|
1202
|
+
this.noInteraction = noInteraction;
|
1203
|
+
this.transformCanvas.setNoInteraction(this.noInteraction);
|
1204
|
+
this.onDraw();
|
1205
|
+
};
|
1206
|
+
getNodeById = (nodeId) => {
|
1207
|
+
return this.data.nodes.find((n) => n.id === nodeId) || null;
|
1208
|
+
};
|
1209
|
+
};
|
1210
|
+
|
1211
|
+
//#endregion
|
1212
|
+
//#region src/components/OpenFormGraph.tsx
|
1213
|
+
function OpenFormGraph(props) {
|
1214
|
+
const { width, height, highlights = [], className, noInteraction = false } = props;
|
1215
|
+
const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode } = useOpenFormGraph();
|
1216
|
+
const canvasRef = useRef(null);
|
1217
|
+
useEffect(() => {
|
1218
|
+
if (!canvasRef.current) return;
|
1219
|
+
simulation.current = new OpenGraphSimulation({
|
1220
|
+
width,
|
1221
|
+
height,
|
1222
|
+
canvas: canvasRef.current,
|
1223
|
+
rootImageSources,
|
1224
|
+
theme,
|
1225
|
+
onHoveredNodeChange: (n) => {
|
1226
|
+
setHoveredNode(n);
|
1227
|
+
},
|
1228
|
+
onSelectedNodeChange: (n) => {
|
1229
|
+
setSelectedNode(n);
|
1230
|
+
}
|
1231
|
+
});
|
1232
|
+
return () => {
|
1233
|
+
simulation.current?.destroy();
|
1234
|
+
};
|
1235
|
+
}, []);
|
1236
|
+
useEffect(() => {
|
1237
|
+
if (!simulation.current) return;
|
1238
|
+
simulation.current.resize(width, height);
|
1239
|
+
}, [width, height]);
|
1240
|
+
useEffect(() => {
|
1241
|
+
if (!simulation.current || !theme) return;
|
1242
|
+
simulation.current.setTheme(theme);
|
1243
|
+
}, [theme]);
|
1244
|
+
useEffect(() => {
|
1245
|
+
if (!simulation.current) return;
|
1246
|
+
simulation.current.setHideThumbnails(hideThumbnails);
|
1247
|
+
}, [hideThumbnails]);
|
1248
|
+
useEffect(() => {
|
1249
|
+
if (!simulation.current) return;
|
1250
|
+
simulation.current.setHighlights(highlights);
|
1251
|
+
}, [highlights]);
|
1252
|
+
useEffect(() => {
|
1253
|
+
if (!simulation.current) return;
|
1254
|
+
simulation.current.setNoInteraction(noInteraction);
|
1255
|
+
}, [noInteraction]);
|
1256
|
+
useEffect(() => {
|
1257
|
+
if (!simulation.current) return;
|
1258
|
+
simulation.current.initialize(data, rootId);
|
1259
|
+
}, [data]);
|
1260
|
+
const dpi = devicePixelRatio || 1;
|
1261
|
+
return /* @__PURE__ */ jsx("canvas", {
|
1262
|
+
onMouseEnter: props.onMouseEnter,
|
1263
|
+
onMouseLeave: props.onMouseLeave,
|
1264
|
+
ref: canvasRef,
|
1265
|
+
className,
|
1266
|
+
width: `${width * dpi}px`,
|
1267
|
+
height: `${height * dpi}px`,
|
1268
|
+
style: {
|
1269
|
+
width: `${width}px`,
|
1270
|
+
height: `${height}px`
|
1271
|
+
}
|
1272
|
+
});
|
1273
|
+
}
|
1274
|
+
|
1275
|
+
//#endregion
|
1276
|
+
export { OpenFormGraph, buildTreeFromGraphData, circle, color, dim, generateTree, getChildren, getClusterSize, getNodeId, getNodeSubgraph, getParents, getPrunedData, hasOnlyLeafs, hexagon, img, isSimLink, isSimNode, loadImage, rect, searchParents };
|
1277
|
+
//# sourceMappingURL=OpenFormGraph-wo_Y20C6.js.map
|