@krainovsd/graph 0.13.0-beta1 → 0.14.0-beta-7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/lib/cjs/index.cjs +1297 -606
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/esm/constants/force-controls.js +4 -2
  4. package/lib/esm/constants/force-controls.js.map +1 -1
  5. package/lib/esm/constants/highlight-controls.js +9 -16
  6. package/lib/esm/constants/highlight-controls.js.map +1 -1
  7. package/lib/esm/constants/link-controls.js +17 -30
  8. package/lib/esm/constants/link-controls.js.map +1 -1
  9. package/lib/esm/constants/node-controls.js +4 -2
  10. package/lib/esm/constants/node-controls.js.map +1 -1
  11. package/lib/esm/index.js +8 -7
  12. package/lib/esm/index.js.map +1 -1
  13. package/lib/esm/module/GraphCanvas/GraphCanvas.js +305 -81
  14. package/lib/esm/module/GraphCanvas/GraphCanvas.js.map +1 -1
  15. package/lib/esm/module/GraphCanvas/constants/force-settings.js +11 -1
  16. package/lib/esm/module/GraphCanvas/constants/force-settings.js.map +1 -1
  17. package/lib/esm/module/GraphCanvas/constants/graph-settings.js +17 -5
  18. package/lib/esm/module/GraphCanvas/constants/graph-settings.js.map +1 -1
  19. package/lib/esm/module/GraphCanvas/constants/highlight-settings.js +5 -3
  20. package/lib/esm/module/GraphCanvas/constants/highlight-settings.js.map +1 -1
  21. package/lib/esm/module/GraphCanvas/constants/index.js +5 -2
  22. package/lib/esm/module/GraphCanvas/constants/index.js.map +1 -1
  23. package/lib/esm/module/GraphCanvas/constants/link-settings.js +19 -9
  24. package/lib/esm/module/GraphCanvas/constants/link-settings.js.map +1 -1
  25. package/lib/esm/module/GraphCanvas/constants/node-settings.js +13 -3
  26. package/lib/esm/module/GraphCanvas/constants/node-settings.js.map +1 -1
  27. package/lib/esm/module/GraphCanvas/lib/settings/force-settings-getter.js +4 -2
  28. package/lib/esm/module/GraphCanvas/lib/settings/force-settings-getter.js.map +1 -1
  29. package/lib/esm/module/GraphCanvas/lib/settings/graph-settings-getter.js +4 -0
  30. package/lib/esm/module/GraphCanvas/lib/settings/graph-settings-getter.js.map +1 -1
  31. package/lib/esm/module/GraphCanvas/lib/settings/highlight-settings-getter.js +4 -2
  32. package/lib/esm/module/GraphCanvas/lib/settings/highlight-settings-getter.js.map +1 -1
  33. package/lib/esm/module/GraphCanvas/lib/settings/link-settings-getter.js +8 -5
  34. package/lib/esm/module/GraphCanvas/lib/settings/link-settings-getter.js.map +1 -1
  35. package/lib/esm/module/GraphCanvas/lib/settings/node-settings-getter.js +14 -2
  36. package/lib/esm/module/GraphCanvas/lib/settings/node-settings-getter.js.map +1 -1
  37. package/lib/esm/module/GraphCanvas/lib/utils/calculate-curve-link-position-by-node.js +45 -0
  38. package/lib/esm/module/GraphCanvas/lib/utils/calculate-curve-link-position-by-node.js.map +1 -0
  39. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-node.js +42 -37
  40. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-node.js.map +1 -1
  41. package/lib/esm/module/GraphCanvas/lib/utils/compute-graph-bounds.js +26 -0
  42. package/lib/esm/module/GraphCanvas/lib/utils/compute-graph-bounds.js.map +1 -0
  43. package/lib/esm/module/GraphCanvas/lib/utils/get-particle-position.js +40 -30
  44. package/lib/esm/module/GraphCanvas/lib/utils/get-particle-position.js.map +1 -1
  45. package/lib/esm/module/GraphCanvas/lib/utils/is-empty-object.js +11 -0
  46. package/lib/esm/module/GraphCanvas/lib/utils/is-empty-object.js.map +1 -0
  47. package/lib/esm/module/GraphCanvas/lib/utils/is-node-visible.js +5 -2
  48. package/lib/esm/module/GraphCanvas/lib/utils/is-node-visible.js.map +1 -1
  49. package/lib/esm/module/GraphCanvas/lib/utils/link-by-pointer-getter.js +72 -26
  50. package/lib/esm/module/GraphCanvas/lib/utils/link-by-pointer-getter.js.map +1 -1
  51. package/lib/esm/module/GraphCanvas/lib/utils/node-by-pointer-getter.js +1 -5
  52. package/lib/esm/module/GraphCanvas/lib/utils/node-by-pointer-getter.js.map +1 -1
  53. package/lib/esm/module/GraphCanvas/lib/utils/pointer-getter.js +16 -8
  54. package/lib/esm/module/GraphCanvas/lib/utils/pointer-getter.js.map +1 -1
  55. package/lib/esm/module/GraphCanvas/slices/draw-links.js +115 -77
  56. package/lib/esm/module/GraphCanvas/slices/draw-links.js.map +1 -1
  57. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js +9 -4
  58. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js.map +1 -1
  59. package/lib/esm/module/GraphCanvas/slices/init-area.js +1 -1
  60. package/lib/esm/module/GraphCanvas/slices/init-area.js.map +1 -1
  61. package/lib/esm/module/GraphCanvas/slices/init-dnd.js +20 -13
  62. package/lib/esm/module/GraphCanvas/slices/init-dnd.js.map +1 -1
  63. package/lib/esm/module/GraphCanvas/slices/init-draw.js +14 -52
  64. package/lib/esm/module/GraphCanvas/slices/init-draw.js.map +1 -1
  65. package/lib/esm/module/GraphCanvas/slices/init-pointer.js +51 -72
  66. package/lib/esm/module/GraphCanvas/slices/init-pointer.js.map +1 -1
  67. package/lib/esm/module/GraphCanvas/slices/init-resize.js +11 -0
  68. package/lib/esm/module/GraphCanvas/slices/init-resize.js.map +1 -1
  69. package/lib/esm/module/GraphCanvas/slices/init-selection.js +152 -0
  70. package/lib/esm/module/GraphCanvas/slices/init-selection.js.map +1 -0
  71. package/lib/esm/module/GraphCanvas/slices/init-simulation.js +12 -2
  72. package/lib/esm/module/GraphCanvas/slices/init-simulation.js.map +1 -1
  73. package/lib/esm/module/GraphCanvas/slices/init-zoom.js +88 -16
  74. package/lib/esm/module/GraphCanvas/slices/init-zoom.js.map +1 -1
  75. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js +63 -3
  76. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js.map +1 -1
  77. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js +61 -16
  78. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js.map +1 -1
  79. package/lib/index.d.ts +127 -62
  80. package/package.json +3 -2
package/lib/cjs/index.cjs CHANGED
@@ -4,11 +4,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
5
  const jsHelpers = require('@krainovsd/js-helpers');
6
6
  const d3Force = require('d3-force');
7
+ const d3Selection = require('d3-selection');
7
8
  const d3Zoom = require('d3-zoom');
8
9
  const d3Array = require('d3-array');
9
- const d3Selection = require('d3-selection');
10
10
  const d3Drag = require('d3-drag');
11
- const lodash = require('lodash');
12
11
 
