@plait/graph-viz 0.62.0-next.10

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.
Files changed (44) hide show
  1. package/README.md +4 -0
  2. package/constants/default.d.ts +2 -0
  3. package/esm2022/constants/default.mjs +5 -0
  4. package/esm2022/force-atlas/constants.mjs +34 -0
  5. package/esm2022/force-atlas/core/node-icon-base.component.mjs +15 -0
  6. package/esm2022/force-atlas/edge.flavour.mjs +25 -0
  7. package/esm2022/force-atlas/force-atlas.flavour.mjs +53 -0
  8. package/esm2022/force-atlas/generators/edge.generator.mjs +30 -0
  9. package/esm2022/force-atlas/generators/node.generator.mjs +44 -0
  10. package/esm2022/force-atlas/node.flavour.mjs +55 -0
  11. package/esm2022/force-atlas/types.mjs +7 -0
  12. package/esm2022/force-atlas/utils/draw.mjs +73 -0
  13. package/esm2022/force-atlas/utils/edge.mjs +72 -0
  14. package/esm2022/force-atlas/utils/node.mjs +55 -0
  15. package/esm2022/force-atlas/with-force-atlas.mjs +71 -0
  16. package/esm2022/force-atlas/with-node-icon.mjs +8 -0
  17. package/esm2022/interfaces/element.mjs +12 -0
  18. package/esm2022/interfaces/index.mjs +2 -0
  19. package/esm2022/perfect-arrows/get-arrow.mjs +91 -0
  20. package/esm2022/perfect-arrows/utils.mjs +111 -0
  21. package/esm2022/plait-graph-viz.mjs +5 -0
  22. package/esm2022/public-api.mjs +10 -0
  23. package/fesm2022/plait-graph-viz.mjs +724 -0
  24. package/fesm2022/plait-graph-viz.mjs.map +1 -0
  25. package/force-atlas/constants.d.ts +25 -0
  26. package/force-atlas/core/node-icon-base.component.d.ts +10 -0
  27. package/force-atlas/edge.flavour.d.ts +15 -0
  28. package/force-atlas/force-atlas.flavour.d.ts +13 -0
  29. package/force-atlas/generators/edge.generator.d.ts +15 -0
  30. package/force-atlas/generators/node.generator.d.ts +15 -0
  31. package/force-atlas/node.flavour.d.ts +15 -0
  32. package/force-atlas/types.d.ts +33 -0
  33. package/force-atlas/utils/draw.d.ts +13 -0
  34. package/force-atlas/utils/edge.d.ts +14 -0
  35. package/force-atlas/utils/node.d.ts +15 -0
  36. package/force-atlas/with-force-atlas.d.ts +2 -0
  37. package/force-atlas/with-node-icon.d.ts +13 -0
  38. package/index.d.ts +5 -0
  39. package/interfaces/element.d.ts +20 -0
  40. package/interfaces/index.d.ts +1 -0
  41. package/package.json +34 -0
  42. package/perfect-arrows/get-arrow.d.ts +42 -0
  43. package/perfect-arrows/utils.d.ts +64 -0
  44. package/public-api.d.ts +6 -0
@@ -0,0 +1,724 @@
1
+ import { cacheSelectedElements, PlaitBoard, normalizePoint, drawCircle, NS, createG, createPath, PlaitElement, PlaitNode, getSelectedElements, RectangleClient, createForeignObject, setSelectionOptions } from '@plait/core';
2
+ import { CommonElementFlavour, Generator, animate, linear } from '@plait/common';
3
+ import Graph from 'graphology';
4
+ import circular from 'graphology-layout/circular';
5
+ import forceAtlas2 from 'graphology-layout-forceatlas2';
6
+
7
+ const DEFAULT_STYLES = {
8
+ fillStyle: 'solid',
9
+ strokeWidth: 1
10
+ };
11
+
12
+ var EdgeDirection;
13
+ (function (EdgeDirection) {
14
+ EdgeDirection[EdgeDirection["IN"] = 0] = "IN";
15
+ EdgeDirection[EdgeDirection["OUT"] = 1] = "OUT";
16
+ EdgeDirection[EdgeDirection["NONE"] = 2] = "NONE";
17
+ })(EdgeDirection || (EdgeDirection = {}));
18
+
19
+ const DEFAULT_EDGE_STYLES = {
20
+ ...DEFAULT_STYLES,
21
+ stroke: '#ddd'
22
+ };
23
+ const DEFAULT_NODE_SIZE = 30;
24
+ const DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER = 1.2;
25
+ const DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER = 1.5;
26
+ const DEFAULT_NODE_LABEL_MARGIN_TOP = 4;
27
+ const DEFAULT_NODE_LABEL_FONT_SIZE = 12;
28
+ const SECOND_DEPTH_NODE_ALPHA = 0.5;
29
+ const SECOND_DEPTH_LINE_ALPHA = 0.5;
30
+ const ACTIVE_BACKGROUND_NODE_ALPHA = 0.1;
31
+ const NODE_ICON_CLASS_NAME = 'force-atlas-node-icon';
32
+ const ACTIVE_NODE_ICON_CLASS_NAME = 'force-atlas-node-icon-active';
33
+ const NODE_ICON_FONT_SIZE = 16;
34
+ const ACTIVE_NODE_ICON_FONT_SIZE = 18;
35
+ const DEFAULT_NODE_BACKGROUND_COLOR = '#9c9cfb';
36
+ const DEFAULT_NODE_ICON_COLOR = '#fff';
37
+ const DEFAULT_NODE_STYLES = {
38
+ ...DEFAULT_STYLES,
39
+ fill: DEFAULT_NODE_BACKGROUND_COLOR,
40
+ strokeWidth: 0
41
+ };
42
+ const DEFAULT_NODE_SCALING_RATIO = 20;
43
+ const DEFAULT_LINE_STYLES = {
44
+ color: {
45
+ [EdgeDirection.IN]: '#73D897',
46
+ [EdgeDirection.OUT]: '#6698FF',
47
+ [EdgeDirection.NONE]: `#ddd`
48
+ }
49
+ };
50
+
51
+ const ForceAtlasElement = {
52
+ isForceAtlas: (value) => {
53
+ return value?.type === 'force-atlas';
54
+ },
55
+ isForceAtlasNodeElement: (value) => {
56
+ return value && value.label && value.icon;
57
+ },
58
+ isForceAtlasEdgeElement: (value) => {
59
+ return value && value.source && value.target;
60
+ }
61
+ };
62
+
63
+ class ForceAtlasFlavour extends CommonElementFlavour {
64
+ constructor() {
65
+ super();
66
+ }
67
+ initializeGraph() {
68
+ this.graph = new Graph();
69
+ this.element.children?.forEach(child => {
70
+ if (ForceAtlasElement.isForceAtlasNodeElement(child)) {
71
+ if (typeof child?.size === 'undefined') {
72
+ child.size = DEFAULT_NODE_SIZE;
73
+ }
74
+ if (child.isActive) {
75
+ cacheSelectedElements(this.board, [child]);
76
+ }
77
+ this.graph.addNode(child.id, child);
78
+ }
79
+ else if (ForceAtlasElement.isForceAtlasEdgeElement(child)) {
80
+ this.graph.addEdge(child.source, child.target);
81
+ }
82
+ });
83
+ circular.assign(this.graph);
84
+ const settings = forceAtlas2.inferSettings(this.graph);
85
+ settings.strongGravityMode = false;
86
+ settings.linLogMode = true;
87
+ settings.gravity = 2;
88
+ settings.adjustSizes = true;
89
+ settings.scalingRatio = DEFAULT_NODE_SCALING_RATIO;
90
+ const positions = forceAtlas2(this.graph, { iterations: 1000, settings });
91
+ this.element.children?.forEach(child => {
92
+ if (ForceAtlasElement.isForceAtlasNodeElement(child)) {
93
+ const pos = positions[child.id];
94
+ child.points = [[pos.x, pos.y]];
95
+ }
96
+ });
97
+ }
98
+ initialize() {
99
+ super.initialize();
100
+ this.initializeGraph();
101
+ }
102
+ onContextChanged(value, previous) { }
103
+ updateText(previousElement, currentElement) { }
104
+ destroy() {
105
+ super.destroy();
106
+ }
107
+ }
108
+
109
+ // Credits to perfect-arrows
110
+ // https://github.com/steveruizok/perfect-arrows/blob/master/src/lib/utils.ts
111
+ const PI = Math.PI;
112
+ /**
113
+ * Modulate a value between two ranges.
114
+ * @param value
115
+ * @param rangeA from [low, high]
116
+ * @param rangeB to [low, high]
117
+ * @param clamp
118
+ */
119
+ function modulate(value, rangeA, rangeB, clamp = false) {
120
+ const [fromLow, fromHigh] = rangeA;
121
+ const [toLow, toHigh] = rangeB;
122
+ const result = toLow + ((value - fromLow) / (fromHigh - fromLow)) * (toHigh - toLow);
123
+ if (clamp === true) {
124
+ if (toLow < toHigh) {
125
+ if (result < toLow) {
126
+ return toLow;
127
+ }
128
+ if (result > toHigh) {
129
+ return toHigh;
130
+ }
131
+ }
132
+ else {
133
+ if (result > toLow) {
134
+ return toLow;
135
+ }
136
+ if (result < toHigh) {
137
+ return toHigh;
138
+ }
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ /**
144
+ * Rotate a point around a center.
145
+ * @param x The x-axis coordinate of the point.
146
+ * @param y The y-axis coordinate of the point.
147
+ * @param cx The x-axis coordinate of the point to rotate round.
148
+ * @param cy The y-axis coordinate of the point to rotate round.
149
+ * @param angle The distance (in radians) to rotate.
150
+ */
151
+ function rotatePoint(x, y, cx, cy, angle) {
152
+ const s = Math.sin(angle);
153
+ const c = Math.cos(angle);
154
+ const px = x - cx;
155
+ const py = y - cy;
156
+ const nx = px * c - py * s;
157
+ const ny = px * s + py * c;
158
+ return [nx + cx, ny + cy];
159
+ }
160
+ /**
161
+ * Get the distance between two points.
162
+ * @param x0 The x-axis coordinate of the first point.
163
+ * @param y0 The y-axis coordinate of the first point.
164
+ * @param x1 The x-axis coordinate of the second point.
165
+ * @param y1 The y-axis coordinate of the second point.
166
+ */
167
+ function getDistance(x0, y0, x1, y1) {
168
+ return Math.hypot(y1 - y0, x1 - x0);
169
+ }
170
+ /**
171
+ * Get an angle (radians) between two points.
172
+ * @param x0 The x-axis coordinate of the first point.
173
+ * @param y0 The y-axis coordinate of the first point.
174
+ * @param x1 The x-axis coordinate of the second point.
175
+ * @param y1 The y-axis coordinate of the second point.
176
+ */
177
+ function getAngle(x0, y0, x1, y1) {
178
+ return Math.atan2(y1 - y0, x1 - x0);
179
+ }
180
+ /**
181
+ * Move a point in an angle by a distance.
182
+ * @param x0
183
+ * @param y0
184
+ * @param a angle (radians)
185
+ * @param d distance
186
+ */
187
+ function projectPoint(x0, y0, a, d) {
188
+ return [Math.cos(a) * d + x0, Math.sin(a) * d + y0];
189
+ }
190
+ /**
191
+ * Get a point between two points.
192
+ * @param x0 The x-axis coordinate of the first point.
193
+ * @param y0 The y-axis coordinate of the first point.
194
+ * @param x1 The x-axis coordinate of the second point.
195
+ * @param y1 The y-axis coordinate of the second point.
196
+ * @param d Normalized
197
+ */
198
+ function getPointBetween(x0, y0, x1, y1, d = 0.5) {
199
+ return [x0 + (x1 - x0) * d, y0 + (y1 - y0) * d];
200
+ }
201
+ /**
202
+ * Get the sector of an angle (e.g. quadrant, octant)
203
+ * @param a The angle to check.
204
+ * @param s The number of sectors to check.
205
+ */
206
+ function getSector(a, s = 8) {
207
+ return Math.floor(s * (0.5 + ((a / (PI * 2)) % s)));
208
+ }
209
+ /**
210
+ * Get a normal value representing how close two points are from being at a 45 degree angle.
211
+ * @param x0 The x-axis coordinate of the first point.
212
+ * @param y0 The y-axis coordinate of the first point.
213
+ * @param x1 The x-axis coordinate of the second point.
214
+ * @param y1 The y-axis coordinate of the second point.
215
+ */
216
+ function getAngliness(x0, y0, x1, y1) {
217
+ return Math.abs((x1 - x0) / 2 / ((y1 - y0) / 2));
218
+ }
219
+
220
+ // Credits to perfect-arrows
221
+ // https://github.com/steveruizok/perfect-arrows/blob/master/src/lib/getArrow.ts
222
+ /**
223
+ * getArrow
224
+ * Get the points for a linking line between two points.
225
+ * @description Draw an arrow between two points.
226
+ * @param x0 The x position of the "from" point.
227
+ * @param y0 The y position of the "from" point.
228
+ * @param x1 The x position of the "to" point.
229
+ * @param y1 The y position of the "to" point.
230
+ * @param options Additional options for computing the line.
231
+ * @returns [sx, sy, cx, cy, e1, e2, ae, as, ac]
232
+ * @example
233
+ * const arrow = getArrow(0, 0, 100, 200, {
234
+ bow: 0
235
+ stretch: .5
236
+ stretchMin: 0
237
+ stretchMax: 420
238
+ padStart: 0
239
+ padEnd: 0
240
+ flip: false
241
+ straights: true
242
+ * })
243
+ *
244
+ * const [
245
+ * startX, startY,
246
+ * controlX, controlY,
247
+ * endX, endY,
248
+ * endAngle,
249
+ * startAngle,
250
+ * controlAngle
251
+ * ] = arrow
252
+ */
253
+ function getArrow(x0, y0, x1, y1, options = {}) {
254
+ const { bow = 0, stretch = 0.5, stretchMin = 0, stretchMax = 420, padStart = 0, padEnd = 0, flip = false, straights = true } = options;
255
+ const angle = getAngle(x0, y0, x1, y1);
256
+ const dist = getDistance(x0, y0, x1, y1);
257
+ const angliness = getAngliness(x0, y0, x1, y1);
258
+ // Step 0 ⤜⤏ Should the arrow be straight?
259
+ if (dist < (padStart + padEnd) * 2 || // Too short
260
+ (bow === 0 && stretch === 0) || // No bow, no stretch
261
+ (straights && [0, 1, Infinity].includes(angliness)) // 45 degree angle
262
+ ) {
263
+ // ⤜⤏ Arrow is straight! Just pad start and end points.
264
+ // Padding distances
265
+ const ps = Math.max(0, Math.min(dist - padStart, padStart));
266
+ const pe = Math.max(0, Math.min(dist - ps, padEnd));
267
+ // Move start point toward end point
268
+ let [px0, py0] = projectPoint(x0, y0, angle, ps);
269
+ // Move end point toward start point
270
+ let [px1, py1] = projectPoint(x1, y1, angle + Math.PI, pe);
271
+ // Get midpoint between new points
272
+ const [mx, my] = getPointBetween(px0, py0, px1, py1, 0.5);
273
+ return [px0, py0, mx, my, px1, py1, angle, angle, angle];
274
+ }
275
+ // ⤜⤏ Arrow is an arc!
276
+ // Is the arc clockwise or counterclockwise?
277
+ let rot = (getSector(angle) % 2 === 0 ? 1 : -1) * (flip ? -1 : 1);
278
+ // Calculate how much the line should "bow" away from center
279
+ const arc = bow + modulate(dist, [stretchMin, stretchMax], [1, 0], true) * stretch;
280
+ // Step 1 ⤜⤏ Find padded points.
281
+ // Get midpoint.
282
+ const [mx, my] = getPointBetween(x0, y0, x1, y1, 0.5);
283
+ // Get control point.
284
+ let [cx, cy] = getPointBetween(x0, y0, x1, y1, 0.5 - arc);
285
+ // Rotate control point (clockwise or counterclockwise).
286
+ [cx, cy] = rotatePoint(cx, cy, mx, my, (Math.PI / 2) * rot);
287
+ // Get padded start point.
288
+ const a0 = getAngle(x0, y0, cx, cy);
289
+ const [px0, py0] = projectPoint(x0, y0, a0, padStart);
290
+ // Get padded end point.
291
+ const a1 = getAngle(x1, y1, cx, cy);
292
+ const [px1, py1] = projectPoint(x1, y1, a1, padEnd);
293
+ // Step 2 ⤜⤏ Find start and end angles.
294
+ // Start angle
295
+ const as = getAngle(cx, cy, x0, y0);
296
+ // End angle
297
+ const ae = getAngle(cx, cy, x1, y1);
298
+ // Step 3 ⤜⤏ Find control point for padded points.
299
+ // Get midpoint between padded start / end points.
300
+ const [mx1, my1] = getPointBetween(px0, py0, px1, py1, 0.5);
301
+ // Get control point for padded start / end points.
302
+ let [cx1, cy1] = getPointBetween(px0, py0, px1, py1, 0.5 - arc);
303
+ // Rotate control point (clockwise or counterclockwise).
304
+ [cx1, cy1] = rotatePoint(cx1, cy1, mx1, my1, (Math.PI / 2) * rot);
305
+ // Finally, average the two control points.
306
+ let [cx2, cy2] = getPointBetween(cx, cy, cx1, cy1, 0.5);
307
+ return [px0, py0, cx2, cy2, px1, py1, ae, as, angle];
308
+ }
309
+
310
+ function drawNode(board, node, point, options) {
311
+ const roughSVG = PlaitBoard.getRoughSVG(board);
312
+ const nodeStyles = {
313
+ ...DEFAULT_NODE_STYLES,
314
+ ...(node.styles || {})
315
+ };
316
+ let { x, y } = normalizePoint(point);
317
+ let diameter = node.size ?? DEFAULT_NODE_SIZE;
318
+ if (options.isActive) {
319
+ diameter = diameter * DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER;
320
+ }
321
+ const nodeG = drawCircle(roughSVG, [x, y], diameter, nodeStyles);
322
+ if (options.iconG) {
323
+ nodeG.append(options.iconG);
324
+ }
325
+ const text = document.createElementNS(NS, 'text');
326
+ text.textContent = node.label || '';
327
+ text.setAttribute('text-anchor', `middle`);
328
+ text.setAttribute('dominant-baseline', `hanging`);
329
+ text.setAttribute('x', `${x}`);
330
+ text.setAttribute('font-size', `${DEFAULT_NODE_LABEL_FONT_SIZE}px`);
331
+ text.setAttribute('style', `user-select: none;`);
332
+ if (options.isActive) {
333
+ const waveDiameter = diameter * DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER;
334
+ const waveCircle = drawCircle(roughSVG, [x, y], waveDiameter, nodeStyles);
335
+ waveCircle.setAttribute('opacity', ACTIVE_BACKGROUND_NODE_ALPHA.toString());
336
+ nodeG.append(waveCircle);
337
+ text.setAttribute('y', `${y + waveDiameter / 2 + DEFAULT_NODE_LABEL_MARGIN_TOP}`);
338
+ }
339
+ else {
340
+ if (!options.isFirstDepth) {
341
+ nodeG.setAttribute('opacity', SECOND_DEPTH_NODE_ALPHA.toString());
342
+ }
343
+ text.setAttribute('y', `${y + diameter / 2 + DEFAULT_NODE_LABEL_MARGIN_TOP}`);
344
+ }
345
+ nodeG.append(text);
346
+ return nodeG;
347
+ }
348
+ function drawEdge(startPoint, endPoint, direction, isMutual) {
349
+ const arrow = getArrow(startPoint[0], startPoint[1], endPoint[0], endPoint[1], {
350
+ stretch: 0.4,
351
+ flip: direction === EdgeDirection.NONE ? false : isMutual,
352
+ padEnd: DEFAULT_NODE_SIZE / 2,
353
+ padStart: DEFAULT_NODE_SIZE / 2
354
+ });
355
+ const [sx, sy, cx, cy, ex, ey, ae, as, ec] = arrow;
356
+ const g = createG();
357
+ const path = createPath();
358
+ path.setAttribute('d', `M${sx},${sy} Q${cx},${cy} ${ex},${ey}`);
359
+ path.setAttribute('fill', 'none');
360
+ path.setAttribute('stroke', DEFAULT_LINE_STYLES.color[direction]);
361
+ g.append(path);
362
+ return {
363
+ g,
364
+ path
365
+ };
366
+ }
367
+ function drawParticle(board, startPoint, direction) {
368
+ const roughSVG = PlaitBoard.getRoughSVG(board);
369
+ const pointG = drawCircle(roughSVG, [0, 0], 5, {
370
+ ...DEFAULT_STYLES,
371
+ strokeWidth: 0,
372
+ fill: DEFAULT_LINE_STYLES.color[direction]
373
+ });
374
+ pointG.setAttribute('transform', `translate(${startPoint[0]}, ${startPoint[1]})`);
375
+ return pointG;
376
+ }
377
+
378
+ class ForceAtlasEdgeGenerator extends Generator {
379
+ static { this.key = 'force-atlas-edge'; }
380
+ constructor(board) {
381
+ super(board);
382
+ }
383
+ canDraw(element) {
384
+ return true;
385
+ }
386
+ draw(element, data) {
387
+ const edgeG = createG();
388
+ const edgeElement = drawEdge(data.startPoint, data.endPoint, data.direction, data.isSourceActive && data.isTargetActive);
389
+ edgeG.append(edgeElement.g);
390
+ if (data.direction !== EdgeDirection.NONE) {
391
+ const particle = drawParticle(this.board, data.startPoint, data.direction);
392
+ edgeElement.g.append(particle);
393
+ this.particleAnimation = playEdgeParticleAnimate(edgeElement.path, particle);
394
+ }
395
+ return edgeG;
396
+ }
397
+ destroy() {
398
+ super.destroy();
399
+ this.particleAnimation?.stop();
400
+ }
401
+ }
402
+
403
+ function getEdges(forceAtlasElement, andCallBack) {
404
+ return forceAtlasElement.children?.filter(f => ForceAtlasElement.isForceAtlasEdgeElement(f) && (andCallBack?.(f) ?? true));
405
+ }
406
+ function getEdgeById(id, forceAtlasElement) {
407
+ const edge = getEdges(forceAtlasElement, e => e.id === id)?.[0];
408
+ if (!edge) {
409
+ throw new Error('can not find edge.');
410
+ }
411
+ return edge;
412
+ }
413
+ function getEdgesInSourceOrTarget(id, forceAtlasElement) {
414
+ const edges = getEdges(forceAtlasElement, edge => edge.source === id || edge.target === id);
415
+ return edges;
416
+ }
417
+ function getEdgeGenerator(edge) {
418
+ const edgeRef = PlaitElement.getElementRef(edge);
419
+ return edgeRef.getGenerator(ForceAtlasEdgeGenerator.key);
420
+ }
421
+ function getEdgeDirection(isSourceActive, isTargetActive) {
422
+ if (isSourceActive) {
423
+ return EdgeDirection.OUT;
424
+ }
425
+ else if (isTargetActive) {
426
+ return EdgeDirection.IN;
427
+ }
428
+ return EdgeDirection.NONE;
429
+ }
430
+ function getEdgeGeneratorData(edge, board) {
431
+ const forceAtlasElement = PlaitNode.parent(board, PlaitBoard.findPath(board, edge));
432
+ const sourceNode = getNodeById(edge.source, forceAtlasElement);
433
+ const targetNode = getNodeById(edge.target, forceAtlasElement);
434
+ if (!sourceNode?.points || !targetNode?.points) {
435
+ throw new Error("Source or target node doesn't have points");
436
+ }
437
+ const startPoint = sourceNode.points[0];
438
+ const endPoint = targetNode.points[0];
439
+ const selectElements = getSelectedElements(board);
440
+ const isSourceActive = getIsNodeActive(sourceNode.id, selectElements);
441
+ const isTargetActive = getIsNodeActive(targetNode.id, selectElements);
442
+ const direction = getEdgeDirection(isSourceActive, isTargetActive);
443
+ return {
444
+ startPoint,
445
+ endPoint,
446
+ direction,
447
+ isSourceActive,
448
+ isTargetActive
449
+ };
450
+ }
451
+ function playEdgeParticleAnimate(path, pointG) {
452
+ const pathLength = path.getTotalLength();
453
+ let anim = animate((t) => {
454
+ const point = path.getPointAtLength(t * pathLength);
455
+ pointG.setAttribute('transform', `translate(${point.x}, ${point.y})`);
456
+ }, 1000, linear, () => {
457
+ anim = playEdgeParticleAnimate(path, pointG);
458
+ });
459
+ return {
460
+ stop: () => {
461
+ anim.stop();
462
+ },
463
+ start: () => {
464
+ anim.start();
465
+ }
466
+ };
467
+ }
468
+
469
+ function getNodes(forceAtlasElement, andBack) {
470
+ return forceAtlasElement.children?.filter(f => ForceAtlasElement.isForceAtlasNodeElement(f) && (andBack?.(f) ?? true));
471
+ }
472
+ function getNodeById(id, forceAtlasElement) {
473
+ const node = getNodes(forceAtlasElement, node => node.id === id)?.[0];
474
+ if (!node) {
475
+ throw new Error('can not find node.');
476
+ }
477
+ return node;
478
+ }
479
+ function getIsNodeActive(id, selectElements) {
480
+ return selectElements.some(node => node.id === id);
481
+ }
482
+ function isHitNode(node, point) {
483
+ const { x, y } = normalizePoint(node.points[0]);
484
+ const size = node.size;
485
+ const hitFlowNode = RectangleClient.isHit(RectangleClient.getRectangleByPoints(point), {
486
+ x: x - size / 2,
487
+ y: y - size / 2,
488
+ width: size,
489
+ height: size
490
+ });
491
+ return hitFlowNode;
492
+ }
493
+ function getAssociatedNodesById(id, forceAtlasElement) {
494
+ const edges = getEdgesInSourceOrTarget(id, forceAtlasElement);
495
+ const nodes = [];
496
+ edges.forEach(edge => {
497
+ nodes.push(getNodeById(edge.source, forceAtlasElement));
498
+ nodes.push(getNodeById(edge.target, forceAtlasElement));
499
+ });
500
+ return nodes;
501
+ }
502
+ function getNodeGenerator(node) {
503
+ const edgeRef = PlaitElement.getElementRef(node);
504
+ return edgeRef.getGenerator(ForceAtlasNodeGenerator.key);
505
+ }
506
+ function isFirstDepthNode(currentNodeId, activeNodeId, forceAtlasElement) {
507
+ const edges = getEdges(forceAtlasElement);
508
+ return edges.some(s => (s.source === activeNodeId && s.target === currentNodeId) || (s.target === activeNodeId && s.source === currentNodeId));
509
+ }
510
+ function getNodeIcon(node) {
511
+ const iconItem = typeof node.icon === 'object' && node.icon.name ? node.icon : null;
512
+ return {
513
+ name: iconItem ? iconItem.name : node.icon,
514
+ fontSize: (iconItem && iconItem.fontSize) || NODE_ICON_FONT_SIZE,
515
+ color: (iconItem && iconItem.color) || DEFAULT_NODE_ICON_COLOR
516
+ };
517
+ }
518
+
519
+ class ForceAtlasNodeGenerator extends Generator {
520
+ static { this.key = 'force-atlas-node'; }
521
+ constructor(board) {
522
+ super(board);
523
+ }
524
+ canDraw(element) {
525
+ return true;
526
+ }
527
+ draw(element, data) {
528
+ const iconRef = this.drawIcon(element, data);
529
+ return drawNode(this.board, element, element?.points?.[0] || [0, 0], { ...data, iconG: iconRef.iconG });
530
+ }
531
+ drawIcon(element, data) {
532
+ const iconG = createG();
533
+ let { x, y } = normalizePoint(element.points?.[0] || [0, 0]);
534
+ const size = element.size;
535
+ const foreignObject = createForeignObject(x - size / 2, y - size / 2, size, size);
536
+ iconG.append(foreignObject);
537
+ const container = document.createElement('div');
538
+ container.classList.add(NODE_ICON_CLASS_NAME);
539
+ if (data.isActive) {
540
+ container.classList.add(ACTIVE_NODE_ICON_CLASS_NAME);
541
+ }
542
+ foreignObject.append(container);
543
+ const nodeIcon = getNodeIcon(element);
544
+ const props = {
545
+ iconItem: {
546
+ name: nodeIcon.name,
547
+ fontSize: data.isActive ? ACTIVE_NODE_ICON_FONT_SIZE : nodeIcon.fontSize,
548
+ color: nodeIcon.color
549
+ },
550
+ board: this.board,
551
+ element: element
552
+ };
553
+ const ref = this.board.renderNodeIcon(container, props);
554
+ return { ref, iconG };
555
+ }
556
+ }
557
+
558
+ class ForceAtlasNodeFlavour extends CommonElementFlavour {
559
+ constructor() {
560
+ super();
561
+ }
562
+ initializeGenerator() {
563
+ this.nodeGenerator = new ForceAtlasNodeGenerator(this.board);
564
+ this.getRef().addGenerator(ForceAtlasNodeGenerator.key, this.nodeGenerator);
565
+ }
566
+ initialize() {
567
+ super.initialize();
568
+ this.initializeGenerator();
569
+ const parent = PlaitNode.parent(this.board, PlaitBoard.findPath(this.board, this.element));
570
+ const selectElements = getSelectedElements(this.board);
571
+ const activeNodeId = selectElements[0]?.id;
572
+ const isActive = activeNodeId === this.element.id;
573
+ this.nodeGenerator.processDrawing(this.element, isActive ? PlaitBoard.getElementActiveHost(this.board) : this.getElementG(), {
574
+ isActive,
575
+ isFirstDepth: isFirstDepthNode(this.element.id, activeNodeId, parent)
576
+ });
577
+ }
578
+ onContextChanged(value, previous) {
579
+ if (value !== previous && value.selected !== previous.selected) {
580
+ const parent = value.parent;
581
+ if (value.selected) {
582
+ cacheSelectedElements(this.board, [value.element]);
583
+ }
584
+ const selectElements = getSelectedElements(this.board);
585
+ const associatedNodes = getAssociatedNodesById(value.element.id, parent);
586
+ associatedNodes.forEach(node => {
587
+ const nodeGenerator = getNodeGenerator(node);
588
+ nodeGenerator.destroy();
589
+ nodeGenerator.processDrawing(node, this.getElementG(), {
590
+ isActive: selectElements?.[0]?.id === node.id,
591
+ isFirstDepth: selectElements.length > 0 && isFirstDepthNode(node.id, selectElements[0].id, parent)
592
+ });
593
+ });
594
+ const associatedEdges = getEdgesInSourceOrTarget(value.element.id, parent);
595
+ associatedEdges.forEach(edge => {
596
+ const edgeGenerator = getEdgeGenerator(edge);
597
+ edgeGenerator.destroy();
598
+ edgeGenerator.processDrawing(edge, PlaitBoard.getElementLowerHost(this.board), getEdgeGeneratorData(edge, this.board));
599
+ });
600
+ }
601
+ }
602
+ updateText(previousElement, currentElement) { }
603
+ destroy() {
604
+ super.destroy();
605
+ }
606
+ }
607
+
608
+ class ForceAtlasEdgeFlavour extends CommonElementFlavour {
609
+ constructor() {
610
+ super();
611
+ }
612
+ initializeGenerator() {
613
+ this.edgeGenerator = new ForceAtlasEdgeGenerator(this.board);
614
+ this.getRef().addGenerator(ForceAtlasEdgeGenerator.key, this.edgeGenerator);
615
+ }
616
+ initialize() {
617
+ super.initialize();
618
+ this.initializeGenerator();
619
+ this.edgeGenerator.processDrawing(this.element, PlaitBoard.getElementLowerHost(this.board), getEdgeGeneratorData(this.element, this.board));
620
+ }
621
+ onContextChanged(value, previous) { }
622
+ updateText(previousElement, currentElement) { }
623
+ destroy() {
624
+ super.destroy();
625
+ this.edgeGenerator.destroy();
626
+ }
627
+ }
628
+
629
+ const withNodeIcon = (board) => {
630
+ const newBoard = board;
631
+ newBoard.renderNodeIcon = (container, props) => {
632
+ throw new Error('No implementation for renderLabeIcon method.');
633
+ };
634
+ return newBoard;
635
+ };
636
+
637
+ const withForceAtlas = (board) => {
638
+ const { drawElement, getRectangle, isRectangleHit, isHit, isInsidePoint, isMovable, isAlign, getRelatedFragment } = board;
639
+ board.drawElement = (context) => {
640
+ if (ForceAtlasElement.isForceAtlas(context.element)) {
641
+ return ForceAtlasFlavour;
642
+ }
643
+ else if (ForceAtlasElement.isForceAtlasNodeElement(context.element)) {
644
+ return ForceAtlasNodeFlavour;
645
+ }
646
+ else if (ForceAtlasElement.isForceAtlasEdgeElement(context.element)) {
647
+ return ForceAtlasEdgeFlavour;
648
+ }
649
+ return drawElement(context);
650
+ };
651
+ board.getRectangle = (element) => {
652
+ if (element.type === 'force-atlas') {
653
+ return {
654
+ width: 0,
655
+ height: 0,
656
+ x: 0,
657
+ y: 0
658
+ };
659
+ }
660
+ else if (ForceAtlasElement.isForceAtlasNodeElement(element)) {
661
+ return RectangleClient.getRectangleByPoints(element.points || []);
662
+ }
663
+ else if (ForceAtlasElement.isForceAtlasEdgeElement(element)) {
664
+ return {
665
+ width: 0,
666
+ height: 0,
667
+ x: 0,
668
+ y: 0
669
+ };
670
+ }
671
+ return getRectangle(element);
672
+ };
673
+ board.isRectangleHit = (element, selection) => {
674
+ return isRectangleHit(element, selection);
675
+ };
676
+ board.isRectangleHit = (element, range) => {
677
+ if (ForceAtlasElement.isForceAtlasNodeElement(element)) {
678
+ return isHitNode(element, [range.anchor, range.focus]);
679
+ }
680
+ return isRectangleHit(element, range);
681
+ };
682
+ board.isHit = (element, point) => {
683
+ if (ForceAtlasElement.isForceAtlasNodeElement(element)) {
684
+ return isHitNode(element, [point, point]);
685
+ }
686
+ return isHit(element, point);
687
+ };
688
+ board.isInsidePoint = (element, point) => {
689
+ return isInsidePoint(element, point);
690
+ };
691
+ board.isMovable = element => {
692
+ if (ForceAtlasElement.isForceAtlasNodeElement(element)) {
693
+ return true;
694
+ }
695
+ return isMovable(element);
696
+ };
697
+ setSelectionOptions(board, { isMultipleSelection: false });
698
+ return withNodeIcon(board);
699
+ };
700
+
701
+ class ForceAtlasNodeIconBaseComponent {
702
+ initialize() {
703
+ if (!this.iconItem.fontSize) {
704
+ this.iconItem.fontSize = NODE_ICON_FONT_SIZE;
705
+ }
706
+ if (!this.iconItem.color) {
707
+ this.iconItem.color = DEFAULT_NODE_ICON_COLOR;
708
+ }
709
+ this.nativeElement().style.fontSize = `${this.iconItem.fontSize}px`;
710
+ this.nativeElement().style.color = `${this.iconItem.color}`;
711
+ this.nativeElement().classList.add(NODE_ICON_CLASS_NAME);
712
+ }
713
+ }
714
+
715
+ /*
716
+ * Public API Surface of utils
717
+ */
718
+
719
+ /**
720
+ * Generated bundle index. Do not edit.
721
+ */
722
+
723
+ export { ACTIVE_BACKGROUND_NODE_ALPHA, ACTIVE_NODE_ICON_CLASS_NAME, ACTIVE_NODE_ICON_FONT_SIZE, DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER, DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER, DEFAULT_EDGE_STYLES, DEFAULT_LINE_STYLES, DEFAULT_NODE_BACKGROUND_COLOR, DEFAULT_NODE_ICON_COLOR, DEFAULT_NODE_LABEL_FONT_SIZE, DEFAULT_NODE_LABEL_MARGIN_TOP, DEFAULT_NODE_SCALING_RATIO, DEFAULT_NODE_SIZE, DEFAULT_NODE_STYLES, EdgeDirection, ForceAtlasElement, ForceAtlasNodeIconBaseComponent, NODE_ICON_CLASS_NAME, NODE_ICON_FONT_SIZE, SECOND_DEPTH_LINE_ALPHA, SECOND_DEPTH_NODE_ALPHA, withForceAtlas, withNodeIcon };
724
+ //# sourceMappingURL=plait-graph-viz.mjs.map