13
12
  const COMMON_SETTINGS = {
14
13
  nodeRadius: 5,
@@ -23,6 +22,10 @@ const FORCE_SETTINGS = {
23
22
  xForce: true,
24
23
  yForce: true,
25
24
  collideForce: true,
25
+ precompute: true,
26
+ precomputeMaxTicks: 300,
27
+ precomputeMaxTimeMs: 200,
28
+ precomputeDisableForcesAfter: false,
26
29
  centerPosition: {},
27
30
  centerStrength: 1,
28
31
  collideStrength: 0.1,
@@ -40,6 +43,12 @@ const FORCE_SETTINGS = {
40
43
  linkStrength: 1,
41
44
  collideRadius: null,
42
45
  };
46
+ const PERFORMANCE_FORCE_SETTINGS = {
47
+ ...FORCE_SETTINGS,
48
+ collideIterations: 1,
49
+ precomputeMaxTimeMs: 350,
50
+ precomputeDisableForcesAfter: true,
51
+ };
43
52
 
44
53
  function forceSettingsGetter(settings, prevSettings) {
45
54
  return {
@@ -59,17 +68,18 @@ function linkSettingsGetter(settings, prevSettings) {
59
68
  return { ...(prevSettings ?? LINK_SETTINGS), ...settings };
60
69
  }
61
70
  function linkOptionsGetter() {
71
+ const aboveScale = this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch;
62
72
  return {
63
73
  ...LINK_OPTIONS,
64
74
  drawExtraLink: null,
65
75
  drawLink: null,
66
- color: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
76
+ color: aboveScale
67
77
  ? this.linkSettings.linkColorAfterScaleSwitch
68
78
  : this.linkSettings.linkColorBeforeScaleSwitch,
69
- arrowColor: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
79
+ arrowColor: aboveScale
70
80
  ? this.linkSettings.linkColorAfterScaleSwitch
71
81
  : this.linkSettings.linkColorBeforeScaleSwitch,
72
- width: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
82
+ width: aboveScale
73
83
  ? this.linkSettings.linkWidthAfterScaleSwitch
74
84
  : this.linkSettings.linkWidthBeforeScaleSwitch,
75
85
  };
@@ -185,11 +195,20 @@ function resetDrawTime() {
185
195
  }
186
196
 
187
197
  function nodeSettingsGetter(settings, prevNodeSettings) {
188
- return {
198
+ const result = {
189
199
  ...(prevNodeSettings ?? NODE_SETTINGS),
190
200
  idGetter: nodeIdGetter,
191
201
  ...settings,
192
202
  };
203
+ if (result.smartCache && (!result.textScaleSteps || result.textScaleSteps.length === 0)) {
204
+ const step = (result.textScaleMax - result.textScaleMin) / 9;
205
+ const steps = [];
206
+ for (let i = 0; i < 10; i++) {
207
+ steps.push(result.textScaleMin + i * step);
208
+ }
209
+ result.textScaleSteps = steps;
210
+ }
211
+ return result;
193
212
  }
194
213
  const color = colorGetter();
195
214
  function nodeOptionsGetter(node) {
@@ -271,15 +290,47 @@ function highlightSettingsGetter(settings, prevSettings) {
271
290
  };
272
291
  }
273
292
 
293
+ function computeGraphBounds(nodes) {
294
+ if (!nodes.length)
295
+ return null;
296
+ let minX = Infinity;
297
+ let minY = Infinity;
298
+ let maxX = -Infinity;
299
+ let maxY = -Infinity;
300
+ for (const node of nodes) {
301
+ if (node.x === null || node.x === undefined || node.y === null || node.y === undefined)
302
+ continue;
303
+ if (node.visible === false)
304
+ continue;
305
+ let radius = node._radius;
306
+ radius ??= Math.max(node._width ?? 0, node._height ?? 0) / 2;
307
+ minX = Math.min(minX, node.x - radius);
308
+ minY = Math.min(minY, node.y - radius);
309
+ maxX = Math.max(maxX, node.x + radius);
310
+ maxY = Math.max(maxY, node.y + radius);
311
+ }
312
+ if (!isFinite(minX))
313
+ return null;
314
+ return { minX, minY, maxX, maxY };
315
+ }
316
+
274
317
  function pointerGetter(mouseEvent, areaRect, areaTransform) {
275
- const clientX = "clientX" in mouseEvent
276
- ? mouseEvent.clientX
277
- : (mouseEvent.touches[0]?.clientX ?? mouseEvent.changedTouches[0]?.clientX);
278
- const clientY = "clientX" in mouseEvent
279
- ? mouseEvent.clientY
280
- : (mouseEvent.touches[0]?.clientY ?? mouseEvent.changedTouches[0]?.clientY);
281
- const px = (clientX - areaRect.left - areaTransform.x) / areaTransform.k;
282
- const py = (clientY - areaRect.top - areaTransform.y) / areaTransform.k;
318
+ let localX;
319
+ let localY;
320
+ if ("offsetX" in mouseEvent) {
321
+ localX = mouseEvent.offsetX;
322
+ localY = mouseEvent.offsetY;
323
+ }
324
+ else {
325
+ if (!areaRect)
326
+ return [0, 0];
327
+ const clientX = mouseEvent.touches[0]?.clientX ?? mouseEvent.changedTouches[0]?.clientX;
328
+ const clientY = mouseEvent.touches[0]?.clientY ?? mouseEvent.changedTouches[0]?.clientY;
329
+ localX = clientX - areaRect.left;
330
+ localY = clientY - areaRect.top;
331
+ }
332
+ const px = (localX - areaTransform.x) / areaTransform.k;
333
+ const py = (localY - areaTransform.y) / areaTransform.k;
283
334
  return [px, py];
284
335
  }
285
336
 
@@ -379,10 +430,7 @@ function linkIterationExtractor(link, i, links, state, option, optionConstantGet
379
430
  return customOptions;
380
431
  }
381
432
 
382
- function nodeByPointerGetter({ areaRect, areaTransform, mouseEvent, nodes, }) {
383
- if (!areaRect)
384
- return undefined;
385
- const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
433
+ function nodeByPointerGetter({ pointerX, pointerY, nodes, }) {
386
434
  return d3Array.greatest(nodes, (node) => {
387
435
  if (isOverlapsNode(node, pointerX, pointerY, undefined))
388
436
  return node.index;
@@ -421,50 +469,52 @@ function nodeIterationExtractor(node, i, nodes, state, option, optionConstantGet
421
469
  return customOptions;
422
470
  }
423
471
 
424
- function calculateLinkPositionByNode(link, arrowSize = 0) {
425
- const source = link.source;
426
- const target = link.target;
427
- if (typeof source != "object" || typeof target != "object")
428
- return null;
472
+ function calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, arrowSize = 0) {
429
473
  const sourcePoint = getLinkPoint({
474
+ xStart,
475
+ yStart,
476
+ xEnd,
477
+ yEnd,
430
478
  arrowSize: 0,
431
- node: source,
432
- oppositeNode: target,
433
- });
479
+ node: sourceNode});
434
480
  const targetPoint = getLinkPoint({
435
- arrowSize: arrowSize > 0 ? arrowSize * 0.85 : 0,
436
- node: target,
437
- oppositeNode: source,
438
- });
439
- if (!sourcePoint || !targetPoint)
440
- return null;
481
+ xStart: xEnd,
482
+ yStart: yEnd,
483
+ xEnd: xStart,
484
+ yEnd: yStart,
485
+ arrowSize: arrowSize > 0 ? arrowSize * 0.9 : 0,
486
+ node: targetNode});
487
+ const targetPointArrow = getLinkPoint({
488
+ xStart: xEnd,
489
+ yStart: yEnd,
490
+ xEnd: xStart,
491
+ yEnd: yStart,
492
+ arrowSize: 0,
493
+ node: targetNode});
441
494
  const distance = targetPoint && sourcePoint
442
495
  ? Math.sqrt((targetPoint.x - sourcePoint.x) ** 2 + (targetPoint.y - sourcePoint.y) ** 2)
443
496
  : 0;
444
497
  return {
445
- x1: sourcePoint.x,
446
- y1: sourcePoint.y,
447
- x2: targetPoint.x,
448
- y2: targetPoint.y,
498
+ xStart: sourcePoint.x,
499
+ yStart: sourcePoint.y,
500
+ xEnd: targetPoint.x,
501
+ yEnd: targetPoint.y,
502
+ xEndArrow: targetPointArrow.x,
503
+ yEndArrow: targetPointArrow.y,
449
504
  distance,
450
505
  };
451
506
  }
452
507
  function getLinkPoint(opts) {
453
- if (opts.node.x == undefined ||
454
- opts.node.y == undefined ||
455
- opts.oppositeNode.x == undefined ||
456
- opts.oppositeNode.y == undefined)
457
- return null;
458
508
  let nodePoint;
459
509
  switch (opts.node._shape) {
460
510
  case "circle": {
461
511
  nodePoint = getCircleIntersection({
462
- x: opts.node.x,
463
- y: opts.node.y,
512
+ x: opts.xStart,
513
+ y: opts.yStart,
464
514
  radius: (opts.node._radius ?? COMMON_SETTINGS.nodeRadius) +
465
515
  (opts.node._borderWidth ? opts.node._borderWidth / 2 : 0),
466
- oppositeX: opts.oppositeNode.x,
467
- oppositeY: opts.oppositeNode.y,
516
+ oppositeX: opts.xEnd,
517
+ oppositeY: opts.yEnd,
468
518
  arrowSize: opts.arrowSize,
469
519
  });
470
520
  break;
@@ -473,23 +523,23 @@ function getLinkPoint(opts) {
473
523
  case "text": {
474
524
  nodePoint = getRectangleIntersection({
475
525
  arrowSize: opts.arrowSize,
476
- x: opts.node.x,
477
- y: opts.node.y,
526
+ x: opts.xStart,
527
+ y: opts.yStart,
478
528
  height: (opts.node._height ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
479
529
  width: (opts.node._width ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
480
- oppositeX: opts.oppositeNode.x,
481
- oppositeY: opts.oppositeNode.y,
530
+ oppositeX: opts.xEnd,
531
+ oppositeY: opts.yEnd,
482
532
  borderRadius: opts.node._shape === "text" ? 0 : (opts.node._borderRadius ?? 0),
483
533
  });
484
534
  break;
485
535
  }
486
536
  default: {
487
537
  nodePoint = getCircleIntersection({
488
- x: opts.node.x,
489
- y: opts.node.y,
538
+ x: opts.xStart,
539
+ y: opts.yStart,
490
540
  radius: opts.node._radius ?? COMMON_SETTINGS.nodeRadius,
491
- oppositeX: opts.oppositeNode.x,
492
- oppositeY: opts.oppositeNode.y,
541
+ oppositeX: opts.xEnd,
542
+ oppositeY: opts.yEnd,
493
543
  arrowSize: opts.arrowSize,
494
544
  });
495
545
  }
@@ -733,68 +783,78 @@ function isNodeVisible(opts) {
733
783
  }
734
784
  }
735
785
 
786
+ const FRAME_INTERVAL = 1000 / 60;
736
787
  function getParticlePosition(opts) {
737
- const prevStepDifference = opts.particle.step - (opts.particle.prev?.step ?? 0);
738
- const nextStepDifference = (opts.particle.next?.step ?? 0) - opts.particle.step;
739
- const needWait = opts.particle.next &&
740
- opts.particle.next.step > opts.particle.step &&
741
- nextStepDifference <= opts.distance;
742
- const needSpeed = opts.particle.prev &&
743
- opts.particle.prev.step !== 0 &&
744
- opts.particle.prev.step < opts.particle.step &&
745
- prevStepDifference < opts.distance;
746
- if (opts.particle.step === 0 && needWait) {
747
- opts.particle.x = undefined;
748
- opts.particle.y = undefined;
749
- return;
788
+ const now = performance.now();
789
+ const particle = opts.particle;
790
+ if (particle._lastTime == undefined) {
791
+ particle._distanceTraveled = (opts.distance / opts.totalCount) * particle.index;
792
+ particle._lastTime = now;
793
+ particle._lastDistance = opts.distance;
750
794
  }
751
- const remainingSteps = opts.totalSteps - opts.particle.step;
752
- const progress = opts.particle.step / opts.totalSteps;
753
- if (remainingSteps <= 0) {
754
- opts.particle.x = opts.xEnd;
755
- opts.particle.y = opts.yEnd;
756
- opts.particle.step = 0;
757
- return;
795
+ const lastDistance = particle._lastDistance ?? 0;
796
+ if (lastDistance !== opts.distance && lastDistance > 0) {
797
+ const scale = opts.distance / lastDistance;
798
+ particle._distanceTraveled = ((particle._distanceTraveled ?? 0) * scale) % opts.distance;
799
+ particle._lastDistance = opts.distance;
758
800
  }
759
- const dx = opts.xEnd - opts.xStart;
760
- const dy = opts.yEnd - opts.yStart;
761
- const newX = opts.xStart + dx * progress;
762
- const newY = opts.yStart + dy * progress;
763
- opts.particle.x = newX;
764
- opts.particle.y = newY;
765
- if (needSpeed) {
766
- opts.particle.step += prevStepDifference <= 3 ? prevStepDifference : 3;
801
+ const delta = Math.min(now - particle._lastTime, FRAME_INTERVAL);
802
+ particle._lastTime = now;
803
+ particle._distanceTraveled =
804
+ ((particle._distanceTraveled ?? 0) + opts.speed * delta) % opts.distance;
805
+ const progress = particle._distanceTraveled / opts.distance;
806
+ if (opts.xControl !== 0 && opts.yControl !== 0) {
807
+ const t = 1 - progress;
808
+ particle.x =
809
+ t * t * opts.xStart + 2 * t * progress * opts.xControl + progress * progress * opts.xEnd;
810
+ particle.y =
811
+ t * t * opts.yStart + 2 * t * progress * opts.yControl + progress * progress * opts.yEnd;
767
812
  }
768
813
  else {
769
- opts.particle.step++;
814
+ const dx = opts.xEnd - opts.xStart;
815
+ const dy = opts.yEnd - opts.yStart;
816
+ particle.x = opts.xStart + dx * progress;
817
+ particle.y = opts.yStart + dy * progress;
770
818
  }
771
819
  }
820
+ function approximateQuadraticBezierLength(x0, y0, x1, y1, x2, y2) {
821
+ const dx01 = x1 - x0;
822
+ const dy01 = y1 - y0;
823
+ const dx12 = x2 - x1;
824
+ const dy12 = y2 - y1;
825
+ const dx02 = x2 - x0;
826
+ const dy02 = y2 - y0;
827
+ const a = Math.sqrt(dx01 * dx01 + dy01 * dy01);
828
+ const b = Math.sqrt(dx12 * dx12 + dy12 * dy12);
829
+ const c = Math.sqrt(dx02 * dx02 + dy02 * dy02);
830
+ return (a + b + c) / 2;
831
+ }
772
832
 
773
- function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, }) {
774
- if (!areaRect)
775
- return undefined;
776
- const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
833
+ function linkByPointerGetter({ pointerX, pointerY, links, linkHoverExtraZone, curve, }) {
777
834
  return d3Array.greatest(links, (link) => {
778
- if (isNearLink(pointerX, pointerY, link, linkHoverExtraZone))
779
- return link.index;
835
+ if (!jsHelpers.isObject(link.source) ||
836
+ !jsHelpers.isObject(link.target) ||
837
+ link.source.visible === false ||
838
+ link.target.visible === false)
839
+ return;
840
+ const x1 = (link._x1 ?? link.source.x);
841
+ const y1 = (link._y1 ?? link.source.y);
842
+ const x2 = (link._x2 ?? link.target.x);
843
+ const y2 = (link._y2 ?? link.target.y);
844
+ const cx = link._cx ?? 0;
845
+ const cy = link._cy ?? 0;
846
+ return curve
847
+ ? hitTestQuadratic(pointerX, pointerY, x1, y1, cx, cy, x2, y2, linkHoverExtraZone)
848
+ ? link.index
849
+ : undefined
850
+ : isNearLink(pointerX, pointerY, x1, y1, x2, y2, linkHoverExtraZone)
851
+ ? link.index
852
+ : undefined;
780
853
  });
781
854
  }
782
- function isNearLink(mouseX, mouseY, link, threshold = 2) {
783
- if (!jsHelpers.isObject(link.source) ||
784
- !jsHelpers.isObject(link.target) ||
785
- link.source.visible === false ||
786
- link.target.visible === false)
787
- return false;
788
- const x1 = link.source.x;
789
- const y1 = link.source.y;
790
- const x2 = link.target.x;
791
- const y2 = link.target.y;
792
- const distance = distanceToLine(mouseX, mouseY, x1, y1, x2, y2);
793
- return distance <= threshold;
794
- }
795
- function distanceToLine(x, y, x1, y1, x2, y2) {
796
- const A = x - x1;
797
- const B = y - y1;
855
+ function isNearLink(px, py, x1, y1, x2, y2, threshold = 2) {
856
+ const A = px - x1;
857
+ const B = py - y1;
798
858
  const C = x2 - x1;
799
859
  const D = y2 - y1;
800
860
  const dot = A * C + B * D;
@@ -816,9 +876,56 @@ function distanceToLine(x, y, x1, y1, x2, y2) {
816
876
  xx = x1 + param * C;
817
877
  yy = y1 + param * D;
818
878
  }
819
- const dx = x - xx;
820
- const dy = y - yy;
821
- return Math.sqrt(dx * dx + dy * dy);
879
+ const dx = px - xx;
880
+ const dy = py - yy;
881
+ return Math.sqrt(dx * dx + dy * dy) <= threshold;
882
+ }
883
+ function hitTestQuadratic(px, py, x0, y0, xc, yc, x2, y2, tol) {
884
+ const bb = bezierBBox(x0, y0, xc, yc, x2, y2);
885
+ if (px < bb.minX - tol || px > bb.maxX + tol || py < bb.minY - tol || py > bb.maxY + tol) {
886
+ return false;
887
+ }
888
+ const chord = Math.hypot(x2 - x0, y2 - y0);
889
+ const contLen = Math.hypot(xc - x0, yc - y0) + Math.hypot(x2 - xc, y2 - yc);
890
+ const approxLen = (chord + contLen) / 2;
891
+ const steps = Math.max(16, Math.ceil(approxLen / 1));
892
+ const tol2 = tol * tol;
893
+ for (let i = 0; i <= steps; i++) {
894
+ const t = i / steps;
895
+ const mt = 1 - t;
896
+ const x = mt * mt * x0 + 2 * mt * t * xc + t * t * x2;
897
+ const y = mt * mt * y0 + 2 * mt * t * yc + t * t * y2;
898
+ const dx = px - x;
899
+ const dy = py - y;
900
+ if (dx * dx + dy * dy <= tol2)
901
+ return true;
902
+ }
903
+ return false;
904
+ }
905
+ function bezierBBox(x0, y0, x1, y1, x2, y2) {
906
+ function extrema(p0, p1, p2) {
907
+ const denom = p0 - 2 * p1 + p2;
908
+ if (denom === 0)
909
+ return [];
910
+ const t = (p0 - p1) / denom;
911
+ return t > 0 && t < 1 ? [t] : [];
912
+ }
913
+ const ts = [...extrema(x0, x1, x2), ...extrema(y0, y1, y2), 0, 1];
914
+ let maxX = -Infinity, maxY = -Infinity, minX = Infinity, minY = Infinity;
915
+ for (const t of ts) {
916
+ const mt = 1 - t;
917
+ const x = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
918
+ const y = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
919
+ if (x < minX)
920
+ minX = x;
921
+ if (x > maxX)
922
+ maxX = x;
923
+ if (y < minY)
924
+ minY = y;
925
+ if (y > maxY)
926
+ maxY = y;
927
+ }
928
+ return { minX, minY, maxX, maxY };
822
929
  }
823
930
 
824
931
  function nodeHighlight(opts) {
@@ -994,22 +1101,38 @@ function extractLinkPointIds(link) {
994
1101
  return { sourceId, targetId };
995
1102
  }
996
1103
 
1104
+ function isEmptyObject(obj) {
1105
+ for (const key in obj) {
1106
+ if (Object.hasOwn(obj, key)) {
1107
+ return false;
1108
+ }
1109
+ }
1110
+ return true;
1111
+ }
1112
+
997
1113
  const GRAPH_SETTINGS = {
998
- zoomExtent: [0.3, 10],
999
- translateExtent: [[], []],
1000
- translateExtentEnable: true,
1001
- translateExtentCoefficient: [3, 3],
1114
+ zoomAnimation: true,
1115
+ zoomAnimationDuration: 300,
1116
+ zoomToNodeScale: 5,
1117
+ zoomToFitMargin: 0.25,
1118
+ zoomExtent: null,
1119
+ zoomExtentMargin: 0.3,
1120
+ translateExtent: null,
1121
+ translateExtentCoefficient: null,
1122
+ translateExtentOverlap: 0.3,
1002
1123
  dragPlaceCoefficient: dragPlaceCoefficientGetter,
1003
1124
  zoomInitial: null,
1004
1125
  showDrawTime: true,
1005
1126
  showDrawTimeEveryTick: false,
1006
1127
  };
1128
+ const PERFORMANCE_GRAPH_SETTINGS = {
1129
+ ...GRAPH_SETTINGS,
1130
+ };
1007
1131
 
1008
1132
  const HIGHLIGHT_SETTINGS = {
1009
1133
  highlightByHoverNode: true,
1010
1134
  highlightByHoverLink: true,
1011
- highlightUpFrames: 5,
1012
- highlightDownFrames: 5,
1135
+ highlightDuration: 150,
1013
1136
  linkHoverExtraZone: 2,
1014
1137
  /** Node */
1015
1138
  highlightByNodeOnlyRoot: true,
@@ -1064,36 +1187,51 @@ const HIGHLIGHT_SETTINGS = {
1064
1187
  highlightByLinkForArrowBorderColor: null,
1065
1188
  highlightByLinkForArrowBorderSizingAdditional: 0,
1066
1189
  };
1190
+ const PERFORMANCE_HIGHLIGHT_SETTINGS = {
1191
+ ...HIGHLIGHT_SETTINGS,
1192
+ };
1067
1193
 
1068
1194
  const LINK_SETTINGS = {
1195
+ smartCache: true,
1069
1196
  prettyDraw: true,
1197
+ curve: true,
1070
1198
  linkScaleSwitch: 1,
1071
- linkColorAfterScaleSwitch: "#000000FF",
1199
+ linkColorAfterScaleSwitch: "#C5C5C5FF",
1072
1200
  linkColorBeforeScaleSwitch: "#999",
1073
1201
  linkWidthAfterScaleSwitch: 0.1,
1074
1202
  linkWidthBeforeScaleSwitch: 1,
1075
1203
  arrow: true,
1076
1204
  arrowByHighlight: true,
1077
1205
  particles: true,
1078
- particleFlexSpeed: true,
1079
- particleFlexSpeedCoefficient: 4,
1206
+ // spawn one more particle for N px
1207
+ particleCountByDistance: 25,
1208
+ // speed for 1px
1209
+ particleSpeedByDistance: 50,
1080
1210
  };
1081
1211
  const LINK_OPTIONS = {
1082
1212
  alpha: 1,
1083
1213
  arrowAlpha: 1,
1084
1214
  arrowSize: 2,
1085
- arrowBorderColor: "#000000FF",
1215
+ arrowBorderColor: "#C5C5C5FF",
1086
1216
  arrowBorderWidth: 0.1,
1087
1217
  particleAlpha: 1,
1088
- particleColor: "#000000FF",
1089
- particleBorderColor: "#000000FF",
1218
+ particleColor: "#C5C5C5FF",
1219
+ particleBorderColor: "#C5C5C5FF",
1090
1220
  particleBorderWidth: 0.1,
1091
- particleCount: 2,
1092
1221
  particleRadius: 0.5,
1093
- particleSteps: 60,
1222
+ };
1223
+ const PERFORMANCE_LINK_SETTINGS = {
1224
+ ...LINK_SETTINGS,
1225
+ particles: false,
1226
+ prettyDraw: false,
1227
+ };
1228
+ const PERFORMANCE_LINK_OPTIONS = {
1229
+ ...LINK_OPTIONS,
1094
1230
  };
1095
1231
 
1096
1232
  const NODE_SETTINGS = {
1233
+ smartCache: true,
1234
+ textScaleSteps: [],
1097
1235
  nodeRadiusFlexible: true,
1098
1236
  nodeRadiusLinkCountForStep: 5,
1099
1237
  nodeRadiusIncrementByStep: 1,
@@ -1130,14 +1268,14 @@ const NODE_OPTIONS = {
1130
1268
  textShiftX: 0,
1131
1269
  textFont: "Arial",
1132
1270
  textAlign: "center",
1133
- textColor: "#333",
1271
+ textColor: "#d2d2d2",
1134
1272
  textStyle: "normal",
1135
1273
  textWeight: 500,
1136
1274
  textGap: 0,
1137
1275
  label: null,
1138
1276
  labelAlpha: 1,
1139
1277
  labelAlign: "center",
1140
- labelColor: "#333",
1278
+ labelColor: "#ffffff",
1141
1279
  labelFont: "Arial",
1142
1280
  labelGap: 0,
1143
1281
  labelSize: 3.5,
@@ -1145,6 +1283,14 @@ const NODE_OPTIONS = {
1145
1283
  labelWeight: 500,
1146
1284
  labelWidth: 20,
1147
1285
  };
1286
+ const PERFORMANCE_NODE_SETTINGS = {
1287
+ ...NODE_SETTINGS,
1288
+ nodeRadiusFlexible: false,
1289
+ nodeSizeFlexible: false,
1290
+ };
1291
+ const PERFOMANCE_NODE_OPTIONS = {
1292
+ ...NODE_OPTIONS,
1293
+ };
1148
1294
 
1149
1295
  const GRAPH_CACHE_TYPE = {
1150
1296
  NodeOptions: "nodeOptions",
@@ -1172,11 +1318,11 @@ function initArea() {
1172
1318
  if (!this.area)
1173
1319
  throw new Error("couldn't create canvas");
1174
1320
  this.container.appendChild(this.area);
1175
- this.areaRect = this.area.getBoundingClientRect();
1176
1321
  this.context = this.area.getContext("2d");
1177
1322
  if (!this.context)
1178
1323
  throw new Error("couldn't create canvas context");
1179
1324
  this.context.scale(this.dpi, this.dpi);
1325
+ this.updateRect();
1180
1326
  }
1181
1327
  }
1182
1328
 
@@ -1188,10 +1334,8 @@ function initDnd() {
1188
1334
  if (this.listeners.onDragSubject) {
1189
1335
  return this.listeners.onDragSubject.call(this, event);
1190
1336
  }
1191
- if (!this.areaRect)
1192
- return;
1193
1337
  const mouseEvent = event.sourceEvent;
1194
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1338
+ const [pointerX, pointerY] = this.getPointerAreaPosition(mouseEvent);
1195
1339
  return d3Array.greatest(this.nodes, (node) => {
1196
1340
  if (!node.x || !node.y || (jsHelpers.isBoolean(node.drag) && !node.drag))
1197
1341
  return undefined;
@@ -1207,12 +1351,16 @@ function initDnd() {
1207
1351
  if (this.simulation)
1208
1352
  this.simulation.alphaTarget(0.3).restart();
1209
1353
  }
1210
- if (!this.areaRect)
1211
- return;
1212
1354
  const mouseEvent = event.sourceEvent;
1213
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1214
- event.subject.fx = pointerX;
1215
- event.subject.fy = pointerY;
1355
+ const [pointerX, pointerY] = this.getPointerAreaPosition(mouseEvent);
1356
+ if (this._translateExtent) {
1357
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1358
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1359
+ }
1360
+ else {
1361
+ event.subject.fx = pointerX;
1362
+ event.subject.fy = pointerY;
1363
+ }
1216
1364
  this.listeners.onMoveDragFinished?.call?.(this, event);
1217
1365
  })
1218
1366
  .on("end", (event) => {
@@ -1220,10 +1368,16 @@ function initDnd() {
1220
1368
  if (!event.active && this.simulation)
1221
1369
  this.simulation.alphaTarget(0);
1222
1370
  const sourceEvent = event.sourceEvent;
1223
- if (sourceEvent.altKey && this.areaRect) {
1224
- const [pointerX, pointerY] = pointerGetter(sourceEvent, this.areaRect, this.areaTransform);
1225
- event.subject.fx = pointerX;
1226
- event.subject.fy = pointerY;
1371
+ if (sourceEvent.altKey) {
1372
+ const [pointerX, pointerY] = this.getPointerAreaPosition(sourceEvent);
1373
+ if (this._translateExtent) {
1374
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1375
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1376
+ }
1377
+ else {
1378
+ event.subject.fx = pointerX;
1379
+ event.subject.fy = pointerY;
1380
+ }
1227
1381
  }
1228
1382
  else {
1229
1383
  event.subject.fx = null;
@@ -1234,8 +1388,44 @@ function initDnd() {
1234
1388
  d3Selection.select(this.area).call(dragHandler);
1235
1389
  }
1236
1390
 
1391
+ function calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, groupIndex, arrowSize = 0) {
1392
+ const sourceRadius = (sourceNode._radius ?? COMMON_SETTINGS.nodeRadius) + (sourceNode._borderWidth ?? 0) * 0.5;
1393
+ const targetRadius = (targetNode._radius ?? COMMON_SETTINGS.nodeRadius) + (targetNode._borderWidth ?? 0) * 0.5;
1394
+ const effectiveTargetRadius = targetRadius + arrowSize * 0.9;
1395
+ const mx = (xStart + xEnd) * 0.5;
1396
+ const my = (yStart + yEnd) * 0.5;
1397
+ const dx = xEnd - xStart;
1398
+ const dy = yEnd - yStart;
1399
+ const lenSq = dx * dx + dy * dy;
1400
+ const len = lenSq > 0 ? Math.sqrt(lenSq) : 1;
1401
+ const nx = -dy / len;
1402
+ const ny = dx / len;
1403
+ const offset = 0.12 * len * groupIndex;
1404
+ const xControl = mx + nx * offset;
1405
+ const yControl = my + ny * offset;
1406
+ const [xStartP, yStartP] = adjustPoint(xStart, yStart, xControl, yControl, sourceRadius);
1407
+ const [xEndP, yEndP] = adjustPoint(xEnd, yEnd, xControl, yControl, effectiveTargetRadius);
1408
+ const [xEndArrow, yEndArrow] = adjustPoint(xEnd, yEnd, xControl, yControl, targetRadius);
1409
+ return {
1410
+ xStart: xStartP,
1411
+ yStart: yStartP,
1412
+ xEnd: xEndP,
1413
+ yEnd: yEndP,
1414
+ xControl,
1415
+ yControl,
1416
+ xEndArrow,
1417
+ yEndArrow,
1418
+ };
1419
+ }
1420
+ function adjustPoint(x, y, xControl, yControl, radius) {
1421
+ const dxToControl = xControl - x;
1422
+ const dyToControl = yControl - y;
1423
+ const distToControl = Math.sqrt(dxToControl * dxToControl + dyToControl * dyToControl) || 1;
1424
+ return [x + (dxToControl / distToControl) * radius, y + (dyToControl / distToControl) * radius];
1425
+ }
1426
+
1237
1427
  function getDrawLink() {
1238
- return function drawLink(link) {
1428
+ return function drawLink(link, index) {
1239
1429
  if (!this.context ||
1240
1430
  typeof link.source !== "object" ||
1241
1431
  typeof link.target !== "object" ||
@@ -1250,8 +1440,7 @@ function getDrawLink() {
1250
1440
  return;
1251
1441
  if (!link.source._visible && !link.target._visible)
1252
1442
  return;
1253
- const id = `${link.target.id}${link.source.id}`;
1254
- const linkOptions = this.linkOptionsCache[id];
1443
+ const linkOptions = this.linkOptionsCache[index];
1255
1444
  if (!linkOptions)
1256
1445
  return;
1257
1446
  if (linkOptions.drawLink) {
@@ -1266,10 +1455,12 @@ function getDrawLink() {
1266
1455
  let arrowSize = linkOptions.arrowSize;
1267
1456
  let arrowBorderWidth = linkOptions.arrowBorderWidth;
1268
1457
  let arrowBorderColor = linkOptions.arrowBorderColor;
1458
+ const currentNodeHighlighted = this.highlightedNode &&
1459
+ (this.highlightedNode.id == link.source.id || this.highlightedNode.id == link.target.id);
1269
1460
  /** Highlight */
1270
1461
  if (this.highlightedNeighbors && this.highlightedNode) {
1271
1462
  /** By Node Not Highlight */
1272
- if (this.highlightedNode.id != link.source.id && this.highlightedNode.id != link.target.id) {
1463
+ if (!currentNodeHighlighted) {
1273
1464
  const fadeOptions = linkFade({
1274
1465
  arrow: this.linkSettings.arrow,
1275
1466
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1303,9 +1494,10 @@ function getDrawLink() {
1303
1494
  arrowBorderColor = highlightOptions.arrowBorderColor;
1304
1495
  }
1305
1496
  }
1497
+ const currentLinkHighlighted = this.highlightedLink === link;
1306
1498
  if (this.highlightedNeighbors && this.highlightedLink) {
1307
1499
  /** By Link Not Highlight */
1308
- if (this.highlightedLink !== link) {
1500
+ if (!currentLinkHighlighted) {
1309
1501
  const fadeOptions = linkFade({
1310
1502
  arrow: this.linkSettings.arrow,
1311
1503
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1349,99 +1541,129 @@ function getDrawLink() {
1349
1541
  let yStart = link.source.y;
1350
1542
  let xEnd = link.target.x;
1351
1543
  let yEnd = link.target.y;
1544
+ let xControl = 0;
1545
+ let yControl = 0;
1546
+ let xEndArrow = xEnd;
1547
+ let yEndArrow = yEnd;
1352
1548
  let linkDistance = 0;
1353
- if (this.linkSettings.prettyDraw ||
1354
- this.linkSettings.particleFlexSpeed ||
1355
- (this.linkSettings.arrow && arrowAlpha > 0)) {
1356
- const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1357
- const position = calculateLinkPositionByNode(link, isHasArrow ? arrowSize : 0);
1358
- if (position) {
1359
- xStart = position.x1;
1360
- xEnd = position.x2;
1361
- yStart = position.y1;
1362
- yEnd = position.y2;
1549
+ const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1550
+ if (!this.linkSettings.curve) {
1551
+ if (this.linkSettings.prettyDraw ||
1552
+ (this.linkSettings.arrow && arrowAlpha > 0) ||
1553
+ (this.particles && (currentNodeHighlighted || currentLinkHighlighted))) {
1554
+ const position = calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, isHasArrow ? arrowSize : 0);
1555
+ xStart = position.xStart;
1556
+ xEnd = position.xEnd;
1557
+ yStart = position.yStart;
1558
+ yEnd = position.yEnd;
1559
+ xEndArrow = position.xEndArrow;
1560
+ yEndArrow = position.yEndArrow;
1363
1561
  linkDistance = position.distance;
1364
1562
  }
1563
+ link._x1 = xStart;
1564
+ link._y1 = yStart;
1565
+ link._x2 = xEnd;
1566
+ link._y2 = yEnd;
1567
+ link._ax = xEndArrow;
1568
+ link._ay = yEndArrow;
1569
+ this.context.moveTo(xStart, yStart);
1570
+ this.context.lineTo(xEnd, yEnd);
1571
+ this.context.stroke();
1572
+ }
1573
+ else {
1574
+ const position = calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, link._groupIndex ?? 0, isHasArrow ? arrowSize : 0);
1575
+ xStart = position.xStart;
1576
+ yStart = position.yStart;
1577
+ xEnd = position.xEnd;
1578
+ yEnd = position.yEnd;
1579
+ xControl = position.xControl;
1580
+ yControl = position.yControl;
1581
+ xEndArrow = position.xEndArrow;
1582
+ yEndArrow = position.yEndArrow;
1583
+ link._x1 = position.xStart;
1584
+ link._y1 = position.yStart;
1585
+ link._x2 = position.xEnd;
1586
+ link._y2 = position.yEnd;
1587
+ link._cx = position.xControl;
1588
+ link._cy = position.yControl;
1589
+ link._ax = position.xEndArrow;
1590
+ link._ay = position.yEndArrow;
1591
+ linkDistance = approximateQuadraticBezierLength(position.xStart, position.yStart, position.xControl, position.yControl, position.xEnd, position.yEnd);
1592
+ this.context.beginPath();
1593
+ this.context.moveTo(position.xStart, position.yStart);
1594
+ this.context.quadraticCurveTo(position.xControl, position.yControl, position.xEnd, position.yEnd);
1595
+ this.context.setLineDash([]);
1596
+ // if (isDashed) {
1597
+ // this.context.setLineDash([10, 5]);
1598
+ // }
1599
+ this.context.stroke();
1365
1600
  }
1366
- this.context.moveTo(xStart, yStart);
1367
- this.context.lineTo(xEnd, yEnd);
1368
- this.context.stroke();
1369
1601
  /** Particle */
1370
- if (this.linkSettings.particles &&
1371
- ((this.highlightedNode &&
1372
- (this.highlightedNode.id === link.source.id ||
1373
- this.highlightedNode.id === link.target.id)) ||
1374
- (this.highlightedLink && this.highlightedLink === link))) {
1375
- const particleSteps = this.linkSettings.particleFlexSpeed
1376
- ? linkDistance <= 0
1377
- ? 0
1378
- : linkDistance * this.linkSettings.particleFlexSpeedCoefficient
1379
- : linkOptions.particleSteps;
1380
- const particleCount = linkOptions.particleCount;
1381
- if (!this.particles[id]) {
1382
- const sourceId = link.source.id;
1383
- const targetId = link.target.id;
1602
+ if (this.linkSettings.particles && (currentLinkHighlighted || currentNodeHighlighted)) {
1603
+ const speed = (1 / this.linkSettings.particleSpeedByDistance) * (1 + 0.5);
1604
+ if (!this.particles[index]) {
1605
+ const particleCount = Math.max(1, Math.floor(linkDistance / this.linkSettings.particleCountByDistance));
1384
1606
  const particles = [];
1385
- let prevParticle;
1386
1607
  for (let i = 0; i < particleCount; i++) {
1387
1608
  const particle = {
1388
- step: 0,
1389
- sourceId,
1390
- targetId,
1391
- prev: prevParticle,
1392
- next: undefined,
1393
1609
  index: i,
1394
1610
  };
1395
- if (prevParticle)
1396
- prevParticle.next = particle;
1397
1611
  particles.push(particle);
1398
- prevParticle = particle;
1399
- }
1400
- if (particles.length >= 2) {
1401
- particles[0].prev = particles[particles.length - 1];
1402
- particles[particles.length - 1].next = particles[0];
1403
1612
  }
1404
- this.particles[id] = particles;
1613
+ this.particles[index] = particles;
1405
1614
  }
1406
- if (particleSteps !== 0) {
1407
- this.particles[id].forEach((particle) => {
1408
- if (!this.context)
1409
- return;
1410
- const distance = particleSteps / particleCount;
1411
- getParticlePosition({
1412
- distance,
1413
- particle,
1414
- totalSteps: particleSteps,
1415
- xEnd,
1416
- xStart,
1417
- yEnd,
1418
- yStart,
1419
- });
1420
- if (particle.x != undefined && particle.y != undefined) {
1421
- this.context.beginPath();
1422
- this.context.strokeStyle = linkOptions.particleBorderColor;
1423
- this.context.lineWidth = linkOptions.particleBorderWidth;
1424
- this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1425
- this.context.fillStyle = linkOptions.particleColor;
1426
- this.context.fill();
1427
- if (linkOptions.particleBorderWidth > 0) {
1428
- this.context.stroke();
1429
- }
1430
- }
1615
+ this.particles[index].forEach((particle, _, particles) => {
1616
+ if (!this.context)
1617
+ return;
1618
+ getParticlePosition({
1619
+ distance: linkDistance,
1620
+ particle,
1621
+ totalCount: particles.length,
1622
+ xEnd,
1623
+ xStart,
1624
+ yEnd,
1625
+ yStart,
1626
+ xControl,
1627
+ yControl,
1628
+ speed,
1431
1629
  });
1432
- }
1630
+ if (particle.x != undefined && particle.y != undefined) {
1631
+ this.context.beginPath();
1632
+ this.context.strokeStyle = linkOptions.particleBorderColor;
1633
+ this.context.lineWidth = linkOptions.particleBorderWidth;
1634
+ this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1635
+ this.context.fillStyle = linkOptions.particleColor;
1636
+ this.context.fill();
1637
+ if (linkOptions.particleBorderWidth > 0) {
1638
+ this.context.stroke();
1639
+ }
1640
+ }
1641
+ });
1433
1642
  }
1434
1643
  /** Arrow */
1435
1644
  if (this.linkSettings.arrow && arrowAlpha > 0) {
1645
+ let angle = 0;
1646
+ if (!this.linkSettings.curve) {
1647
+ angle = Math.atan2(yEndArrow - yStart, xEndArrow - xStart);
1648
+ }
1649
+ else {
1650
+ const tangentX = 2 * (xEndArrow - xControl);
1651
+ const tangentY = 2 * (yEndArrow - yControl);
1652
+ if (Math.abs(tangentX) < 0.001 && Math.abs(tangentY) < 0.001) {
1653
+ angle = Math.atan2(yEndArrow - yControl, xEndArrow - xControl);
1654
+ }
1655
+ else {
1656
+ angle = Math.atan2(tangentY, tangentX);
1657
+ }
1658
+ }
1436
1659
  this.context.beginPath();
1437
1660
  this.context.globalAlpha = arrowAlpha;
1438
1661
  this.context.strokeStyle = arrowBorderColor;
1439
1662
  this.context.lineWidth = arrowBorderWidth;
1440
1663
  this.context.fillStyle = arrowColor;
1441
- const angle = Math.atan2(yEnd - yStart, xEnd - xStart);
1442
- this.context.moveTo(xEnd, yEnd);
1443
- this.context.lineTo(xEnd - arrowSize * Math.cos(angle - Math.PI / 6), yEnd - arrowSize * Math.sin(angle - Math.PI / 6));
1444
- this.context.lineTo(xEnd - arrowSize * Math.cos(angle + Math.PI / 6), yEnd - arrowSize * Math.sin(angle + Math.PI / 6));
1664
+ this.context.moveTo(xEndArrow, yEndArrow);
1665
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle - Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle - Math.PI / 6));
1666
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle + Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle + Math.PI / 6));
1445
1667
  this.context.closePath();
1446
1668
  this.context.fill();
1447
1669
  if (arrowBorderWidth > 0) {
@@ -1500,7 +1722,7 @@ function getTextLines({ context, textAlign, textColor, textFont, textStyle, text
1500
1722
  }
1501
1723
 
1502
1724
  function getDrawNode(nodeRenders, textRenders) {
1503
- return function drawNode(node) {
1725
+ return function drawNode(node, index) {
1504
1726
  if (!this.context || !node.x || !node.y)
1505
1727
  return;
1506
1728
  if (node.visible != undefined && !node.visible) {
@@ -1509,7 +1731,7 @@ function getDrawNode(nodeRenders, textRenders) {
1509
1731
  node._height = 0;
1510
1732
  return;
1511
1733
  }
1512
- const nodeOptions = this.nodeOptionsCache[node.id];
1734
+ const nodeOptions = this.nodeOptionsCache[index];
1513
1735
  if (!nodeOptions)
1514
1736
  return;
1515
1737
  if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
@@ -1668,7 +1890,7 @@ function getDrawNode(nodeRenders, textRenders) {
1668
1890
  this.context.lineWidth = borderWidth;
1669
1891
  this.context.strokeStyle = borderColor;
1670
1892
  this.context.fillStyle = color;
1671
- const labelLines = this.cachedNodeLabel[node.id];
1893
+ const labelLines = this.cachedNodeLabel[index];
1672
1894
  switch (nodeOptions.shape) {
1673
1895
  case "circle": {
1674
1896
  if (!node.image) {
@@ -1794,7 +2016,7 @@ function getDrawNode(nodeRenders, textRenders) {
1794
2016
  });
1795
2017
  }
1796
2018
  /** Text draw */
1797
- const textLines = this.cachedNodeText[node.id];
2019
+ const textLines = this.cachedNodeText[index];
1798
2020
  if (nodeOptions.textVisible && nodeOptions.text && textLines) {
1799
2021
  textRenders.push(() => {
1800
2022
  if (nodeOptions.textDraw) {
@@ -1854,57 +2076,7 @@ function getDrawNode(nodeRenders, textRenders) {
1854
2076
  }
1855
2077
 
1856
2078
  function initDraw() {
1857
- let hasHighlight = false;
1858
- function calculateHighlight(recursive) {
1859
- if (hasHighlight && !recursive)
1860
- return;
1861
- /** animation up */
1862
- if (!this.highlightWorking && this.highlightProgress > 0) {
1863
- const highlightDownStep = 1 / this.highlightSettings.highlightDownFrames;
1864
- this.highlightProgress -= highlightDownStep;
1865
- if (!this.simulationWorking) {
1866
- hasHighlight = true;
1867
- return void requestAnimationFrame(() => this.draw(true));
1868
- }
1869
- if (!this.linkSettings.particles) {
1870
- hasHighlight = false;
1871
- return;
1872
- }
1873
- }
1874
- /** animation down */
1875
- if (this.highlightWorking && this.highlightProgress < 1) {
1876
- const highlightUpStep = 1 / this.highlightSettings.highlightUpFrames;
1877
- this.highlightProgress += highlightUpStep;
1878
- if (!this.simulationWorking) {
1879
- hasHighlight = true;
1880
- return void requestAnimationFrame(() => this.draw(true));
1881
- }
1882
- if (!this.linkSettings.particles) {
1883
- hasHighlight = false;
1884
- return;
1885
- }
1886
- }
1887
- /** animation active */
1888
- if (this.linkSettings.particles && this.highlightWorking && !this.simulationWorking) {
1889
- hasHighlight = true;
1890
- return void requestAnimationFrame(() => this.draw(true));
1891
- }
1892
- /** animation stop */
1893
- if (!this.highlightWorking && this.highlightProgress <= 0) {
1894
- if (this.highlightedNeighbors || this.highlightedNode || this.highlightedLink) {
1895
- this.highlightedNeighbors = null;
1896
- this.highlightedNode = null;
1897
- this.highlightedLink = null;
1898
- this.particles = {};
1899
- if (!this.simulationWorking) {
1900
- hasHighlight = true;
1901
- return void requestAnimationFrame(() => this.draw(true));
1902
- }
1903
- }
1904
- }
1905
- hasHighlight = false;
1906
- }
1907
- function draw(recursive = false) {
2079
+ function draw() {
1908
2080
  if (!this.context)
1909
2081
  return;
1910
2082
  if (this.listeners.onDraw) {
@@ -1924,8 +2096,20 @@ function initDraw() {
1924
2096
  nodeRenders.forEach((render) => render());
1925
2097
  textRenders.forEach((render) => render());
1926
2098
  this.context.restore();
2099
+ /** selection rectangle */
2100
+ if (this.isSelecting && this.selectionRect) {
2101
+ const rect = this.selectionRect;
2102
+ const screenX = Math.min(rect.x1, rect.x2) * this.areaTransform.k + this.areaTransform.x;
2103
+ const screenY = Math.min(rect.y1, rect.y2) * this.areaTransform.k + this.areaTransform.y;
2104
+ const screenW = Math.abs(rect.x2 - rect.x1) * this.areaTransform.k;
2105
+ const screenH = Math.abs(rect.y2 - rect.y1) * this.areaTransform.k;
2106
+ this.context.fillStyle = "rgba(66, 133, 244, 0.2)";
2107
+ this.context.fillRect(screenX, screenY, screenW, screenH);
2108
+ this.context.strokeStyle = "rgba(66, 133, 244, 0.8)";
2109
+ this.context.lineWidth = 2;
2110
+ this.context.strokeRect(screenX, screenY, screenW, screenH);
2111
+ }
1927
2112
  this.listeners.onDrawFinished?.call?.(this);
1928
- calculateHighlight.bind(this)(recursive);
1929
2113
  }
1930
2114
  if (this.graphSettings.showDrawTime) {
1931
2115
  return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
@@ -1937,7 +2121,7 @@ function initPointer() {
1937
2121
  if (!this.area || !this.nodes)
1938
2122
  throw new Error("bad init data");
1939
2123
  function onHover(event) {
1940
- if (!this.area)
2124
+ if (!this.area || this.isSelecting)
1941
2125
  return;
1942
2126
  let currentNode;
1943
2127
  let currentLink;
@@ -1946,10 +2130,10 @@ function initPointer() {
1946
2130
  let highlightNode = true;
1947
2131
  let highlightLink = true;
1948
2132
  if (checkHighlightNode) {
2133
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
1949
2134
  currentNode = nodeByPointerGetter({
1950
- areaRect: this.areaRect,
1951
- areaTransform: this.areaTransform,
1952
- mouseEvent: event,
2135
+ pointerX,
2136
+ pointerY,
1953
2137
  nodes: this.nodes,
1954
2138
  });
1955
2139
  if (currentNode?.highlight != undefined)
@@ -1959,12 +2143,13 @@ function initPointer() {
1959
2143
  this.area.style.cursor = "pointer";
1960
2144
  }
1961
2145
  else if (checkHighlightLink) {
2146
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
1962
2147
  currentLink = linkByPointerGetter({
2148
+ pointerX,
2149
+ pointerY,
1963
2150
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
1964
- areaRect: this.areaRect,
1965
- areaTransform: this.areaTransform,
1966
- mouseEvent: event,
1967
2151
  links: this.links,
2152
+ curve: this.linkSettings.curve,
1968
2153
  });
1969
2154
  if (currentLink?.highlight != undefined)
1970
2155
  highlightLink = currentLink?.highlight;
@@ -1978,54 +2163,25 @@ function initPointer() {
1978
2163
  else {
1979
2164
  this.area.style.cursor = "default";
1980
2165
  }
1981
- if (currentNode && highlightNode && this.highlightedNode !== currentNode) {
1982
- this.highlightedNode = currentNode;
1983
- this.highlightedLink = null;
1984
- this.highlightedNeighbors = new Set(this.highlightedNode?.neighbors ?? []);
1985
- this.highlightWorking = true;
1986
- if (!this.simulationWorking)
1987
- requestAnimationFrame(() => {
1988
- this.draw();
1989
- });
1990
- }
1991
- else if (currentLink &&
1992
- highlightLink &&
1993
- checkType(currentLink.source, lodash.isObject(currentLink.source)) &&
1994
- checkType(currentLink.target, lodash.isObject(currentLink.target)) &&
1995
- this.highlightedLink !== currentLink) {
1996
- this.highlightProgress = 0;
1997
- this.highlightedLink = currentLink;
1998
- this.highlightedNode = null;
1999
- this.highlightedNeighbors = new Set([currentLink.source.id, currentLink.target.id]);
2000
- this.highlightWorking = true;
2001
- if (!this.simulationWorking)
2002
- requestAnimationFrame(() => {
2003
- this.draw();
2004
- });
2005
- }
2006
- else if (!currentNode && !currentLink && (this.highlightedNode || this.highlightedLink)) {
2007
- this.highlightWorking = false;
2008
- if (!this.simulationWorking)
2009
- requestAnimationFrame(() => {
2010
- this.draw();
2011
- });
2012
- }
2166
+ this.animateHighlight(highlightNode ? currentNode : undefined, highlightLink ? currentLink : undefined);
2013
2167
  if (!this.listeners.onMove)
2014
2168
  return;
2015
- if (!currentNode && !checkHighlightNode)
2169
+ if (!currentNode && !checkHighlightNode) {
2170
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2016
2171
  currentNode = nodeByPointerGetter({
2017
- areaRect: this.areaRect,
2018
- areaTransform: this.areaTransform,
2019
- mouseEvent: event,
2172
+ pointerX,
2173
+ pointerY,
2020
2174
  nodes: this.nodes,
2021
2175
  });
2176
+ }
2022
2177
  if (!currentNode && (!checkHighlightNode || (!checkHighlightLink && !currentLink))) {
2178
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2023
2179
  currentLink = linkByPointerGetter({
2180
+ pointerX,
2181
+ pointerY,
2024
2182
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2025
- areaRect: this.areaRect,
2026
- areaTransform: this.areaTransform,
2027
- mouseEvent: event,
2028
2183
  links: this.links,
2184
+ curve: this.linkSettings.curve,
2029
2185
  });
2030
2186
  }
2031
2187
  if (!currentNode)
@@ -2037,19 +2193,20 @@ function initPointer() {
2037
2193
  !("button" in event) ||
2038
2194
  event.button !== 1)
2039
2195
  return;
2196
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2040
2197
  const currentNode = nodeByPointerGetter({
2041
- areaRect: this.areaRect,
2042
- areaTransform: this.areaTransform,
2043
- mouseEvent: event,
2198
+ pointerX,
2199
+ pointerY,
2044
2200
  nodes: this.nodes,
2045
2201
  });
2046
2202
  if (!currentNode) {
2203
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2047
2204
  const currentLink = linkByPointerGetter({
2205
+ pointerX,
2206
+ pointerY,
2048
2207
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2049
- areaRect: this.areaRect,
2050
- areaTransform: this.areaTransform,
2051
- mouseEvent: event,
2052
2208
  links: this.links,
2209
+ curve: this.linkSettings.curve,
2053
2210
  });
2054
2211
  return void this.listeners.onWheelClick.call(this, event, undefined, currentLink);
2055
2212
  }
@@ -2058,19 +2215,20 @@ function initPointer() {
2058
2215
  function onRightClick(event) {
2059
2216
  if (!this.listeners.onContextMenu)
2060
2217
  return;
2218
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2061
2219
  const currentNode = nodeByPointerGetter({
2062
- areaRect: this.areaRect,
2063
- areaTransform: this.areaTransform,
2064
- mouseEvent: event,
2220
+ pointerX,
2221
+ pointerY,
2065
2222
  nodes: this.nodes,
2066
2223
  });
2067
2224
  if (!currentNode) {
2225
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2068
2226
  const currentLink = linkByPointerGetter({
2227
+ pointerX,
2228
+ pointerY,
2069
2229
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2070
- areaRect: this.areaRect,
2071
- areaTransform: this.areaTransform,
2072
- mouseEvent: event,
2073
2230
  links: this.links,
2231
+ curve: this.linkSettings.curve,
2074
2232
  });
2075
2233
  return void this.listeners.onContextMenu.call(this, event, undefined, currentLink);
2076
2234
  }
@@ -2079,19 +2237,20 @@ function initPointer() {
2079
2237
  function onDoubleClick(event) {
2080
2238
  if (!this.listeners.onDoubleClick)
2081
2239
  return;
2240
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2082
2241
  const currentNode = nodeByPointerGetter({
2083
- areaRect: this.areaRect,
2084
- areaTransform: this.areaTransform,
2085
- mouseEvent: event,
2242
+ pointerX,
2243
+ pointerY,
2086
2244
  nodes: this.nodes,
2087
2245
  });
2088
2246
  if (!currentNode) {
2247
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2089
2248
  const currentLink = linkByPointerGetter({
2249
+ pointerX,
2250
+ pointerY,
2090
2251
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2091
- areaRect: this.areaRect,
2092
- areaTransform: this.areaTransform,
2093
- mouseEvent: event,
2094
2252
  links: this.links,
2253
+ curve: this.linkSettings.curve,
2095
2254
  });
2096
2255
  return void this.listeners.onDoubleClick.call(this, event, undefined, currentLink);
2097
2256
  }
@@ -2100,19 +2259,20 @@ function initPointer() {
2100
2259
  function onClick(event) {
2101
2260
  if (this.isDragging || !this.listeners.onClick || ("button" in event && event.button !== 0))
2102
2261
  return;
2262
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2103
2263
  const currentNode = nodeByPointerGetter({
2104
- areaRect: this.areaRect,
2105
- areaTransform: this.areaTransform,
2106
- mouseEvent: event,
2264
+ pointerX,
2265
+ pointerY,
2107
2266
  nodes: this.nodes,
2108
2267
  });
2109
2268
  if (!currentNode) {
2269
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2110
2270
  const currentLink = linkByPointerGetter({
2271
+ pointerX,
2272
+ pointerY,
2111
2273
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2112
- areaRect: this.areaRect,
2113
- areaTransform: this.areaTransform,
2114
- mouseEvent: event,
2115
2274
  links: this.links,
2275
+ curve: this.linkSettings.curve,
2116
2276
  });
2117
2277
  return void this.listeners.onClick.call(this, event, undefined, currentLink);
2118
2278
  }
@@ -2158,148 +2318,270 @@ function initResize() {
2158
2318
  return;
2159
2319
  }
2160
2320
  this.updateSize();
2321
+ requestAnimationFrame(() => {
2322
+ this.updateSize();
2323
+ });
2161
2324
  });
2162
2325
  document.addEventListener("scroll", this.updateRect.bind(this), {
2163
2326
  capture: true,
2164
2327
  passive: true,
2165
2328
  signal: abortController.signal,
2166
2329
  });
2330
+ document.addEventListener("visibilitychange", () => {
2331
+ if (document.hidden)
2332
+ return;
2333
+ this.updateSize();
2334
+ requestAnimationFrame(() => {
2335
+ this.updateSize();
2336
+ });
2337
+ }, { signal: abortController.signal });
2167
2338
  observer.observe(this.area);
2168
2339
  }
2169
2340
 
2170
- function initSimulation() {
2171
- if (!this.simulation) {
2172
- this.simulation = d3Force.forceSimulation()
2173
- .nodes(this.nodes)
2174
- .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2175
- .on("tick", () => {
2176
- this.draw();
2177
- })
2178
- .on("end", () => {
2179
- this.listeners.onSimulationEnd?.call?.(this);
2180
- if (this.graphSettings.showDrawTime) {
2181
- getDrawTime();
2182
- // eslint-disable-next-line no-console
2183
- console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2184
- resetDrawTime();
2185
- }
2341
+ function initSelection() {
2342
+ if (!this.area)
2343
+ throw new Error("bad init data");
2344
+ this.isSelecting = false;
2345
+ this.selectionRect = null;
2346
+ let selectedNodesSet = new Set();
2347
+ let selectedLinksSet = new Set();
2348
+ let localSelection = false;
2349
+ function onMouseDown(event) {
2350
+ if (event.button !== 0 || !event.shiftKey)
2351
+ return;
2352
+ if (!this.area)
2353
+ return;
2354
+ event.stopPropagation();
2355
+ event.preventDefault();
2356
+ const [startX, startY] = this.getPointerAreaPosition(event);
2357
+ this.isSelecting = true;
2358
+ localSelection = true;
2359
+ this.selectionRect = { x1: startX, y1: startY, x2: startX, y2: startY };
2360
+ selectedNodesSet = new Set();
2361
+ selectedLinksSet = new Set();
2362
+ const controller = new AbortController();
2363
+ this.area.addEventListener("mousemove", onMouseMove.bind(this), {
2364
+ signal: controller.signal,
2186
2365
  });
2187
- initSimulationForces.call(this);
2188
- }
2189
- }
2190
- function initSimulationForces() {
2191
- if (!this.simulation)
2192
- return;
2193
- const linkForce = this.simulation.force("link");
2194
- if (!linkForce)
2195
- return;
2196
- if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2197
- this.simulation.force("x", null);
2198
- }
2199
- if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2200
- this.simulation.force("y", null);
2201
- }
2202
- if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2203
- this.simulation.force("charge")) {
2204
- this.simulation.force("charge", null);
2366
+ this.area.addEventListener("mouseup", () => {
2367
+ onMouseUp.call(this, controller);
2368
+ }, {
2369
+ signal: controller.signal,
2370
+ });
2371
+ window.addEventListener("keyup", (event) => {
2372
+ if (event.key === "Shift") {
2373
+ onMouseUp.call(this, controller);
2374
+ }
2375
+ }, { signal: controller.signal });
2205
2376
  }
2206
- if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2207
- this.simulation.force("center")) {
2208
- this.simulation.force("center", null);
2377
+ function onMouseMove(event) {
2378
+ if (!this.isSelecting || !this.selectionRect)
2379
+ return;
2380
+ event.stopPropagation();
2381
+ const [x, y] = this.getPointerAreaPosition(event);
2382
+ this.selectionRect.x2 = x;
2383
+ this.selectionRect.y2 = y;
2384
+ const rect = normalizeRect(this.selectionRect);
2385
+ const newNodes = new Set();
2386
+ for (let i = 0; i < this.nodes.length; i++) {
2387
+ const node = this.nodes[i];
2388
+ if (node.x == undefined || node.y == undefined)
2389
+ continue;
2390
+ if (node.visible === false)
2391
+ continue;
2392
+ const cache = this.nodeOptionsCache[i];
2393
+ if (isNodeInRect(node, rect, cache)) {
2394
+ newNodes.add(node);
2395
+ }
2396
+ }
2397
+ const newLinks = new Set();
2398
+ for (const link of this.links) {
2399
+ const source = link.source;
2400
+ const target = link.target;
2401
+ if (newNodes.has(source) && newNodes.has(target)) {
2402
+ newLinks.add(link);
2403
+ }
2404
+ }
2405
+ for (const node of selectedNodesSet) {
2406
+ if (!newNodes.has(node)) {
2407
+ node._selected = false;
2408
+ this.listeners.onSelectionOut?.call(this, node, undefined);
2409
+ }
2410
+ }
2411
+ for (const link of selectedLinksSet) {
2412
+ if (!newLinks.has(link)) {
2413
+ link._selected = false;
2414
+ this.listeners.onSelectionOut?.call(this, undefined, link);
2415
+ }
2416
+ }
2417
+ for (const node of newNodes) {
2418
+ if (!selectedNodesSet.has(node)) {
2419
+ node._selected = true;
2420
+ this.listeners.onSelectionIn?.call(this, node, undefined);
2421
+ }
2422
+ }
2423
+ for (const link of newLinks) {
2424
+ if (!selectedLinksSet.has(link)) {
2425
+ link._selected = true;
2426
+ this.listeners.onSelectionIn?.call(this, undefined, link);
2427
+ }
2428
+ }
2429
+ selectedNodesSet = newNodes;
2430
+ selectedLinksSet = newLinks;
2431
+ this.draw();
2209
2432
  }
2210
- if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2211
- linkForce.distance(0).strength(0).iterations(0);
2433
+ function onMouseUp(controller) {
2434
+ controller.abort();
2435
+ if (!this.isSelecting)
2436
+ return;
2437
+ const nodes = Array.from(selectedNodesSet);
2438
+ const links = Array.from(selectedLinksSet);
2439
+ for (const node of nodes) {
2440
+ node._selected = false;
2441
+ }
2442
+ for (const link of links) {
2443
+ link._selected = false;
2444
+ }
2445
+ this.isSelecting = false;
2446
+ this.selectionRect = null;
2447
+ selectedNodesSet = new Set();
2448
+ selectedLinksSet = new Set();
2449
+ this.listeners.onSelectionEnd?.call(this, nodes, links);
2450
+ this.tick();
2212
2451
  }
2213
- if (this.forceSettings.forces && this.forceSettings.linkForce) {
2214
- linkForce
2215
- .distance(this.forceSettings.linkDistance)
2216
- .strength(this.forceSettings.linkStrength)
2217
- .iterations(this.forceSettings.linkIterations);
2452
+ function onSelectionEnd(event) {
2453
+ if (localSelection) {
2454
+ event.stopImmediatePropagation();
2455
+ localSelection = false;
2456
+ }
2218
2457
  }
2219
- if (this.forceSettings.forces && this.forceSettings.xForce) {
2220
- this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2458
+ this.area.addEventListener("mousedown", onMouseDown.bind(this), {
2459
+ signal: this.eventAbortController.signal,
2460
+ });
2461
+ this.area.addEventListener("click", onSelectionEnd, { signal: this.eventAbortController.signal });
2462
+ }
2463
+ function normalizeRect(rect) {
2464
+ const x = Math.min(rect.x1, rect.x2);
2465
+ const y = Math.min(rect.y1, rect.y2);
2466
+ const width = Math.abs(rect.x2 - rect.x1);
2467
+ const height = Math.abs(rect.y2 - rect.y1);
2468
+ return { x, y, width, height };
2469
+ }
2470
+ function isNodeInRect(node, rect, cache) {
2471
+ const nx = node.x;
2472
+ const ny = node.y;
2473
+ if (nx == undefined || ny == undefined)
2474
+ return false;
2475
+ if (cache?.shape === "circle") {
2476
+ const r = cache.radius ?? 0;
2477
+ const closestX = Math.max(rect.x, Math.min(nx, rect.x + rect.width));
2478
+ const closestY = Math.max(rect.y, Math.min(ny, rect.y + rect.height));
2479
+ const dx = nx - closestX;
2480
+ const dy = ny - closestY;
2481
+ return dx * dx + dy * dy <= r * r;
2221
2482
  }
2222
- if (this.forceSettings.forces && this.forceSettings.yForce) {
2223
- this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2483
+ const hw = (cache?.width ?? 0) / 2;
2484
+ const hh = (cache?.height ?? 0) / 2;
2485
+ return (nx - hw >= rect.x &&
2486
+ nx + hw <= rect.x + rect.width &&
2487
+ ny - hh >= rect.y &&
2488
+ ny + hh <= rect.y + rect.height);
2489
+ }
2490
+
2491
+ function updateLinkCache() {
2492
+ if (!this.context)
2493
+ return;
2494
+ const current = this.areaTransform.k;
2495
+ if (this.linkSettings.smartCache &&
2496
+ this._lastLinkZoomK !== undefined &&
2497
+ this.linkOptionsCache.length > 0) {
2498
+ const corner = this.linkSettings.linkScaleSwitch;
2499
+ const prev = this._lastLinkZoomK;
2500
+ this._lastLinkZoomK = current;
2501
+ if (!((prev <= corner && current >= corner) || (prev >= corner && current <= corner))) {
2502
+ return;
2503
+ }
2504
+ for (let i = 0; i < this.links.length; i++) {
2505
+ const link = this.links[i];
2506
+ const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2507
+ const cache = this.linkOptionsCache[i];
2508
+ if (cache) {
2509
+ cache.color = linkOptions.color;
2510
+ cache.arrowColor = linkOptions.arrowColor;
2511
+ cache.width = linkOptions.width;
2512
+ }
2513
+ }
2514
+ return;
2224
2515
  }
2225
- if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2226
- this.simulation.force("charge", d3Force.forceManyBody()
2227
- .strength(this.forceSettings.chargeStrength)
2228
- .distanceMax(Infinity)
2229
- .distanceMin(this.forceSettings.chargeDistanceMin));
2516
+ this._lastLinkZoomK = current;
2517
+ this.linkOptionsCache.length = 0;
2518
+ const groupMap = {};
2519
+ const groupSelfMap = {};
2520
+ for (let i = 0; i < this.links.length; i++) {
2521
+ const link = this.links[i];
2522
+ const { sourceId, targetId } = extractLinkPointIds(link);
2523
+ const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2524
+ this.linkOptionsCache[i] = linkOptions;
2525
+ if (this.linkSettings.curve) {
2526
+ const id = `${targetId}${sourceId}`;
2527
+ if (sourceId === targetId) {
2528
+ groupSelfMap[id] ??= 0;
2529
+ link._groupIndex = ++groupSelfMap[id];
2530
+ link._self = true;
2531
+ continue;
2532
+ }
2533
+ const reverseId = `${sourceId}${targetId}`;
2534
+ groupMap[reverseId] ??= 0;
2535
+ groupMap[reverseId]++;
2536
+ groupMap[id] ??= 0;
2537
+ link._groupIndex = indexToPosition(++groupMap[id]);
2538
+ link._self = false;
2539
+ }
2230
2540
  }
2231
- if (this.forceSettings.forces && this.forceSettings.centerForce) {
2232
- this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2541
+ if (this.linkSettings.curve) {
2542
+ for (const link of this.links) {
2543
+ const { sourceId, targetId } = extractLinkPointIds(link);
2544
+ const id = `${targetId}${sourceId}`;
2545
+ link._groupSize = link._self ? (groupSelfMap[id] ?? 1) : (groupMap[id] ?? 1);
2546
+ }
2233
2547
  }
2234
- initCollideForce.call(this, true);
2235
2548
  }
2236
- function initCollideForce(forceUpdate) {
2237
- if (!this.simulation)
2549
+ function indexToPosition(n) {
2550
+ if (n === 1)
2551
+ return 0;
2552
+ const index = n >> 1;
2553
+ return n & 1 ? -(index * 2) : index * 2;
2554
+ // return n & 1 ? -(n >> 1) : n >> 1;
2555
+ }
2556
+
2557
+ function updateNodeCache() {
2558
+ if (!this.context)
2238
2559
  return;
2239
- if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2240
- this.simulation.force("collide")) {
2241
- this.simulation.force("collide", null);
2242
- }
2243
- if (this.forceSettings.forces && this.forceSettings.collideForce) {
2244
- const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2245
- const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2246
- const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2247
- if (isMaxCollideNodes && isMaxCollideLinks) {
2248
- this.simulation.force("collide", null);
2249
- }
2250
- else if (!this.simulation.force("collide") || forceUpdate) {
2251
- this.simulation.force("collide", d3Force.forceCollide()
2252
- .radius((node, index) => {
2253
- const nodeOptions = this.nodeOptionsCache[node.id];
2254
- if (!nodeOptions)
2255
- return 0;
2256
- switch (nodeOptions.shape) {
2257
- case "circle": {
2258
- if (this.forceSettings.collideRadius) {
2259
- return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2260
- }
2261
- return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2262
- }
2263
- case "square": {
2264
- return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2265
- this.forceSettings.collideAdditionalRadius);
2266
- }
2267
- case "text": {
2268
- return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2269
- this.forceSettings.collideAdditionalRadius);
2270
- }
2271
- default: {
2272
- if (this.forceSettings.collideRadius) {
2273
- return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2274
- }
2275
- return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2276
- }
2277
- }
2278
- })
2279
- .strength(this.forceSettings.collideStrength)
2280
- .iterations(this.forceSettings.collideIterations));
2560
+ const currentZoom = this.areaTransform.k;
2561
+ const options = this.nodeOptionsCache.length === 0;
2562
+ const label = this.cachedNodeLabel.length === 0;
2563
+ let text = this.cachedNodeText.length === 0;
2564
+ if (this.nodeSettings.smartCache &&
2565
+ this._lastNodeZoomK !== undefined &&
2566
+ !options &&
2567
+ !text &&
2568
+ !label) {
2569
+ const thresholds = this.nodeSettings.textScaleSteps;
2570
+ if (thresholds && thresholds.length > 0) {
2571
+ const min = Math.min(this._lastNodeZoomK, currentZoom);
2572
+ const max = Math.max(this._lastNodeZoomK, currentZoom);
2573
+ const crossed = thresholds.some((t) => t >= min && t <= max);
2574
+ this._lastNodeZoomK = currentZoom;
2575
+ if (!crossed)
2576
+ return;
2281
2577
  }
2578
+ this.cachedNodeText.length = 0;
2579
+ text = true;
2282
2580
  }
2283
- }
2284
-
2285
- function updateLinkCache() {
2286
- this.linkOptionsCache = {};
2287
- for (let i = 0; i < this.links.length; i++) {
2288
- const link = this.links[i];
2289
- const { sourceId, targetId } = extractLinkPointIds(link);
2290
- const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2291
- const id = `${targetId}${sourceId}`;
2292
- this.linkOptionsCache[id] = linkOptions;
2293
- }
2294
- }
2295
-
2296
- function updateNodeCache(types) {
2297
- this.nodeOptionsCache = {};
2298
- if (!this.context)
2299
- return;
2581
+ this._lastNodeZoomK = currentZoom;
2300
2582
  for (let i = 0; i < this.nodes.length; i++) {
2301
2583
  const node = this.nodes[i];
2302
- if (types === true || (jsHelpers.isObject(types) && types.options)) {
2584
+ if (options) {
2303
2585
  const nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2304
2586
  const radius = nodeOptions.shape === "circle"
2305
2587
  ? nodeRadiusGetter({
@@ -2350,11 +2632,19 @@ function updateNodeCache(types) {
2350
2632
  nodeOptions.height = node.visible === false ? 0 : height;
2351
2633
  nodeOptions.radius = node.visible === false ? 0 : radius;
2352
2634
  nodeOptions.labelSize = labelSize;
2353
- this.nodeOptionsCache[node.id] = nodeOptions;
2635
+ this.nodeOptionsCache[i] = nodeOptions;
2354
2636
  }
2355
- const nodeOptions = this.nodeOptionsCache[node.id];
2637
+ let nodeOptions = this.nodeOptionsCache[i];
2638
+ if (!nodeOptions)
2639
+ continue;
2356
2640
  /** label */
2357
- if (types === true || (jsHelpers.isObject(types) && types.label)) {
2641
+ if (label) {
2642
+ if (!options) {
2643
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2644
+ const cache = this.nodeOptionsCache[i];
2645
+ if (cache)
2646
+ cache.label = nodeOptions.label;
2647
+ }
2358
2648
  /** label in not text shape */
2359
2649
  if (nodeOptions.shape !== "text" && nodeOptions.label) {
2360
2650
  this.context.font = `${nodeOptions.labelStyle} normal ${nodeOptions.labelWeight} ${nodeOptions.labelSize}px ${nodeOptions.labelFont}`;
@@ -2362,7 +2652,7 @@ function updateNodeCache(types) {
2362
2652
  this.context.textAlign = nodeOptions.labelAlign;
2363
2653
  if (nodeOptions.labelWidth == undefined ||
2364
2654
  this.context.measureText(nodeOptions.label).width <= nodeOptions.labelWidth) {
2365
- this.cachedNodeLabel[node.id] = [nodeOptions.label];
2655
+ this.cachedNodeLabel[i] = [nodeOptions.label];
2366
2656
  }
2367
2657
  const { lines } = getTextLines({
2368
2658
  context: this.context,
@@ -2375,7 +2665,7 @@ function updateNodeCache(types) {
2375
2665
  textStyle: nodeOptions.labelStyle,
2376
2666
  textWeight: nodeOptions.labelWeight,
2377
2667
  });
2378
- this.cachedNodeLabel[node.id] = lines;
2668
+ this.cachedNodeLabel[i] = lines;
2379
2669
  /** label in text shape */
2380
2670
  }
2381
2671
  else if (nodeOptions.shape === "text" && nodeOptions.label) {
@@ -2399,18 +2689,29 @@ function updateNodeCache(types) {
2399
2689
  (lines.length - 1) * nodeOptions.labelGap +
2400
2690
  nodeOptions.labelYPadding;
2401
2691
  nodeOptions.width = maxSize + nodeOptions.labelXPadding;
2402
- this.cachedNodeLabel[node.id] = lines;
2692
+ this.cachedNodeLabel[i] = lines;
2693
+ }
2694
+ else {
2695
+ this.cachedNodeLabel[i] = [];
2403
2696
  }
2404
2697
  }
2405
2698
  /** text */
2406
- if (nodeOptions.text && (types === true || (jsHelpers.isObject(types) && types.text))) {
2699
+ if (text) {
2700
+ if (!options) {
2701
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2702
+ const cache = this.nodeOptionsCache[i];
2703
+ if (cache) {
2704
+ cache.text = nodeOptions.text;
2705
+ cache.textVisible = nodeOptions.textVisible;
2706
+ }
2707
+ }
2708
+ if (!nodeOptions.text) {
2709
+ this.cachedNodeText[i] = [];
2710
+ return;
2711
+ }
2407
2712
  this.context.font = `${nodeOptions.textStyle} normal ${nodeOptions.textWeight} ${nodeOptions.textSize}px ${nodeOptions.textFont}`;
2408
2713
  this.context.fillStyle = nodeOptions.textColor;
2409
2714
  this.context.textAlign = nodeOptions.textAlign;
2410
- if (nodeOptions.textWidth == undefined ||
2411
- this.context.measureText(nodeOptions.text).width <= nodeOptions.textWidth) {
2412
- this.cachedNodeText[node.id] = [nodeOptions.text];
2413
- }
2414
2715
  const { lines } = getTextLines({
2415
2716
  context: this.context,
2416
2717
  maxWidth: nodeOptions.textWidth,
@@ -2422,49 +2723,233 @@ function updateNodeCache(types) {
2422
2723
  textStyle: nodeOptions.textStyle,
2423
2724
  textWeight: nodeOptions.textWeight,
2424
2725
  });
2425
- this.cachedNodeText[node.id] = lines;
2726
+ this.cachedNodeText[i] = lines;
2426
2727
  }
2427
2728
  }
2428
2729
  }
2429
2730
 
2731
+ const DEFAULT_ZOOM_EXTENT = [0.3, 10];
2732
+ const DEFAULT_TRANSLATE_EXTENT_COEFFICIENT = 8;
2733
+ const DEFAULT_MARGIN = 0.25;
2430
2734
  function initZoom(currentZoom) {
2431
2735
  if (!this.area)
2432
2736
  throw new Error("bad init data");
2737
+ const bounds = computeGraphBounds(this.nodes);
2738
+ const zoomExtent = resolveZoomExtent(this.graphSettings.zoomExtent, this.width, this.height, this.graphSettings.zoomExtentMargin ?? DEFAULT_MARGIN, bounds);
2739
+ const translateExtent = resolveTranslateExtent(this.graphSettings.translateExtent, this.graphSettings.translateExtentCoefficient, this.graphSettings.translateExtentOverlap, this.width, this.height, zoomExtent, bounds);
2740
+ this._translateExtent = translateExtent;
2433
2741
  const zoomInstance = d3Zoom.zoom()
2434
- .scaleExtent(this.graphSettings.zoomExtent)
2742
+ .scaleExtent(zoomExtent)
2743
+ .filter((event) => {
2744
+ return !(event instanceof MouseEvent && event.shiftKey);
2745
+ })
2435
2746
  .on("zoom", (event) => {
2747
+ if (this._zoomAnimating)
2748
+ return;
2436
2749
  this.listeners.onZoom?.call?.(this, event);
2437
2750
  const oldTransform = this.areaTransform;
2438
2751
  this.areaTransform = event.transform;
2439
2752
  if (this.areaTransform.k !== oldTransform.k) {
2440
2753
  updateLinkCache.call(this);
2441
- updateNodeCache.call(this, true);
2754
+ updateNodeCache.call(this);
2442
2755
  }
2443
- if (!this.simulationWorking && !this.highlightWorking)
2444
- requestAnimationFrame(() => this.draw());
2756
+ this.tick();
2445
2757
  });
2446
- if (this.graphSettings.translateExtentEnable) {
2447
- const coefficient = this.graphSettings.translateExtentCoefficient;
2448
- const [coefficientX, coefficientY] = jsHelpers.isArray(coefficient)
2449
- ? coefficient
2450
- : [coefficient, coefficient];
2451
- const [[minX = -this.width * coefficientX, minY = -this.height * coefficientY], [maxX = this.width * coefficientX, maxY = this.height * coefficientY],] = this.graphSettings.translateExtent;
2452
- zoomInstance.translateExtent([
2453
- [minX, minY],
2454
- [maxX, maxY],
2455
- ]);
2456
- }
2758
+ zoomInstance.translateExtent(translateExtent);
2457
2759
  d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2458
2760
  const zoomInitial = currentZoom ?? this.graphSettings.zoomInitial;
2459
- this.areaTransform = new d3Zoom.ZoomTransform(zoomInitial?.k ?? 1, zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2761
+ this.areaTransform = new d3Zoom.ZoomTransform(Math.max(zoomInitial?.k ?? 1, zoomExtent[0]), zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2460
2762
  d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
2461
2763
  }
2764
+ function resolveZoomExtent(userExtent, viewWidth, viewHeight, margin, bounds) {
2765
+ if (userExtent &&
2766
+ userExtent.length === 2 &&
2767
+ userExtent[0] != undefined &&
2768
+ userExtent[1] != undefined) {
2769
+ return userExtent;
2770
+ }
2771
+ if (!bounds)
2772
+ return DEFAULT_ZOOM_EXTENT;
2773
+ const graphWidth = bounds.maxX - bounds.minX;
2774
+ const graphHeight = bounds.maxY - bounds.minY;
2775
+ if (graphWidth === 0 || graphHeight === 0)
2776
+ return DEFAULT_ZOOM_EXTENT;
2777
+ const scaleToFit = (1 - margin) / Math.max(graphWidth / viewWidth, graphHeight / viewHeight);
2778
+ return [scaleToFit, DEFAULT_ZOOM_EXTENT[1]];
2779
+ }
2780
+ function resolveTranslateExtent(userExtent, userCoefficient, overlap, viewWidth, viewHeight, zoomExtent, bounds) {
2781
+ if (userExtent && userExtent.length >= 2 && jsHelpers.isArray(userExtent[0]) && jsHelpers.isArray(userExtent[1])) {
2782
+ return [
2783
+ [
2784
+ userExtent[0][0] ?? -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2785
+ userExtent[0][1] ?? -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2786
+ ],
2787
+ [
2788
+ userExtent[1][0] ?? viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2789
+ userExtent[1][1] ?? viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2790
+ ],
2791
+ ];
2792
+ }
2793
+ if (userCoefficient) {
2794
+ const [cx, cy] = jsHelpers.isArray(userCoefficient)
2795
+ ? userCoefficient
2796
+ : [userCoefficient, userCoefficient];
2797
+ return [
2798
+ [-viewWidth * cx, -viewHeight * cy],
2799
+ [viewWidth * cx, viewHeight * cy],
2800
+ ];
2801
+ }
2802
+ if (!bounds) {
2803
+ return [
2804
+ [
2805
+ -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2806
+ -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2807
+ ],
2808
+ [
2809
+ viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2810
+ viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2811
+ ],
2812
+ ];
2813
+ }
2814
+ const minZoom = zoomExtent[0];
2815
+ const zoomViewWidth = viewWidth / minZoom;
2816
+ const zoomViewHeight = viewHeight / minZoom;
2817
+ const graphWidth = bounds.maxX - bounds.minX;
2818
+ const graphHeight = bounds.maxY - bounds.minY;
2819
+ const minX = bounds.minX + overlap * graphWidth - zoomViewWidth;
2820
+ const maxX = bounds.maxX - overlap * graphWidth + zoomViewWidth;
2821
+ const minY = bounds.minY + overlap * graphHeight - zoomViewHeight;
2822
+ const maxY = bounds.maxY - overlap * graphHeight + zoomViewHeight;
2823
+ return [
2824
+ [minX, minY],
2825
+ [maxX, maxY],
2826
+ ];
2827
+ }
2828
+
2829
+ function initSimulation() {
2830
+ if (!this.simulation) {
2831
+ this.simulation = d3Force.forceSimulation()
2832
+ .nodes(this.nodes)
2833
+ .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2834
+ .on("tick", () => {
2835
+ this.draw();
2836
+ })
2837
+ .on("end", () => {
2838
+ this.listeners.onSimulationEnd?.call?.(this);
2839
+ if (this.area) {
2840
+ initZoom.call(this, this.areaTransform);
2841
+ }
2842
+ if (this.graphSettings.showDrawTime) {
2843
+ getDrawTime();
2844
+ // eslint-disable-next-line no-console
2845
+ console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2846
+ resetDrawTime();
2847
+ }
2848
+ })
2849
+ .stop();
2850
+ initSimulationForces.call(this);
2851
+ }
2852
+ }
2853
+ function initSimulationForces() {
2854
+ if (!this.simulation)
2855
+ return;
2856
+ const linkForce = this.simulation.force("link");
2857
+ if (!linkForce)
2858
+ return;
2859
+ if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2860
+ this.simulation.force("x", null);
2861
+ }
2862
+ if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2863
+ this.simulation.force("y", null);
2864
+ }
2865
+ if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2866
+ this.simulation.force("charge")) {
2867
+ this.simulation.force("charge", null);
2868
+ }
2869
+ if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2870
+ this.simulation.force("center")) {
2871
+ this.simulation.force("center", null);
2872
+ }
2873
+ if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2874
+ linkForce.distance(0).strength(0).iterations(0);
2875
+ }
2876
+ if (this.forceSettings.forces && this.forceSettings.linkForce) {
2877
+ linkForce
2878
+ .distance(this.forceSettings.linkDistance)
2879
+ .strength(this.forceSettings.linkStrength)
2880
+ .iterations(this.forceSettings.linkIterations);
2881
+ }
2882
+ if (this.forceSettings.forces && this.forceSettings.xForce) {
2883
+ this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2884
+ }
2885
+ if (this.forceSettings.forces && this.forceSettings.yForce) {
2886
+ this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2887
+ }
2888
+ if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2889
+ this.simulation.force("charge", d3Force.forceManyBody()
2890
+ .strength(this.forceSettings.chargeStrength)
2891
+ .distanceMax(Infinity)
2892
+ .distanceMin(this.forceSettings.chargeDistanceMin));
2893
+ }
2894
+ if (this.forceSettings.forces && this.forceSettings.centerForce) {
2895
+ this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2896
+ }
2897
+ initCollideForce.call(this, true);
2898
+ }
2899
+ function initCollideForce(forceUpdate) {
2900
+ if (!this.simulation)
2901
+ return;
2902
+ if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2903
+ this.simulation.force("collide")) {
2904
+ this.simulation.force("collide", null);
2905
+ }
2906
+ if (this.forceSettings.forces && this.forceSettings.collideForce) {
2907
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2908
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2909
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2910
+ if (isMaxCollideNodes && isMaxCollideLinks) {
2911
+ this.simulation.force("collide", null);
2912
+ }
2913
+ else if (!this.simulation.force("collide") || forceUpdate) {
2914
+ this.simulation.force("collide", d3Force.forceCollide()
2915
+ .radius((node, index) => {
2916
+ const nodeOptions = this.nodeOptionsCache[index];
2917
+ if (!nodeOptions)
2918
+ return 0;
2919
+ switch (nodeOptions.shape) {
2920
+ case "circle": {
2921
+ if (this.forceSettings.collideRadius) {
2922
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2923
+ }
2924
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2925
+ }
2926
+ case "square": {
2927
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2928
+ this.forceSettings.collideAdditionalRadius);
2929
+ }
2930
+ case "text": {
2931
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2932
+ this.forceSettings.collideAdditionalRadius);
2933
+ }
2934
+ default: {
2935
+ if (this.forceSettings.collideRadius) {
2936
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2937
+ }
2938
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2939
+ }
2940
+ }
2941
+ })
2942
+ .strength(this.forceSettings.collideStrength)
2943
+ .iterations(this.forceSettings.collideIterations));
2944
+ }
2945
+ }
2946
+ }
2462
2947
 
2463
2948
  class GraphCanvas {
2464
2949
  /** initial data */
2465
2950
  nodes;
2466
2951
  links;
2467
- particles = {};
2952
+ particles = [];
2468
2953
  width;
2469
2954
  height;
2470
2955
  root;
@@ -2481,19 +2966,30 @@ class GraphCanvas {
2481
2966
  context;
2482
2967
  simulation;
2483
2968
  areaTransform = d3Zoom.zoomIdentity;
2484
- areaRect;
2969
+ _translateExtent;
2485
2970
  draw;
2486
2971
  eventAbortController;
2487
- cachedNodeText = {};
2488
- cachedNodeLabel = {};
2489
- linkOptionsCache = {};
2490
- nodeOptionsCache = {};
2972
+ cachedNodeText = [];
2973
+ cachedNodeLabel = [];
2974
+ linkOptionsCache = [];
2975
+ nodeOptionsCache = [];
2491
2976
  isDragging = false;
2492
2977
  highlightedNode = null;
2493
2978
  highlightedLink = null;
2494
2979
  highlightedNeighbors = null;
2495
2980
  highlightProgress = 1;
2496
- highlightWorking = false;
2981
+ highlightStart = null;
2982
+ highlightPositive = false;
2983
+ highlightController;
2984
+ _lastNodeZoomK;
2985
+ _lastLinkZoomK;
2986
+ _zoomAnimating = false;
2987
+ isSelecting = false;
2988
+ selectionRect = null;
2989
+ // protected areaRect: DOMRect | undefined;
2990
+ get areaRect() {
2991
+ return this.area?.getBoundingClientRect();
2992
+ }
2497
2993
  get simulationWorking() {
2498
2994
  const simulationAlpha = this.simulation?.alpha?.() ?? 0;
2499
2995
  const simulationAlphaMin = this.simulation?.alphaMin?.() ?? 0;
@@ -2522,21 +3018,72 @@ class GraphCanvas {
2522
3018
  get dpi() {
2523
3019
  return devicePixelRatio;
2524
3020
  }
2525
- getData() {
3021
+ getData = () => {
2526
3022
  return {
2527
3023
  links: this.links,
2528
3024
  nodes: this.nodes,
2529
3025
  };
2530
- }
2531
- changeData(options, alpha = 0.5, clearCache = true) {
3026
+ };
3027
+ fitToView = (margin = this.graphSettings.zoomToFitMargin, duration = this.graphSettings.zoomAnimationDuration) => {
3028
+ const area = this.area;
3029
+ if (!area)
3030
+ return;
3031
+ const bounds = computeGraphBounds(this.nodes);
3032
+ if (!bounds)
3033
+ return;
3034
+ const graphWidth = bounds.maxX - bounds.minX;
3035
+ const graphHeight = bounds.maxY - bounds.minY;
3036
+ if (graphWidth === 0 || graphHeight === 0)
3037
+ return;
3038
+ const graphCenterX = bounds.minX + graphWidth / 2;
3039
+ const graphCenterY = bounds.minY + graphHeight / 2;
3040
+ const scale = (1 - margin) / Math.max(graphWidth / this.width, graphHeight / this.height);
3041
+ const clampedScale = Math.min(scale, this.graphSettings.zoomExtent?.[1] ?? scale);
3042
+ const target = d3Zoom.zoomIdentity
3043
+ .translate(this.width / 2, this.height / 2)
3044
+ .scale(clampedScale)
3045
+ .translate(-graphCenterX, -graphCenterY);
3046
+ if (!this.graphSettings.zoomAnimation) {
3047
+ this.areaTransform = target;
3048
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
3049
+ this.clearCache(true);
3050
+ this.tick();
3051
+ return;
3052
+ }
3053
+ this.animateZoom(area, target, this.areaTransform, duration);
3054
+ };
3055
+ focusOnNode = (nodeId, scale = this.graphSettings.zoomToNodeScale, duration = this.graphSettings.zoomAnimationDuration) => {
3056
+ const area = this.area;
3057
+ if (!area)
3058
+ return;
3059
+ const node = this.nodes.find((n) => n.id === nodeId);
3060
+ if (!node)
3061
+ return;
3062
+ if (node.x == undefined || node.y == undefined)
3063
+ return;
3064
+ const target = d3Zoom.zoomIdentity
3065
+ .translate(this.width / 2, this.height / 2)
3066
+ .scale(scale)
3067
+ .translate(-node.x, -node.y);
3068
+ if (!this.graphSettings.zoomAnimation) {
3069
+ this.areaTransform = target;
3070
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
3071
+ this.clearCache(true);
3072
+ this.tick();
3073
+ return;
3074
+ }
3075
+ this.animateZoom(area, target, this.areaTransform, duration);
3076
+ };
3077
+ changeData = (options, alpha = 0.5, clearCache = true, precompute = false) => {
2532
3078
  if (options.links != undefined)
2533
3079
  this.links = options.links;
2534
3080
  if (options.nodes != undefined)
2535
3081
  this.nodes = options.nodes;
2536
- if (options.nodes != undefined || options.links != undefined)
2537
- this.updateData(alpha, clearCache);
2538
- }
2539
- changeSettings(options, clearCache = true) {
3082
+ if (options.nodes != undefined || options.links != undefined) {
3083
+ this.updateData(alpha, clearCache, precompute);
3084
+ }
3085
+ };
3086
+ changeSettings = (options, clearCache = true, precompute = false) => {
2540
3087
  if (options.graphSettings) {
2541
3088
  this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
2542
3089
  this.draw = initDraw.call(this);
@@ -2583,16 +3130,15 @@ class GraphCanvas {
2583
3130
  initCollideForce.call(this, true);
2584
3131
  }
2585
3132
  if (options.forceSettings) {
2586
- return void this.updateSimulation();
3133
+ return void this.updateSimulation(precompute);
2587
3134
  }
2588
3135
  this.tick();
2589
- }
2590
- updateRect() {
2591
- if (!this.area)
2592
- return;
2593
- this.areaRect = this.area.getBoundingClientRect();
2594
- }
2595
- updateSize() {
3136
+ };
3137
+ updateRect = () => {
3138
+ // if (!this.area) return;
3139
+ // this.areaRect = this.area.getBoundingClientRect();
3140
+ };
3141
+ updateSize = () => {
2596
3142
  if (!this.area)
2597
3143
  return;
2598
3144
  const { width, height } = this.root.getBoundingClientRect();
@@ -2600,57 +3146,102 @@ class GraphCanvas {
2600
3146
  this.height = height;
2601
3147
  this.area.width = this.dpi * this.width;
2602
3148
  this.area.height = this.dpi * this.height;
2603
- this.areaRect = this.area.getBoundingClientRect();
3149
+ this.updateRect();
2604
3150
  this.context = this.area.getContext("2d");
2605
3151
  if (!this.context)
2606
3152
  throw new Error("couldn't create canvas context");
2607
3153
  this.context.scale(this.dpi, this.dpi);
2608
3154
  this.draw();
2609
- }
2610
- clearCache(keys) {
3155
+ };
3156
+ clearCache = (keys) => {
2611
3157
  if (keys === true) {
2612
- updateNodeCache.call(this, true);
2613
- updateLinkCache.call(this);
3158
+ this.nodeOptionsCache.length = 0;
3159
+ this.linkOptionsCache.length = 0;
3160
+ this.cachedNodeLabel.length = 0;
3161
+ this.cachedNodeText.length = 0;
2614
3162
  }
2615
3163
  else if (jsHelpers.isArray(keys)) {
2616
- if (keys.some((k) => k === GRAPH_CACHE_TYPE.NodeText ||
2617
- k === GRAPH_CACHE_TYPE.NodeLabel ||
2618
- k === GRAPH_CACHE_TYPE.NodeOptions)) {
2619
- updateNodeCache.call(this, {
2620
- label: keys.includes(GRAPH_CACHE_TYPE.NodeLabel),
2621
- options: keys.includes(GRAPH_CACHE_TYPE.NodeOptions),
2622
- text: keys.includes(GRAPH_CACHE_TYPE.NodeOptions),
2623
- });
2624
- }
2625
- if (keys.includes(GRAPH_CACHE_TYPE.LinkOptions)) {
2626
- updateLinkCache.call(this);
3164
+ for (const key of keys) {
3165
+ switch (key) {
3166
+ case GRAPH_CACHE_TYPE.NodeText: {
3167
+ this.cachedNodeText.length = 0;
3168
+ break;
3169
+ }
3170
+ case GRAPH_CACHE_TYPE.NodeLabel: {
3171
+ this.cachedNodeLabel.length = 0;
3172
+ break;
3173
+ }
3174
+ case GRAPH_CACHE_TYPE.NodeOptions: {
3175
+ this.nodeOptionsCache.length = 0;
3176
+ break;
3177
+ }
3178
+ case GRAPH_CACHE_TYPE.LinkOptions: {
3179
+ this.linkOptionsCache.length = 0;
3180
+ break;
3181
+ }
3182
+ }
2627
3183
  }
2628
3184
  }
2629
- }
2630
- tick() {
2631
- if (!this.simulationWorking && !this.highlightWorking)
3185
+ updateNodeCache.call(this);
3186
+ updateLinkCache.call(this);
3187
+ };
3188
+ tick = () => {
3189
+ if (!this.simulationWorking)
2632
3190
  this.draw();
2633
- }
2634
- restart(alpha) {
2635
- if (this.simulation)
3191
+ };
3192
+ restart = (alpha, options) => {
3193
+ if (!this.simulation)
3194
+ return;
3195
+ const settings = {
3196
+ precompute: options?.precompute ?? this.forceSettings.precompute,
3197
+ precomputeMaxTimeMs: options?.precomputeMaxTimeMs ?? this.forceSettings?.precomputeMaxTimeMs,
3198
+ precomputeMaxTicks: options?.precomputeMaxTicks ?? this.forceSettings?.precomputeMaxTicks ?? 300,
3199
+ precomputeDisableForcesAfter: options?.precomputeDisableForcesAfter ??
3200
+ this.forceSettings?.precomputeDisableForcesAfter ??
3201
+ false,
3202
+ };
3203
+ if (!settings.precompute) {
2636
3204
  this.simulation.alpha(alpha ?? 1).restart();
2637
- }
2638
- start() {
3205
+ return;
3206
+ }
3207
+ if (settings.precomputeDisableForcesAfter) {
3208
+ this.forceSettings = forceSettingsGetter({ forces: true }, this.forceSettings);
3209
+ }
3210
+ initSimulationForces.call(this);
3211
+ this.simulation.stop();
3212
+ this.simulation.alpha(alpha ?? 1);
3213
+ const startTime = performance.now();
3214
+ let ticks = 0;
3215
+ while (performance.now() - startTime < settings.precomputeMaxTimeMs &&
3216
+ ticks < settings.precomputeMaxTicks) {
3217
+ this.simulation.tick(1);
3218
+ ticks++;
3219
+ if (this.simulation.alpha() <= this.simulation.alphaMin())
3220
+ break;
3221
+ }
3222
+ if (settings.precomputeDisableForcesAfter) {
3223
+ this.forceSettings = forceSettingsGetter({ forces: false }, this.forceSettings);
3224
+ }
3225
+ initSimulationForces.call(this);
3226
+ this.simulation.restart();
3227
+ this.tick();
3228
+ };
3229
+ start = () => {
2639
3230
  if (this.simulation)
2640
3231
  this.simulation.alpha(1).restart();
2641
3232
  if (this.container)
2642
3233
  this.container.style.display = "block";
2643
- }
2644
- stop() {
3234
+ };
3235
+ stop = () => {
2645
3236
  if (this.simulation)
2646
3237
  this.simulation.stop();
2647
3238
  if (this.container)
2648
3239
  this.container.style.display = "none";
2649
- }
2650
- create() {
3240
+ };
3241
+ create = () => {
2651
3242
  this.init();
2652
- }
2653
- destroy() {
3243
+ };
3244
+ destroy = () => {
2654
3245
  if (this.simulation) {
2655
3246
  this.simulation.stop();
2656
3247
  this.simulation = undefined;
@@ -2658,39 +3249,79 @@ class GraphCanvas {
2658
3249
  this.clearHTMLElements();
2659
3250
  this.clearState();
2660
3251
  this.clearCache(true);
2661
- }
2662
- clearHTMLElements() {
3252
+ };
3253
+ getPointerAreaPosition = (event) => {
3254
+ let localX;
3255
+ let localY;
3256
+ if ("offsetX" in event) {
3257
+ // localX = event.offsetX;
3258
+ // localY = event.offsetY;
3259
+ const rect = this.areaRect;
3260
+ if (!rect)
3261
+ return [0, 0];
3262
+ localX = event.clientX - rect.left;
3263
+ localY = event.clientY - rect.top;
3264
+ }
3265
+ else {
3266
+ const rect = this.areaRect;
3267
+ if (!rect)
3268
+ return [0, 0];
3269
+ const clientX = event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX;
3270
+ const clientY = event.touches[0]?.clientY ?? event.changedTouches[0]?.clientY;
3271
+ localX = clientX - rect.left;
3272
+ localY = clientY - rect.top;
3273
+ }
3274
+ const px = (localX - this.areaTransform.x) / this.areaTransform.k;
3275
+ const py = (localY - this.areaTransform.y) / this.areaTransform.k;
3276
+ return [px, py];
3277
+ };
3278
+ clearHTMLElements = () => {
2663
3279
  this.root.replaceChildren();
2664
3280
  this.area = undefined;
2665
3281
  this.context = undefined;
2666
3282
  this.container = undefined;
2667
3283
  this.eventAbortController.abort();
2668
3284
  this.eventAbortController = new AbortController();
2669
- }
2670
- updateSimulation() {
3285
+ };
3286
+ updateSimulation = (precompute = false) => {
2671
3287
  if (this.simulation) {
2672
3288
  initSimulationForces.call(this);
2673
- this.simulation.alpha(1);
2674
- this.simulation.restart();
3289
+ this.restart(1, { precompute });
2675
3290
  }
2676
- }
2677
- clearState() {
3291
+ };
3292
+ clearState = () => {
2678
3293
  this.isDragging = false;
2679
3294
  this.highlightedNode = null;
2680
3295
  this.highlightedLink = null;
2681
3296
  this.highlightedNeighbors = null;
2682
3297
  this.highlightProgress = 0;
2683
- this.highlightWorking = false;
2684
- }
2685
- updateData(alpha = 0.5, clearCache = true) {
3298
+ this.highlightStart = null;
3299
+ this.highlightPositive = false;
3300
+ this.isSelecting = false;
3301
+ this.selectionRect = null;
3302
+ };
3303
+ init = () => {
3304
+ initArea.call(this);
3305
+ updateNodeCache.call(this);
3306
+ updateLinkCache.call(this);
3307
+ initSimulation.call(this);
3308
+ this.restart(1);
3309
+ initDnd.call(this);
3310
+ initZoom.call(this);
3311
+ initResize.call(this);
3312
+ initSelection.call(this);
3313
+ initPointer.call(this);
3314
+ updateNodeCache.call(this);
3315
+ updateLinkCache.call(this);
3316
+ this.tick();
3317
+ };
3318
+ updateData = (alpha = 0.5, clearCache = true, precompute = false) => {
2686
3319
  if (clearCache) {
2687
3320
  this.clearCache(clearCache);
2688
3321
  }
2689
3322
  if (this.simulation) {
2690
3323
  initCollideForce.call(this, false);
2691
- this.simulation
2692
- .nodes(this.nodes)
2693
- .force("link", d3Force.forceLink(this.links)
3324
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2694
3325
  .id(this.nodeSettings.idGetter.bind(this))
2695
3326
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2696
3327
  ? this.forceSettings.linkDistance
@@ -2700,21 +3331,95 @@ class GraphCanvas {
2700
3331
  : 0)
2701
3332
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2702
3333
  ? this.forceSettings.linkIterations
2703
- : 0))
2704
- .alpha(alpha)
2705
- .restart();
3334
+ : 0));
3335
+ this.restart(alpha, { precompute });
3336
+ initZoom.call(this, this.areaTransform);
2706
3337
  }
2707
- }
2708
- init() {
2709
- initArea.call(this);
2710
- updateNodeCache.call(this, true);
2711
- updateLinkCache.call(this);
2712
- initSimulation.call(this);
2713
- initDnd.call(this);
2714
- initZoom.call(this);
2715
- initResize.call(this);
2716
- initPointer.call(this);
2717
- }
3338
+ };
3339
+ animateHighlight = (node, link, baseDuration = this.highlightSettings.highlightDuration) => {
3340
+ let positive = true;
3341
+ if (node && (this.highlightedNode !== node || !this.highlightPositive)) {
3342
+ this.highlightedNode = node;
3343
+ this.highlightedNeighbors = new Set(node.neighbors);
3344
+ this.particles = [];
3345
+ this.highlightedLink = null;
3346
+ this.highlightStart = performance.now();
3347
+ }
3348
+ else if (link && (this.highlightedLink !== link || !this.highlightPositive)) {
3349
+ const { sourceId, targetId } = extractLinkPointIds(link);
3350
+ this.highlightProgress = 0;
3351
+ this.highlightedLink = link;
3352
+ this.highlightedNeighbors = new Set([sourceId, targetId]);
3353
+ this.particles = [];
3354
+ this.highlightedNode = null;
3355
+ this.highlightStart = performance.now();
3356
+ }
3357
+ else if (!node && !link && this.highlightPositive) {
3358
+ positive = false;
3359
+ }
3360
+ else {
3361
+ return;
3362
+ }
3363
+ if (this.highlightController) {
3364
+ this.highlightController.abort();
3365
+ }
3366
+ const controller = new AbortController();
3367
+ this.highlightPositive = positive;
3368
+ this.highlightController = controller;
3369
+ const startTime = performance.now();
3370
+ const startProgress = this.highlightProgress;
3371
+ const targetProgress = positive ? 1 : 0;
3372
+ const delta = targetProgress - startProgress;
3373
+ const duration = baseDuration * Math.abs(delta);
3374
+ const animate = () => {
3375
+ if (controller.signal.aborted)
3376
+ return;
3377
+ const elapsed = performance.now() - startTime;
3378
+ const t = Math.min(elapsed / duration, 1);
3379
+ const current = startProgress + delta * t;
3380
+ const eased = current < 0.5 ? 4 * current * current * current : 1 - (-2 * current + 2) ** 3 / 2;
3381
+ this.highlightProgress = eased;
3382
+ if (t < 1 || positive) {
3383
+ requestAnimationFrame(animate);
3384
+ this.draw();
3385
+ }
3386
+ else {
3387
+ this.highlightedNode = null;
3388
+ this.highlightedLink = null;
3389
+ this.highlightedNeighbors = null;
3390
+ this.highlightStart = null;
3391
+ this.particles = [];
3392
+ this.tick();
3393
+ }
3394
+ };
3395
+ requestAnimationFrame(animate);
3396
+ };
3397
+ animateZoom = (area, target, start, duration) => {
3398
+ this._zoomAnimating = true;
3399
+ const startTime = performance.now();
3400
+ const animate = () => {
3401
+ const elapsed = performance.now() - startTime;
3402
+ const t = Math.min(elapsed / duration, 1);
3403
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
3404
+ const x = start.x + (target.x - start.x) * eased;
3405
+ const y = start.y + (target.y - start.y) * eased;
3406
+ const k = start.k + (target.k - start.k) * eased;
3407
+ this.areaTransform = new d3Zoom.ZoomTransform(k, x, y);
3408
+ d3Zoom.zoom().transform(d3Selection.select(area), this.areaTransform);
3409
+ updateLinkCache.call(this);
3410
+ updateNodeCache.call(this);
3411
+ this.tick();
3412
+ if (t < 1) {
3413
+ requestAnimationFrame(animate);
3414
+ }
3415
+ else {
3416
+ this._zoomAnimating = false;
3417
+ this.clearCache(true);
3418
+ this.tick();
3419
+ }
3420
+ };
3421
+ requestAnimationFrame(animate);
3422
+ };
2718
3423
  }
2719
3424
 
2720
3425
  const FORCE_CONTROLS = [
@@ -2875,22 +3580,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
2875
3580
  label: "Расширение границы связи для курсора",
2876
3581
  },
2877
3582
  {
2878
- id: "highlightDownFrames",
2879
- initialValue: HIGHLIGHT_SETTINGS.highlightDownFrames,
2880
- max: 60,
2881
- min: 1,
2882
- step: 1,
2883
- type: "range",
2884
- label: "Скорость отмены анимации в кадрах",
2885
- },
2886
- {
2887
- id: "highlightUpFrames",
2888
- initialValue: HIGHLIGHT_SETTINGS.highlightUpFrames,
2889
- max: 60,
2890
- min: 1,
3583
+ id: "highlightDuration",
3584
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3585
+ max: 1000,
3586
+ min: 10,
2891
3587
  step: 1,
2892
3588
  type: "range",
2893
- label: "Скорость применения анимации в кадрах",
3589
+ label: "Скорость анимации",
2894
3590
  },
2895
3591
  ];
2896
3592
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3363,19 +4059,22 @@ const LINK_SETTINGS_CONTROLS = [
3363
4059
  initialValue: LINK_SETTINGS.particles,
3364
4060
  },
3365
4061
  {
3366
- id: "particleFlexSpeed",
3367
- type: "checkbox",
3368
- label: "Гибкий расчет скорости частицы",
3369
- initialValue: LINK_SETTINGS.particleFlexSpeed,
4062
+ id: "particleSpeedByDistance",
4063
+ initialValue: LINK_SETTINGS.particleSpeedByDistance,
4064
+ max: 500,
4065
+ min: 1,
4066
+ step: 1,
4067
+ type: "range",
4068
+ label: "Скорость частицы на один пиксель",
3370
4069
  },
3371
4070
  {
3372
- id: "particleFlexSpeedCoefficient",
3373
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3374
- max: 10,
3375
- min: 0.1,
3376
- step: 0.1,
4071
+ id: "particleCountByDistance",
4072
+ initialValue: LINK_SETTINGS.particleCountByDistance,
4073
+ max: 250,
4074
+ min: 1,
4075
+ step: 1,
3377
4076
  type: "range",
3378
- label: "Коэффициент скорости частицы относительно расстояния",
4077
+ label: "Количество пикселей для появления частицы",
3379
4078
  },
3380
4079
  ];
3381
4080
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3470,24 +4169,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3470
4169
  label: "Радиус",
3471
4170
  initialValue: LINK_OPTIONS.particleRadius,
3472
4171
  },
3473
- {
3474
- id: "particleCount",
3475
- type: "range",
3476
- max: 20,
3477
- min: 1,
3478
- step: 1,
3479
- label: "Количество",
3480
- initialValue: LINK_OPTIONS.particleCount,
3481
- },
3482
- {
3483
- id: "particleSteps",
3484
- type: "range",
3485
- max: 200,
3486
- min: 1,
3487
- step: 1,
3488
- label: "Количество кадров",
3489
- initialValue: LINK_OPTIONS.particleSteps,
3490
- },
3491
4172
  {
3492
4173
  id: "particleBorderColor",
3493
4174
  type: "color",
@@ -3921,10 +4602,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
3921
4602
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
3922
4603
  exports.NODE_SETTINGS = NODE_SETTINGS;
3923
4604
  exports.NODE_SETTINGS_CONTROLS = NODE_SETTINGS_CONTROLS;
4605
+ exports.PERFOMANCE_NODE_OPTIONS = PERFOMANCE_NODE_OPTIONS;
4606
+ exports.PERFORMANCE_FORCE_SETTINGS = PERFORMANCE_FORCE_SETTINGS;
4607
+ exports.PERFORMANCE_GRAPH_SETTINGS = PERFORMANCE_GRAPH_SETTINGS;
4608
+ exports.PERFORMANCE_HIGHLIGHT_SETTINGS = PERFORMANCE_HIGHLIGHT_SETTINGS;
4609
+ exports.PERFORMANCE_LINK_OPTIONS = PERFORMANCE_LINK_OPTIONS;
4610
+ exports.PERFORMANCE_LINK_SETTINGS = PERFORMANCE_LINK_SETTINGS;
4611
+ exports.PERFORMANCE_NODE_SETTINGS = PERFORMANCE_NODE_SETTINGS;
3924
4612
  exports.animationByProgress = animationByProgress;
4613
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
3925
4614
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
3926
4615
  exports.colorGetter = colorGetter;
3927
4616
  exports.colorToRgb = colorToRgb;
4617
+ exports.computeGraphBounds = computeGraphBounds;
3928
4618
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
3929
4619
  exports.drawText = drawText;
3930
4620
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -3936,6 +4626,7 @@ exports.getDrawNode = getDrawNode;
3936
4626
  exports.getParticlePosition = getParticlePosition;
3937
4627
  exports.graphSettingsGetter = graphSettingsGetter;
3938
4628
  exports.highlightSettingsGetter = highlightSettingsGetter;
4629
+ exports.isEmptyObject = isEmptyObject;
3939
4630
  exports.isNodeVisible = isNodeVisible;
3940
4631
  exports.isOverlapsNode = isOverlapsNode;
3941
4632
  exports.linkByPointerGetter = linkByPointerGetter;