@krainovsd/graph 0.13.0-beta1 → 0.14.0-beta.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 (72) hide show
  1. package/lib/cjs/index.cjs +984 -497
  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 +222 -34
  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 -22
  50. package/lib/esm/module/GraphCanvas/lib/utils/link-by-pointer-getter.js.map +1 -1
  51. package/lib/esm/module/GraphCanvas/slices/draw-links.js +115 -77
  52. package/lib/esm/module/GraphCanvas/slices/draw-links.js.map +1 -1
  53. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js +9 -4
  54. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js.map +1 -1
  55. package/lib/esm/module/GraphCanvas/slices/init-dnd.js +21 -4
  56. package/lib/esm/module/GraphCanvas/slices/init-dnd.js.map +1 -1
  57. package/lib/esm/module/GraphCanvas/slices/init-draw.js +1 -52
  58. package/lib/esm/module/GraphCanvas/slices/init-draw.js.map +1 -1
  59. package/lib/esm/module/GraphCanvas/slices/init-pointer.js +12 -34
  60. package/lib/esm/module/GraphCanvas/slices/init-pointer.js.map +1 -1
  61. package/lib/esm/module/GraphCanvas/slices/init-resize.js +7 -0
  62. package/lib/esm/module/GraphCanvas/slices/init-resize.js.map +1 -1
  63. package/lib/esm/module/GraphCanvas/slices/init-simulation.js +12 -2
  64. package/lib/esm/module/GraphCanvas/slices/init-simulation.js.map +1 -1
  65. package/lib/esm/module/GraphCanvas/slices/init-zoom.js +85 -16
  66. package/lib/esm/module/GraphCanvas/slices/init-zoom.js.map +1 -1
  67. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js +63 -3
  68. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js.map +1 -1
  69. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js +61 -16
  70. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js.map +1 -1
  71. package/lib/index.d.ts +90 -36
  72. 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,6 +290,30 @@ 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
318
  const clientX = "clientX" in mouseEvent
276
319
  ? mouseEvent.clientX
@@ -421,50 +464,52 @@ function nodeIterationExtractor(node, i, nodes, state, option, optionConstantGet
421
464
  return customOptions;
422
465
  }
423
466
 
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;
467
+ function calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, arrowSize = 0) {
429
468
  const sourcePoint = getLinkPoint({
469
+ xStart,
470
+ yStart,
471
+ xEnd,
472
+ yEnd,
430
473
  arrowSize: 0,
431
- node: source,
432
- oppositeNode: target,
433
- });
474
+ node: sourceNode});
434
475
  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;
476
+ xStart: xEnd,
477
+ yStart: yEnd,
478
+ xEnd: xStart,
479
+ yEnd: yStart,
480
+ arrowSize: arrowSize > 0 ? arrowSize * 0.9 : 0,
481
+ node: targetNode});
482
+ const targetPointArrow = getLinkPoint({
483
+ xStart: xEnd,
484
+ yStart: yEnd,
485
+ xEnd: xStart,
486
+ yEnd: yStart,
487
+ arrowSize: 0,
488
+ node: targetNode});
441
489
  const distance = targetPoint && sourcePoint
442
490
  ? Math.sqrt((targetPoint.x - sourcePoint.x) ** 2 + (targetPoint.y - sourcePoint.y) ** 2)
443
491
  : 0;
444
492
  return {
445
- x1: sourcePoint.x,
446
- y1: sourcePoint.y,
447
- x2: targetPoint.x,
448
- y2: targetPoint.y,
493
+ xStart: sourcePoint.x,
494
+ yStart: sourcePoint.y,
495
+ xEnd: targetPoint.x,
496
+ yEnd: targetPoint.y,
497
+ xEndArrow: targetPointArrow.x,
498
+ yEndArrow: targetPointArrow.y,
449
499
  distance,
450
500
  };
451
501
  }
452
502
  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
503
  let nodePoint;
459
504
  switch (opts.node._shape) {
460
505
  case "circle": {
461
506
  nodePoint = getCircleIntersection({
462
- x: opts.node.x,
463
- y: opts.node.y,
507
+ x: opts.xStart,
508
+ y: opts.yStart,
464
509
  radius: (opts.node._radius ?? COMMON_SETTINGS.nodeRadius) +
465
510
  (opts.node._borderWidth ? opts.node._borderWidth / 2 : 0),
466
- oppositeX: opts.oppositeNode.x,
467
- oppositeY: opts.oppositeNode.y,
511
+ oppositeX: opts.xEnd,
512
+ oppositeY: opts.yEnd,
468
513
  arrowSize: opts.arrowSize,
469
514
  });
470
515
  break;
@@ -473,23 +518,23 @@ function getLinkPoint(opts) {
473
518
  case "text": {
474
519
  nodePoint = getRectangleIntersection({
475
520
  arrowSize: opts.arrowSize,
476
- x: opts.node.x,
477
- y: opts.node.y,
521
+ x: opts.xStart,
522
+ y: opts.yStart,
478
523
  height: (opts.node._height ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
479
524
  width: (opts.node._width ?? COMMON_SETTINGS.nodeSize) + (opts.node._borderWidth ?? 0),
480
- oppositeX: opts.oppositeNode.x,
481
- oppositeY: opts.oppositeNode.y,
525
+ oppositeX: opts.xEnd,
526
+ oppositeY: opts.yEnd,
482
527
  borderRadius: opts.node._shape === "text" ? 0 : (opts.node._borderRadius ?? 0),
483
528
  });
484
529
  break;
485
530
  }
486
531
  default: {
487
532
  nodePoint = getCircleIntersection({
488
- x: opts.node.x,
489
- y: opts.node.y,
533
+ x: opts.xStart,
534
+ y: opts.yStart,
490
535
  radius: opts.node._radius ?? COMMON_SETTINGS.nodeRadius,
491
- oppositeX: opts.oppositeNode.x,
492
- oppositeY: opts.oppositeNode.y,
536
+ oppositeX: opts.xEnd,
537
+ oppositeY: opts.yEnd,
493
538
  arrowSize: opts.arrowSize,
494
539
  });
495
540
  }
@@ -733,68 +778,81 @@ function isNodeVisible(opts) {
733
778
  }
734
779
  }
735
780
 
781
+ const FRAME_INTERVAL = 1000 / 60;
736
782
  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;
783
+ const now = performance.now();
784
+ const particle = opts.particle;
785
+ if (particle._lastTime == undefined) {
786
+ particle._distanceTraveled = (opts.distance / opts.totalCount) * particle.index;
787
+ particle._lastTime = now;
788
+ particle._lastDistance = opts.distance;
750
789
  }
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;
790
+ const lastDistance = particle._lastDistance ?? 0;
791
+ if (lastDistance !== opts.distance && lastDistance > 0) {
792
+ const scale = opts.distance / lastDistance;
793
+ particle._distanceTraveled = ((particle._distanceTraveled ?? 0) * scale) % opts.distance;
794
+ particle._lastDistance = opts.distance;
758
795
  }
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;
796
+ const delta = Math.min(now - particle._lastTime, FRAME_INTERVAL);
797
+ particle._lastTime = now;
798
+ particle._distanceTraveled =
799
+ ((particle._distanceTraveled ?? 0) + opts.speed * delta) % opts.distance;
800
+ const progress = particle._distanceTraveled / opts.distance;
801
+ if (opts.xControl !== 0 && opts.yControl !== 0) {
802
+ const t = 1 - progress;
803
+ particle.x =
804
+ t * t * opts.xStart + 2 * t * progress * opts.xControl + progress * progress * opts.xEnd;
805
+ particle.y =
806
+ t * t * opts.yStart + 2 * t * progress * opts.yControl + progress * progress * opts.yEnd;
767
807
  }
768
808
  else {
769
- opts.particle.step++;
809
+ const dx = opts.xEnd - opts.xStart;
810
+ const dy = opts.yEnd - opts.yStart;
811
+ particle.x = opts.xStart + dx * progress;
812
+ particle.y = opts.yStart + dy * progress;
770
813
  }
771
814
  }
815
+ function approximateQuadraticBezierLength(x0, y0, x1, y1, x2, y2) {
816
+ const dx01 = x1 - x0;
817
+ const dy01 = y1 - y0;
818
+ const dx12 = x2 - x1;
819
+ const dy12 = y2 - y1;
820
+ const dx02 = x2 - x0;
821
+ const dy02 = y2 - y0;
822
+ const a = Math.sqrt(dx01 * dx01 + dy01 * dy01);
823
+ const b = Math.sqrt(dx12 * dx12 + dy12 * dy12);
824
+ const c = Math.sqrt(dx02 * dx02 + dy02 * dy02);
825
+ return (a + b + c) / 2;
826
+ }
772
827
 
773
- function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, }) {
828
+ function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, curve, }) {
774
829
  if (!areaRect)
775
830
  return undefined;
776
831
  const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
777
832
  return d3Array.greatest(links, (link) => {
778
- if (isNearLink(pointerX, pointerY, link, linkHoverExtraZone))
779
- return link.index;
833
+ if (!jsHelpers.isObject(link.source) ||
834
+ !jsHelpers.isObject(link.target) ||
835
+ link.source.visible === false ||
836
+ link.target.visible === false)
837
+ return;
838
+ const x1 = (link._x1 ?? link.source.x);
839
+ const y1 = (link._y1 ?? link.source.y);
840
+ const x2 = (link._x2 ?? link.target.x);
841
+ const y2 = (link._y2 ?? link.target.y);
842
+ const cx = link._cx ?? 0;
843
+ const cy = link._cy ?? 0;
844
+ return curve
845
+ ? hitTestQuadratic(pointerX, pointerY, x1, y1, cx, cy, x2, y2, linkHoverExtraZone)
846
+ ? link.index
847
+ : undefined
848
+ : isNearLink(pointerX, pointerY, x1, y1, x2, y2, linkHoverExtraZone)
849
+ ? link.index
850
+ : undefined;
780
851
  });
781
852
  }
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;
853
+ function isNearLink(px, py, x1, y1, x2, y2, threshold = 2) {
854
+ const A = px - x1;
855
+ const B = py - y1;
798
856
  const C = x2 - x1;
799
857
  const D = y2 - y1;
800
858
  const dot = A * C + B * D;
@@ -816,9 +874,56 @@ function distanceToLine(x, y, x1, y1, x2, y2) {
816
874
  xx = x1 + param * C;
817
875
  yy = y1 + param * D;
818
876
  }
819
- const dx = x - xx;
820
- const dy = y - yy;
821
- return Math.sqrt(dx * dx + dy * dy);
877
+ const dx = px - xx;
878
+ const dy = py - yy;
879
+ return Math.sqrt(dx * dx + dy * dy) <= threshold;
880
+ }
881
+ function hitTestQuadratic(px, py, x0, y0, xc, yc, x2, y2, tol) {
882
+ const bb = bezierBBox(x0, y0, xc, yc, x2, y2);
883
+ if (px < bb.minX - tol || px > bb.maxX + tol || py < bb.minY - tol || py > bb.maxY + tol) {
884
+ return false;
885
+ }
886
+ const chord = Math.hypot(x2 - x0, y2 - y0);
887
+ const contLen = Math.hypot(xc - x0, yc - y0) + Math.hypot(x2 - xc, y2 - yc);
888
+ const approxLen = (chord + contLen) / 2;
889
+ const steps = Math.max(16, Math.ceil(approxLen / 1));
890
+ const tol2 = tol * tol;
891
+ for (let i = 0; i <= steps; i++) {
892
+ const t = i / steps;
893
+ const mt = 1 - t;
894
+ const x = mt * mt * x0 + 2 * mt * t * xc + t * t * x2;
895
+ const y = mt * mt * y0 + 2 * mt * t * yc + t * t * y2;
896
+ const dx = px - x;
897
+ const dy = py - y;
898
+ if (dx * dx + dy * dy <= tol2)
899
+ return true;
900
+ }
901
+ return false;
902
+ }
903
+ function bezierBBox(x0, y0, x1, y1, x2, y2) {
904
+ function extrema(p0, p1, p2) {
905
+ const denom = p0 - 2 * p1 + p2;
906
+ if (denom === 0)
907
+ return [];
908
+ const t = (p0 - p1) / denom;
909
+ return t > 0 && t < 1 ? [t] : [];
910
+ }
911
+ const ts = [...extrema(x0, x1, x2), ...extrema(y0, y1, y2), 0, 1];
912
+ let maxX = -Infinity, maxY = -Infinity, minX = Infinity, minY = Infinity;
913
+ for (const t of ts) {
914
+ const mt = 1 - t;
915
+ const x = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
916
+ const y = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
917
+ if (x < minX)
918
+ minX = x;
919
+ if (x > maxX)
920
+ maxX = x;
921
+ if (y < minY)
922
+ minY = y;
923
+ if (y > maxY)
924
+ maxY = y;
925
+ }
926
+ return { minX, minY, maxX, maxY };
822
927
  }
823
928
 
824
929
  function nodeHighlight(opts) {
@@ -994,22 +1099,38 @@ function extractLinkPointIds(link) {
994
1099
  return { sourceId, targetId };
995
1100
  }
996
1101
 
1102
+ function isEmptyObject(obj) {
1103
+ for (const key in obj) {
1104
+ if (Object.hasOwn(obj, key)) {
1105
+ return false;
1106
+ }
1107
+ }
1108
+ return true;
1109
+ }
1110
+
997
1111
  const GRAPH_SETTINGS = {
998
- zoomExtent: [0.3, 10],
999
- translateExtent: [[], []],
1000
- translateExtentEnable: true,
1001
- translateExtentCoefficient: [3, 3],
1112
+ zoomAnimation: true,
1113
+ zoomAnimationDuration: 300,
1114
+ zoomToNodeScale: 5,
1115
+ zoomToFitMargin: 0.25,
1116
+ zoomExtent: null,
1117
+ zoomExtentMargin: 0.3,
1118
+ translateExtent: null,
1119
+ translateExtentCoefficient: null,
1120
+ translateExtentOverlap: 0.3,
1002
1121
  dragPlaceCoefficient: dragPlaceCoefficientGetter,
1003
1122
  zoomInitial: null,
1004
1123
  showDrawTime: true,
1005
1124
  showDrawTimeEveryTick: false,
1006
1125
  };
1126
+ const PERFORMANCE_GRAPH_SETTINGS = {
1127
+ ...GRAPH_SETTINGS,
1128
+ };
1007
1129
 
1008
1130
  const HIGHLIGHT_SETTINGS = {
1009
1131
  highlightByHoverNode: true,
1010
1132
  highlightByHoverLink: true,
1011
- highlightUpFrames: 5,
1012
- highlightDownFrames: 5,
1133
+ highlightDuration: 150,
1013
1134
  linkHoverExtraZone: 2,
1014
1135
  /** Node */
1015
1136
  highlightByNodeOnlyRoot: true,
@@ -1064,36 +1185,51 @@ const HIGHLIGHT_SETTINGS = {
1064
1185
  highlightByLinkForArrowBorderColor: null,
1065
1186
  highlightByLinkForArrowBorderSizingAdditional: 0,
1066
1187
  };
1188
+ const PERFORMANCE_HIGHLIGHT_SETTINGS = {
1189
+ ...HIGHLIGHT_SETTINGS,
1190
+ };
1067
1191
 
1068
1192
  const LINK_SETTINGS = {
1193
+ smartCache: true,
1069
1194
  prettyDraw: true,
1195
+ curve: true,
1070
1196
  linkScaleSwitch: 1,
1071
- linkColorAfterScaleSwitch: "#000000FF",
1197
+ linkColorAfterScaleSwitch: "#C5C5C5FF",
1072
1198
  linkColorBeforeScaleSwitch: "#999",
1073
1199
  linkWidthAfterScaleSwitch: 0.1,
1074
1200
  linkWidthBeforeScaleSwitch: 1,
1075
1201
  arrow: true,
1076
1202
  arrowByHighlight: true,
1077
1203
  particles: true,
1078
- particleFlexSpeed: true,
1079
- particleFlexSpeedCoefficient: 4,
1204
+ // spawn one more particle for N px
1205
+ particleCountByDistance: 25,
1206
+ // speed for 1px
1207
+ particleSpeedByDistance: 50,
1080
1208
  };
1081
1209
  const LINK_OPTIONS = {
1082
1210
  alpha: 1,
1083
1211
  arrowAlpha: 1,
1084
1212
  arrowSize: 2,
1085
- arrowBorderColor: "#000000FF",
1213
+ arrowBorderColor: "#C5C5C5FF",
1086
1214
  arrowBorderWidth: 0.1,
1087
1215
  particleAlpha: 1,
1088
- particleColor: "#000000FF",
1089
- particleBorderColor: "#000000FF",
1216
+ particleColor: "#C5C5C5FF",
1217
+ particleBorderColor: "#C5C5C5FF",
1090
1218
  particleBorderWidth: 0.1,
1091
- particleCount: 2,
1092
1219
  particleRadius: 0.5,
1093
- particleSteps: 60,
1220
+ };
1221
+ const PERFORMANCE_LINK_SETTINGS = {
1222
+ ...LINK_SETTINGS,
1223
+ particles: false,
1224
+ prettyDraw: false,
1225
+ };
1226
+ const PERFORMANCE_LINK_OPTIONS = {
1227
+ ...LINK_OPTIONS,
1094
1228
  };
1095
1229
 
1096
1230
  const NODE_SETTINGS = {
1231
+ smartCache: true,
1232
+ textScaleSteps: [],
1097
1233
  nodeRadiusFlexible: true,
1098
1234
  nodeRadiusLinkCountForStep: 5,
1099
1235
  nodeRadiusIncrementByStep: 1,
@@ -1130,14 +1266,14 @@ const NODE_OPTIONS = {
1130
1266
  textShiftX: 0,
1131
1267
  textFont: "Arial",
1132
1268
  textAlign: "center",
1133
- textColor: "#333",
1269
+ textColor: "#d2d2d2",
1134
1270
  textStyle: "normal",
1135
1271
  textWeight: 500,
1136
1272
  textGap: 0,
1137
1273
  label: null,
1138
1274
  labelAlpha: 1,
1139
1275
  labelAlign: "center",
1140
- labelColor: "#333",
1276
+ labelColor: "#ffffff",
1141
1277
  labelFont: "Arial",
1142
1278
  labelGap: 0,
1143
1279
  labelSize: 3.5,
@@ -1145,6 +1281,14 @@ const NODE_OPTIONS = {
1145
1281
  labelWeight: 500,
1146
1282
  labelWidth: 20,
1147
1283
  };
1284
+ const PERFORMANCE_NODE_SETTINGS = {
1285
+ ...NODE_SETTINGS,
1286
+ nodeRadiusFlexible: false,
1287
+ nodeSizeFlexible: false,
1288
+ };
1289
+ const PERFOMANCE_NODE_OPTIONS = {
1290
+ ...NODE_OPTIONS,
1291
+ };
1148
1292
 
1149
1293
  const GRAPH_CACHE_TYPE = {
1150
1294
  NodeOptions: "nodeOptions",
@@ -1211,8 +1355,14 @@ function initDnd() {
1211
1355
  return;
1212
1356
  const mouseEvent = event.sourceEvent;
1213
1357
  const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1214
- event.subject.fx = pointerX;
1215
- event.subject.fy = pointerY;
1358
+ if (this._translateExtent) {
1359
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1360
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1361
+ }
1362
+ else {
1363
+ event.subject.fx = pointerX;
1364
+ event.subject.fy = pointerY;
1365
+ }
1216
1366
  this.listeners.onMoveDragFinished?.call?.(this, event);
1217
1367
  })
1218
1368
  .on("end", (event) => {
@@ -1222,8 +1372,14 @@ function initDnd() {
1222
1372
  const sourceEvent = event.sourceEvent;
1223
1373
  if (sourceEvent.altKey && this.areaRect) {
1224
1374
  const [pointerX, pointerY] = pointerGetter(sourceEvent, this.areaRect, this.areaTransform);
1225
- event.subject.fx = pointerX;
1226
- event.subject.fy = pointerY;
1375
+ if (this._translateExtent) {
1376
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1377
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1378
+ }
1379
+ else {
1380
+ event.subject.fx = pointerX;
1381
+ event.subject.fy = pointerY;
1382
+ }
1227
1383
  }
1228
1384
  else {
1229
1385
  event.subject.fx = null;
@@ -1234,8 +1390,44 @@ function initDnd() {
1234
1390
  d3Selection.select(this.area).call(dragHandler);
1235
1391
  }
1236
1392
 
1393
+ function calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, sourceNode, targetNode, groupIndex, arrowSize = 0) {
1394
+ const sourceRadius = (sourceNode._radius ?? COMMON_SETTINGS.nodeRadius) + (sourceNode._borderWidth ?? 0) * 0.5;
1395
+ const targetRadius = (targetNode._radius ?? COMMON_SETTINGS.nodeRadius) + (targetNode._borderWidth ?? 0) * 0.5;
1396
+ const effectiveTargetRadius = targetRadius + arrowSize * 0.9;
1397
+ const mx = (xStart + xEnd) * 0.5;
1398
+ const my = (yStart + yEnd) * 0.5;
1399
+ const dx = xEnd - xStart;
1400
+ const dy = yEnd - yStart;
1401
+ const lenSq = dx * dx + dy * dy;
1402
+ const len = lenSq > 0 ? Math.sqrt(lenSq) : 1;
1403
+ const nx = -dy / len;
1404
+ const ny = dx / len;
1405
+ const offset = 0.12 * len * groupIndex;
1406
+ const xControl = mx + nx * offset;
1407
+ const yControl = my + ny * offset;
1408
+ const [xStartP, yStartP] = adjustPoint(xStart, yStart, xControl, yControl, sourceRadius);
1409
+ const [xEndP, yEndP] = adjustPoint(xEnd, yEnd, xControl, yControl, effectiveTargetRadius);
1410
+ const [xEndArrow, yEndArrow] = adjustPoint(xEnd, yEnd, xControl, yControl, targetRadius);
1411
+ return {
1412
+ xStart: xStartP,
1413
+ yStart: yStartP,
1414
+ xEnd: xEndP,
1415
+ yEnd: yEndP,
1416
+ xControl,
1417
+ yControl,
1418
+ xEndArrow,
1419
+ yEndArrow,
1420
+ };
1421
+ }
1422
+ function adjustPoint(x, y, xControl, yControl, radius) {
1423
+ const dxToControl = xControl - x;
1424
+ const dyToControl = yControl - y;
1425
+ const distToControl = Math.sqrt(dxToControl * dxToControl + dyToControl * dyToControl) || 1;
1426
+ return [x + (dxToControl / distToControl) * radius, y + (dyToControl / distToControl) * radius];
1427
+ }
1428
+
1237
1429
  function getDrawLink() {
1238
- return function drawLink(link) {
1430
+ return function drawLink(link, index) {
1239
1431
  if (!this.context ||
1240
1432
  typeof link.source !== "object" ||
1241
1433
  typeof link.target !== "object" ||
@@ -1250,8 +1442,7 @@ function getDrawLink() {
1250
1442
  return;
1251
1443
  if (!link.source._visible && !link.target._visible)
1252
1444
  return;
1253
- const id = `${link.target.id}${link.source.id}`;
1254
- const linkOptions = this.linkOptionsCache[id];
1445
+ const linkOptions = this.linkOptionsCache[index];
1255
1446
  if (!linkOptions)
1256
1447
  return;
1257
1448
  if (linkOptions.drawLink) {
@@ -1266,10 +1457,12 @@ function getDrawLink() {
1266
1457
  let arrowSize = linkOptions.arrowSize;
1267
1458
  let arrowBorderWidth = linkOptions.arrowBorderWidth;
1268
1459
  let arrowBorderColor = linkOptions.arrowBorderColor;
1460
+ const currentNodeHighlighted = this.highlightedNode &&
1461
+ (this.highlightedNode.id == link.source.id || this.highlightedNode.id == link.target.id);
1269
1462
  /** Highlight */
1270
1463
  if (this.highlightedNeighbors && this.highlightedNode) {
1271
1464
  /** By Node Not Highlight */
1272
- if (this.highlightedNode.id != link.source.id && this.highlightedNode.id != link.target.id) {
1465
+ if (!currentNodeHighlighted) {
1273
1466
  const fadeOptions = linkFade({
1274
1467
  arrow: this.linkSettings.arrow,
1275
1468
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1303,9 +1496,10 @@ function getDrawLink() {
1303
1496
  arrowBorderColor = highlightOptions.arrowBorderColor;
1304
1497
  }
1305
1498
  }
1499
+ const currentLinkHighlighted = this.highlightedLink === link;
1306
1500
  if (this.highlightedNeighbors && this.highlightedLink) {
1307
1501
  /** By Link Not Highlight */
1308
- if (this.highlightedLink !== link) {
1502
+ if (!currentLinkHighlighted) {
1309
1503
  const fadeOptions = linkFade({
1310
1504
  arrow: this.linkSettings.arrow,
1311
1505
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1349,99 +1543,129 @@ function getDrawLink() {
1349
1543
  let yStart = link.source.y;
1350
1544
  let xEnd = link.target.x;
1351
1545
  let yEnd = link.target.y;
1546
+ let xControl = 0;
1547
+ let yControl = 0;
1548
+ let xEndArrow = xEnd;
1549
+ let yEndArrow = yEnd;
1352
1550
  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;
1551
+ const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1552
+ if (!this.linkSettings.curve) {
1553
+ if (this.linkSettings.prettyDraw ||
1554
+ (this.linkSettings.arrow && arrowAlpha > 0) ||
1555
+ (this.particles && (currentNodeHighlighted || currentLinkHighlighted))) {
1556
+ const position = calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, isHasArrow ? arrowSize : 0);
1557
+ xStart = position.xStart;
1558
+ xEnd = position.xEnd;
1559
+ yStart = position.yStart;
1560
+ yEnd = position.yEnd;
1561
+ xEndArrow = position.xEndArrow;
1562
+ yEndArrow = position.yEndArrow;
1363
1563
  linkDistance = position.distance;
1364
1564
  }
1565
+ link._x1 = xStart;
1566
+ link._y1 = yStart;
1567
+ link._x2 = xEnd;
1568
+ link._y2 = yEnd;
1569
+ link._ax = xEndArrow;
1570
+ link._ay = yEndArrow;
1571
+ this.context.moveTo(xStart, yStart);
1572
+ this.context.lineTo(xEnd, yEnd);
1573
+ this.context.stroke();
1574
+ }
1575
+ else {
1576
+ const position = calculateCurveLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, link._groupIndex ?? 0, isHasArrow ? arrowSize : 0);
1577
+ xStart = position.xStart;
1578
+ yStart = position.yStart;
1579
+ xEnd = position.xEnd;
1580
+ yEnd = position.yEnd;
1581
+ xControl = position.xControl;
1582
+ yControl = position.yControl;
1583
+ xEndArrow = position.xEndArrow;
1584
+ yEndArrow = position.yEndArrow;
1585
+ link._x1 = position.xStart;
1586
+ link._y1 = position.yStart;
1587
+ link._x2 = position.xEnd;
1588
+ link._y2 = position.yEnd;
1589
+ link._cx = position.xControl;
1590
+ link._cy = position.yControl;
1591
+ link._ax = position.xEndArrow;
1592
+ link._ay = position.yEndArrow;
1593
+ linkDistance = approximateQuadraticBezierLength(position.xStart, position.yStart, position.xControl, position.yControl, position.xEnd, position.yEnd);
1594
+ this.context.beginPath();
1595
+ this.context.moveTo(position.xStart, position.yStart);
1596
+ this.context.quadraticCurveTo(position.xControl, position.yControl, position.xEnd, position.yEnd);
1597
+ this.context.setLineDash([]);
1598
+ // if (isDashed) {
1599
+ // this.context.setLineDash([10, 5]);
1600
+ // }
1601
+ this.context.stroke();
1365
1602
  }
1366
- this.context.moveTo(xStart, yStart);
1367
- this.context.lineTo(xEnd, yEnd);
1368
- this.context.stroke();
1369
1603
  /** 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;
1604
+ if (this.linkSettings.particles && (currentLinkHighlighted || currentNodeHighlighted)) {
1605
+ const speed = (1 / this.linkSettings.particleSpeedByDistance) * (1 + 0.5);
1606
+ if (!this.particles[index]) {
1607
+ const particleCount = Math.max(1, Math.floor(linkDistance / this.linkSettings.particleCountByDistance));
1384
1608
  const particles = [];
1385
- let prevParticle;
1386
1609
  for (let i = 0; i < particleCount; i++) {
1387
1610
  const particle = {
1388
- step: 0,
1389
- sourceId,
1390
- targetId,
1391
- prev: prevParticle,
1392
- next: undefined,
1393
1611
  index: i,
1394
1612
  };
1395
- if (prevParticle)
1396
- prevParticle.next = particle;
1397
1613
  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
1614
  }
1404
- this.particles[id] = particles;
1615
+ this.particles[index] = particles;
1405
1616
  }
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
- }
1617
+ this.particles[index].forEach((particle, _, particles) => {
1618
+ if (!this.context)
1619
+ return;
1620
+ getParticlePosition({
1621
+ distance: linkDistance,
1622
+ particle,
1623
+ totalCount: particles.length,
1624
+ xEnd,
1625
+ xStart,
1626
+ yEnd,
1627
+ yStart,
1628
+ xControl,
1629
+ yControl,
1630
+ speed,
1431
1631
  });
1432
- }
1632
+ if (particle.x != undefined && particle.y != undefined) {
1633
+ this.context.beginPath();
1634
+ this.context.strokeStyle = linkOptions.particleBorderColor;
1635
+ this.context.lineWidth = linkOptions.particleBorderWidth;
1636
+ this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1637
+ this.context.fillStyle = linkOptions.particleColor;
1638
+ this.context.fill();
1639
+ if (linkOptions.particleBorderWidth > 0) {
1640
+ this.context.stroke();
1641
+ }
1642
+ }
1643
+ });
1433
1644
  }
1434
1645
  /** Arrow */
1435
1646
  if (this.linkSettings.arrow && arrowAlpha > 0) {
1647
+ let angle = 0;
1648
+ if (!this.linkSettings.curve) {
1649
+ angle = Math.atan2(yEndArrow - yStart, xEndArrow - xStart);
1650
+ }
1651
+ else {
1652
+ const tangentX = 2 * (xEndArrow - xControl);
1653
+ const tangentY = 2 * (yEndArrow - yControl);
1654
+ if (Math.abs(tangentX) < 0.001 && Math.abs(tangentY) < 0.001) {
1655
+ angle = Math.atan2(yEndArrow - yControl, xEndArrow - xControl);
1656
+ }
1657
+ else {
1658
+ angle = Math.atan2(tangentY, tangentX);
1659
+ }
1660
+ }
1436
1661
  this.context.beginPath();
1437
1662
  this.context.globalAlpha = arrowAlpha;
1438
1663
  this.context.strokeStyle = arrowBorderColor;
1439
1664
  this.context.lineWidth = arrowBorderWidth;
1440
1665
  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));
1666
+ this.context.moveTo(xEndArrow, yEndArrow);
1667
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle - Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle - Math.PI / 6));
1668
+ this.context.lineTo(xEndArrow - arrowSize * Math.cos(angle + Math.PI / 6), yEndArrow - arrowSize * Math.sin(angle + Math.PI / 6));
1445
1669
  this.context.closePath();
1446
1670
  this.context.fill();
1447
1671
  if (arrowBorderWidth > 0) {
@@ -1500,7 +1724,7 @@ function getTextLines({ context, textAlign, textColor, textFont, textStyle, text
1500
1724
  }
1501
1725
 
1502
1726
  function getDrawNode(nodeRenders, textRenders) {
1503
- return function drawNode(node) {
1727
+ return function drawNode(node, index) {
1504
1728
  if (!this.context || !node.x || !node.y)
1505
1729
  return;
1506
1730
  if (node.visible != undefined && !node.visible) {
@@ -1509,7 +1733,7 @@ function getDrawNode(nodeRenders, textRenders) {
1509
1733
  node._height = 0;
1510
1734
  return;
1511
1735
  }
1512
- const nodeOptions = this.nodeOptionsCache[node.id];
1736
+ const nodeOptions = this.nodeOptionsCache[index];
1513
1737
  if (!nodeOptions)
1514
1738
  return;
1515
1739
  if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
@@ -1668,7 +1892,7 @@ function getDrawNode(nodeRenders, textRenders) {
1668
1892
  this.context.lineWidth = borderWidth;
1669
1893
  this.context.strokeStyle = borderColor;
1670
1894
  this.context.fillStyle = color;
1671
- const labelLines = this.cachedNodeLabel[node.id];
1895
+ const labelLines = this.cachedNodeLabel[index];
1672
1896
  switch (nodeOptions.shape) {
1673
1897
  case "circle": {
1674
1898
  if (!node.image) {
@@ -1794,7 +2018,7 @@ function getDrawNode(nodeRenders, textRenders) {
1794
2018
  });
1795
2019
  }
1796
2020
  /** Text draw */
1797
- const textLines = this.cachedNodeText[node.id];
2021
+ const textLines = this.cachedNodeText[index];
1798
2022
  if (nodeOptions.textVisible && nodeOptions.text && textLines) {
1799
2023
  textRenders.push(() => {
1800
2024
  if (nodeOptions.textDraw) {
@@ -1854,57 +2078,7 @@ function getDrawNode(nodeRenders, textRenders) {
1854
2078
  }
1855
2079
 
1856
2080
  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) {
2081
+ function draw() {
1908
2082
  if (!this.context)
1909
2083
  return;
1910
2084
  if (this.listeners.onDraw) {
@@ -1925,7 +2099,6 @@ function initDraw() {
1925
2099
  textRenders.forEach((render) => render());
1926
2100
  this.context.restore();
1927
2101
  this.listeners.onDrawFinished?.call?.(this);
1928
- calculateHighlight.bind(this)(recursive);
1929
2102
  }
1930
2103
  if (this.graphSettings.showDrawTime) {
1931
2104
  return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
@@ -1965,6 +2138,7 @@ function initPointer() {
1965
2138
  areaTransform: this.areaTransform,
1966
2139
  mouseEvent: event,
1967
2140
  links: this.links,
2141
+ curve: this.linkSettings.curve,
1968
2142
  });
1969
2143
  if (currentLink?.highlight != undefined)
1970
2144
  highlightLink = currentLink?.highlight;
@@ -1978,38 +2152,7 @@ function initPointer() {
1978
2152
  else {
1979
2153
  this.area.style.cursor = "default";
1980
2154
  }
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
- }
2155
+ this.animateHighlight(highlightNode ? currentNode : undefined, highlightLink ? currentLink : undefined);
2013
2156
  if (!this.listeners.onMove)
2014
2157
  return;
2015
2158
  if (!currentNode && !checkHighlightNode)
@@ -2026,6 +2169,7 @@ function initPointer() {
2026
2169
  areaTransform: this.areaTransform,
2027
2170
  mouseEvent: event,
2028
2171
  links: this.links,
2172
+ curve: this.linkSettings.curve,
2029
2173
  });
2030
2174
  }
2031
2175
  if (!currentNode)
@@ -2050,6 +2194,7 @@ function initPointer() {
2050
2194
  areaTransform: this.areaTransform,
2051
2195
  mouseEvent: event,
2052
2196
  links: this.links,
2197
+ curve: this.linkSettings.curve,
2053
2198
  });
2054
2199
  return void this.listeners.onWheelClick.call(this, event, undefined, currentLink);
2055
2200
  }
@@ -2071,6 +2216,7 @@ function initPointer() {
2071
2216
  areaTransform: this.areaTransform,
2072
2217
  mouseEvent: event,
2073
2218
  links: this.links,
2219
+ curve: this.linkSettings.curve,
2074
2220
  });
2075
2221
  return void this.listeners.onContextMenu.call(this, event, undefined, currentLink);
2076
2222
  }
@@ -2092,6 +2238,7 @@ function initPointer() {
2092
2238
  areaTransform: this.areaTransform,
2093
2239
  mouseEvent: event,
2094
2240
  links: this.links,
2241
+ curve: this.linkSettings.curve,
2095
2242
  });
2096
2243
  return void this.listeners.onDoubleClick.call(this, event, undefined, currentLink);
2097
2244
  }
@@ -2113,6 +2260,7 @@ function initPointer() {
2113
2260
  areaTransform: this.areaTransform,
2114
2261
  mouseEvent: event,
2115
2262
  links: this.links,
2263
+ curve: this.linkSettings.curve,
2116
2264
  });
2117
2265
  return void this.listeners.onClick.call(this, event, undefined, currentLink);
2118
2266
  }
@@ -2158,148 +2306,116 @@ function initResize() {
2158
2306
  return;
2159
2307
  }
2160
2308
  this.updateSize();
2309
+ requestAnimationFrame(() => {
2310
+ this.updateSize();
2311
+ });
2161
2312
  });
2162
2313
  document.addEventListener("scroll", this.updateRect.bind(this), {
2163
2314
  capture: true,
2164
2315
  passive: true,
2165
2316
  signal: abortController.signal,
2166
2317
  });
2318
+ document.addEventListener("visibilitychange", () => {
2319
+ if (!document.hidden)
2320
+ this.updateRect();
2321
+ }, { signal: abortController.signal });
2167
2322
  observer.observe(this.area);
2168
2323
  }
2169
2324
 
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
- }
2186
- });
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);
2205
- }
2206
- if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2207
- this.simulation.force("center")) {
2208
- this.simulation.force("center", null);
2209
- }
2210
- if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2211
- linkForce.distance(0).strength(0).iterations(0);
2212
- }
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);
2218
- }
2219
- if (this.forceSettings.forces && this.forceSettings.xForce) {
2220
- this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2221
- }
2222
- if (this.forceSettings.forces && this.forceSettings.yForce) {
2223
- this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2224
- }
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));
2230
- }
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));
2233
- }
2234
- initCollideForce.call(this, true);
2235
- }
2236
- function initCollideForce(forceUpdate) {
2237
- if (!this.simulation)
2325
+ function updateLinkCache() {
2326
+ if (!this.context)
2238
2327
  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);
2328
+ const current = this.areaTransform.k;
2329
+ if (this.linkSettings.smartCache &&
2330
+ this._lastLinkZoomK !== undefined &&
2331
+ this.linkOptionsCache.length > 0) {
2332
+ const corner = this.linkSettings.linkScaleSwitch;
2333
+ const prev = this._lastLinkZoomK;
2334
+ this._lastLinkZoomK = current;
2335
+ if (!((prev <= corner && current >= corner) || (prev >= corner && current <= corner))) {
2336
+ return;
2249
2337
  }
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));
2338
+ for (let i = 0; i < this.links.length; i++) {
2339
+ const link = this.links[i];
2340
+ const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2341
+ const cache = this.linkOptionsCache[i];
2342
+ if (cache) {
2343
+ cache.color = linkOptions.color;
2344
+ cache.arrowColor = linkOptions.arrowColor;
2345
+ cache.width = linkOptions.width;
2346
+ }
2281
2347
  }
2348
+ return;
2282
2349
  }
2283
- }
2284
-
2285
- function updateLinkCache() {
2286
- this.linkOptionsCache = {};
2350
+ this._lastLinkZoomK = current;
2351
+ this.linkOptionsCache.length = 0;
2352
+ const groupMap = {};
2353
+ const groupSelfMap = {};
2287
2354
  for (let i = 0; i < this.links.length; i++) {
2288
2355
  const link = this.links[i];
2289
2356
  const { sourceId, targetId } = extractLinkPointIds(link);
2290
2357
  const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2291
- const id = `${targetId}${sourceId}`;
2292
- this.linkOptionsCache[id] = linkOptions;
2358
+ this.linkOptionsCache[i] = linkOptions;
2359
+ if (this.linkSettings.curve) {
2360
+ const id = `${targetId}${sourceId}`;
2361
+ if (sourceId === targetId) {
2362
+ groupSelfMap[id] ??= 0;
2363
+ link._groupIndex = ++groupSelfMap[id];
2364
+ link._self = true;
2365
+ continue;
2366
+ }
2367
+ const reverseId = `${sourceId}${targetId}`;
2368
+ groupMap[reverseId] ??= 0;
2369
+ groupMap[reverseId]++;
2370
+ groupMap[id] ??= 0;
2371
+ link._groupIndex = indexToPosition(++groupMap[id]);
2372
+ link._self = false;
2373
+ }
2293
2374
  }
2375
+ if (this.linkSettings.curve) {
2376
+ for (const link of this.links) {
2377
+ const { sourceId, targetId } = extractLinkPointIds(link);
2378
+ const id = `${targetId}${sourceId}`;
2379
+ link._groupSize = link._self ? (groupSelfMap[id] ?? 1) : (groupMap[id] ?? 1);
2380
+ }
2381
+ }
2382
+ }
2383
+ function indexToPosition(n) {
2384
+ if (n === 1)
2385
+ return 0;
2386
+ const index = n >> 1;
2387
+ return n & 1 ? -(index * 2) : index * 2;
2388
+ // return n & 1 ? -(n >> 1) : n >> 1;
2294
2389
  }
2295
2390
 
2296
- function updateNodeCache(types) {
2297
- this.nodeOptionsCache = {};
2391
+ function updateNodeCache() {
2298
2392
  if (!this.context)
2299
2393
  return;
2394
+ const currentZoom = this.areaTransform.k;
2395
+ const options = this.nodeOptionsCache.length === 0;
2396
+ const label = this.cachedNodeLabel.length === 0;
2397
+ let text = this.cachedNodeText.length === 0;
2398
+ if (this.nodeSettings.smartCache &&
2399
+ this._lastNodeZoomK !== undefined &&
2400
+ !options &&
2401
+ !text &&
2402
+ !label) {
2403
+ const thresholds = this.nodeSettings.textScaleSteps;
2404
+ if (thresholds && thresholds.length > 0) {
2405
+ const min = Math.min(this._lastNodeZoomK, currentZoom);
2406
+ const max = Math.max(this._lastNodeZoomK, currentZoom);
2407
+ const crossed = thresholds.some((t) => t >= min && t <= max);
2408
+ this._lastNodeZoomK = currentZoom;
2409
+ if (!crossed)
2410
+ return;
2411
+ }
2412
+ this.cachedNodeText.length = 0;
2413
+ text = true;
2414
+ }
2415
+ this._lastNodeZoomK = currentZoom;
2300
2416
  for (let i = 0; i < this.nodes.length; i++) {
2301
2417
  const node = this.nodes[i];
2302
- if (types === true || (jsHelpers.isObject(types) && types.options)) {
2418
+ if (options) {
2303
2419
  const nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2304
2420
  const radius = nodeOptions.shape === "circle"
2305
2421
  ? nodeRadiusGetter({
@@ -2350,11 +2466,19 @@ function updateNodeCache(types) {
2350
2466
  nodeOptions.height = node.visible === false ? 0 : height;
2351
2467
  nodeOptions.radius = node.visible === false ? 0 : radius;
2352
2468
  nodeOptions.labelSize = labelSize;
2353
- this.nodeOptionsCache[node.id] = nodeOptions;
2469
+ this.nodeOptionsCache[i] = nodeOptions;
2354
2470
  }
2355
- const nodeOptions = this.nodeOptionsCache[node.id];
2471
+ let nodeOptions = this.nodeOptionsCache[i];
2472
+ if (!nodeOptions)
2473
+ continue;
2356
2474
  /** label */
2357
- if (types === true || (jsHelpers.isObject(types) && types.label)) {
2475
+ if (label) {
2476
+ if (!options) {
2477
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2478
+ const cache = this.nodeOptionsCache[i];
2479
+ if (cache)
2480
+ cache.label = nodeOptions.label;
2481
+ }
2358
2482
  /** label in not text shape */
2359
2483
  if (nodeOptions.shape !== "text" && nodeOptions.label) {
2360
2484
  this.context.font = `${nodeOptions.labelStyle} normal ${nodeOptions.labelWeight} ${nodeOptions.labelSize}px ${nodeOptions.labelFont}`;
@@ -2362,7 +2486,7 @@ function updateNodeCache(types) {
2362
2486
  this.context.textAlign = nodeOptions.labelAlign;
2363
2487
  if (nodeOptions.labelWidth == undefined ||
2364
2488
  this.context.measureText(nodeOptions.label).width <= nodeOptions.labelWidth) {
2365
- this.cachedNodeLabel[node.id] = [nodeOptions.label];
2489
+ this.cachedNodeLabel[i] = [nodeOptions.label];
2366
2490
  }
2367
2491
  const { lines } = getTextLines({
2368
2492
  context: this.context,
@@ -2375,7 +2499,7 @@ function updateNodeCache(types) {
2375
2499
  textStyle: nodeOptions.labelStyle,
2376
2500
  textWeight: nodeOptions.labelWeight,
2377
2501
  });
2378
- this.cachedNodeLabel[node.id] = lines;
2502
+ this.cachedNodeLabel[i] = lines;
2379
2503
  /** label in text shape */
2380
2504
  }
2381
2505
  else if (nodeOptions.shape === "text" && nodeOptions.label) {
@@ -2399,18 +2523,29 @@ function updateNodeCache(types) {
2399
2523
  (lines.length - 1) * nodeOptions.labelGap +
2400
2524
  nodeOptions.labelYPadding;
2401
2525
  nodeOptions.width = maxSize + nodeOptions.labelXPadding;
2402
- this.cachedNodeLabel[node.id] = lines;
2526
+ this.cachedNodeLabel[i] = lines;
2527
+ }
2528
+ else {
2529
+ this.cachedNodeLabel[i] = [];
2403
2530
  }
2404
2531
  }
2405
2532
  /** text */
2406
- if (nodeOptions.text && (types === true || (jsHelpers.isObject(types) && types.text))) {
2533
+ if (text) {
2534
+ if (!options) {
2535
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2536
+ const cache = this.nodeOptionsCache[i];
2537
+ if (cache) {
2538
+ cache.text = nodeOptions.text;
2539
+ cache.textVisible = nodeOptions.textVisible;
2540
+ }
2541
+ }
2542
+ if (!nodeOptions.text) {
2543
+ this.cachedNodeText[i] = [];
2544
+ return;
2545
+ }
2407
2546
  this.context.font = `${nodeOptions.textStyle} normal ${nodeOptions.textWeight} ${nodeOptions.textSize}px ${nodeOptions.textFont}`;
2408
2547
  this.context.fillStyle = nodeOptions.textColor;
2409
2548
  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
2549
  const { lines } = getTextLines({
2415
2550
  context: this.context,
2416
2551
  maxWidth: nodeOptions.textWidth,
@@ -2422,49 +2557,230 @@ function updateNodeCache(types) {
2422
2557
  textStyle: nodeOptions.textStyle,
2423
2558
  textWeight: nodeOptions.textWeight,
2424
2559
  });
2425
- this.cachedNodeText[node.id] = lines;
2560
+ this.cachedNodeText[i] = lines;
2426
2561
  }
2427
2562
  }
2428
2563
  }
2429
2564
 
2565
+ const DEFAULT_ZOOM_EXTENT = [0.3, 10];
2566
+ const DEFAULT_TRANSLATE_EXTENT_COEFFICIENT = 8;
2567
+ const DEFAULT_MARGIN = 0.25;
2430
2568
  function initZoom(currentZoom) {
2431
2569
  if (!this.area)
2432
2570
  throw new Error("bad init data");
2571
+ const bounds = computeGraphBounds(this.nodes);
2572
+ const zoomExtent = resolveZoomExtent(this.graphSettings.zoomExtent, this.width, this.height, this.graphSettings.zoomExtentMargin ?? DEFAULT_MARGIN, bounds);
2573
+ const translateExtent = resolveTranslateExtent(this.graphSettings.translateExtent, this.graphSettings.translateExtentCoefficient, this.graphSettings.translateExtentOverlap, this.width, this.height, zoomExtent, bounds);
2574
+ this._translateExtent = translateExtent;
2433
2575
  const zoomInstance = d3Zoom.zoom()
2434
- .scaleExtent(this.graphSettings.zoomExtent)
2576
+ .scaleExtent(zoomExtent)
2435
2577
  .on("zoom", (event) => {
2578
+ if (this._zoomAnimating)
2579
+ return;
2436
2580
  this.listeners.onZoom?.call?.(this, event);
2437
2581
  const oldTransform = this.areaTransform;
2438
2582
  this.areaTransform = event.transform;
2439
2583
  if (this.areaTransform.k !== oldTransform.k) {
2440
2584
  updateLinkCache.call(this);
2441
- updateNodeCache.call(this, true);
2585
+ updateNodeCache.call(this);
2442
2586
  }
2443
- if (!this.simulationWorking && !this.highlightWorking)
2444
- requestAnimationFrame(() => this.draw());
2587
+ this.tick();
2445
2588
  });
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
- }
2589
+ zoomInstance.translateExtent(translateExtent);
2457
2590
  d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2458
2591
  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);
2592
+ this.areaTransform = new d3Zoom.ZoomTransform(Math.max(zoomInitial?.k ?? 1, zoomExtent[0]), zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2460
2593
  d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
2461
2594
  }
2595
+ function resolveZoomExtent(userExtent, viewWidth, viewHeight, margin, bounds) {
2596
+ if (userExtent &&
2597
+ userExtent.length === 2 &&
2598
+ userExtent[0] != undefined &&
2599
+ userExtent[1] != undefined) {
2600
+ return userExtent;
2601
+ }
2602
+ if (!bounds)
2603
+ return DEFAULT_ZOOM_EXTENT;
2604
+ const graphWidth = bounds.maxX - bounds.minX;
2605
+ const graphHeight = bounds.maxY - bounds.minY;
2606
+ if (graphWidth === 0 || graphHeight === 0)
2607
+ return DEFAULT_ZOOM_EXTENT;
2608
+ const scaleToFit = (1 - margin) / Math.max(graphWidth / viewWidth, graphHeight / viewHeight);
2609
+ return [scaleToFit, DEFAULT_ZOOM_EXTENT[1]];
2610
+ }
2611
+ function resolveTranslateExtent(userExtent, userCoefficient, overlap, viewWidth, viewHeight, zoomExtent, bounds) {
2612
+ if (userExtent && userExtent.length >= 2 && jsHelpers.isArray(userExtent[0]) && jsHelpers.isArray(userExtent[1])) {
2613
+ return [
2614
+ [
2615
+ userExtent[0][0] ?? -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2616
+ userExtent[0][1] ?? -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2617
+ ],
2618
+ [
2619
+ userExtent[1][0] ?? viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2620
+ userExtent[1][1] ?? viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2621
+ ],
2622
+ ];
2623
+ }
2624
+ if (userCoefficient) {
2625
+ const [cx, cy] = jsHelpers.isArray(userCoefficient)
2626
+ ? userCoefficient
2627
+ : [userCoefficient, userCoefficient];
2628
+ return [
2629
+ [-viewWidth * cx, -viewHeight * cy],
2630
+ [viewWidth * cx, viewHeight * cy],
2631
+ ];
2632
+ }
2633
+ if (!bounds) {
2634
+ return [
2635
+ [
2636
+ -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2637
+ -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2638
+ ],
2639
+ [
2640
+ viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2641
+ viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2642
+ ],
2643
+ ];
2644
+ }
2645
+ const minZoom = zoomExtent[0];
2646
+ const zoomViewWidth = viewWidth / minZoom;
2647
+ const zoomViewHeight = viewHeight / minZoom;
2648
+ const graphWidth = bounds.maxX - bounds.minX;
2649
+ const graphHeight = bounds.maxY - bounds.minY;
2650
+ const minX = bounds.minX + overlap * graphWidth - zoomViewWidth;
2651
+ const maxX = bounds.maxX - overlap * graphWidth + zoomViewWidth;
2652
+ const minY = bounds.minY + overlap * graphHeight - zoomViewHeight;
2653
+ const maxY = bounds.maxY - overlap * graphHeight + zoomViewHeight;
2654
+ return [
2655
+ [minX, minY],
2656
+ [maxX, maxY],
2657
+ ];
2658
+ }
2659
+
2660
+ function initSimulation() {
2661
+ if (!this.simulation) {
2662
+ this.simulation = d3Force.forceSimulation()
2663
+ .nodes(this.nodes)
2664
+ .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2665
+ .on("tick", () => {
2666
+ this.draw();
2667
+ })
2668
+ .on("end", () => {
2669
+ this.listeners.onSimulationEnd?.call?.(this);
2670
+ if (this.area) {
2671
+ initZoom.call(this, this.areaTransform);
2672
+ }
2673
+ if (this.graphSettings.showDrawTime) {
2674
+ getDrawTime();
2675
+ // eslint-disable-next-line no-console
2676
+ console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2677
+ resetDrawTime();
2678
+ }
2679
+ })
2680
+ .stop();
2681
+ initSimulationForces.call(this);
2682
+ }
2683
+ }
2684
+ function initSimulationForces() {
2685
+ if (!this.simulation)
2686
+ return;
2687
+ const linkForce = this.simulation.force("link");
2688
+ if (!linkForce)
2689
+ return;
2690
+ if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2691
+ this.simulation.force("x", null);
2692
+ }
2693
+ if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2694
+ this.simulation.force("y", null);
2695
+ }
2696
+ if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2697
+ this.simulation.force("charge")) {
2698
+ this.simulation.force("charge", null);
2699
+ }
2700
+ if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2701
+ this.simulation.force("center")) {
2702
+ this.simulation.force("center", null);
2703
+ }
2704
+ if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2705
+ linkForce.distance(0).strength(0).iterations(0);
2706
+ }
2707
+ if (this.forceSettings.forces && this.forceSettings.linkForce) {
2708
+ linkForce
2709
+ .distance(this.forceSettings.linkDistance)
2710
+ .strength(this.forceSettings.linkStrength)
2711
+ .iterations(this.forceSettings.linkIterations);
2712
+ }
2713
+ if (this.forceSettings.forces && this.forceSettings.xForce) {
2714
+ this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2715
+ }
2716
+ if (this.forceSettings.forces && this.forceSettings.yForce) {
2717
+ this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2718
+ }
2719
+ if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2720
+ this.simulation.force("charge", d3Force.forceManyBody()
2721
+ .strength(this.forceSettings.chargeStrength)
2722
+ .distanceMax(Infinity)
2723
+ .distanceMin(this.forceSettings.chargeDistanceMin));
2724
+ }
2725
+ if (this.forceSettings.forces && this.forceSettings.centerForce) {
2726
+ this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2727
+ }
2728
+ initCollideForce.call(this, true);
2729
+ }
2730
+ function initCollideForce(forceUpdate) {
2731
+ if (!this.simulation)
2732
+ return;
2733
+ if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2734
+ this.simulation.force("collide")) {
2735
+ this.simulation.force("collide", null);
2736
+ }
2737
+ if (this.forceSettings.forces && this.forceSettings.collideForce) {
2738
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2739
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2740
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2741
+ if (isMaxCollideNodes && isMaxCollideLinks) {
2742
+ this.simulation.force("collide", null);
2743
+ }
2744
+ else if (!this.simulation.force("collide") || forceUpdate) {
2745
+ this.simulation.force("collide", d3Force.forceCollide()
2746
+ .radius((node, index) => {
2747
+ const nodeOptions = this.nodeOptionsCache[index];
2748
+ if (!nodeOptions)
2749
+ return 0;
2750
+ switch (nodeOptions.shape) {
2751
+ case "circle": {
2752
+ if (this.forceSettings.collideRadius) {
2753
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2754
+ }
2755
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2756
+ }
2757
+ case "square": {
2758
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2759
+ this.forceSettings.collideAdditionalRadius);
2760
+ }
2761
+ case "text": {
2762
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2763
+ this.forceSettings.collideAdditionalRadius);
2764
+ }
2765
+ default: {
2766
+ if (this.forceSettings.collideRadius) {
2767
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2768
+ }
2769
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2770
+ }
2771
+ }
2772
+ })
2773
+ .strength(this.forceSettings.collideStrength)
2774
+ .iterations(this.forceSettings.collideIterations));
2775
+ }
2776
+ }
2777
+ }
2462
2778
 
2463
2779
  class GraphCanvas {
2464
2780
  /** initial data */
2465
2781
  nodes;
2466
2782
  links;
2467
- particles = {};
2783
+ particles = [];
2468
2784
  width;
2469
2785
  height;
2470
2786
  root;
@@ -2481,19 +2797,25 @@ class GraphCanvas {
2481
2797
  context;
2482
2798
  simulation;
2483
2799
  areaTransform = d3Zoom.zoomIdentity;
2800
+ _translateExtent;
2484
2801
  areaRect;
2485
2802
  draw;
2486
2803
  eventAbortController;
2487
- cachedNodeText = {};
2488
- cachedNodeLabel = {};
2489
- linkOptionsCache = {};
2490
- nodeOptionsCache = {};
2804
+ cachedNodeText = [];
2805
+ cachedNodeLabel = [];
2806
+ linkOptionsCache = [];
2807
+ nodeOptionsCache = [];
2491
2808
  isDragging = false;
2492
2809
  highlightedNode = null;
2493
2810
  highlightedLink = null;
2494
2811
  highlightedNeighbors = null;
2495
2812
  highlightProgress = 1;
2496
- highlightWorking = false;
2813
+ highlightStart = null;
2814
+ highlightPositive = false;
2815
+ highlightController;
2816
+ _lastNodeZoomK;
2817
+ _lastLinkZoomK;
2818
+ _zoomAnimating = false;
2497
2819
  get simulationWorking() {
2498
2820
  const simulationAlpha = this.simulation?.alpha?.() ?? 0;
2499
2821
  const simulationAlphaMin = this.simulation?.alphaMin?.() ?? 0;
@@ -2528,13 +2850,148 @@ class GraphCanvas {
2528
2850
  nodes: this.nodes,
2529
2851
  };
2530
2852
  }
2853
+ fitToView(margin = this.graphSettings.zoomToFitMargin, duration = this.graphSettings.zoomAnimationDuration) {
2854
+ const area = this.area;
2855
+ if (!area)
2856
+ return;
2857
+ const bounds = computeGraphBounds(this.nodes);
2858
+ if (!bounds)
2859
+ return;
2860
+ const graphWidth = bounds.maxX - bounds.minX;
2861
+ const graphHeight = bounds.maxY - bounds.minY;
2862
+ if (graphWidth === 0 || graphHeight === 0)
2863
+ return;
2864
+ const graphCenterX = bounds.minX + graphWidth / 2;
2865
+ const graphCenterY = bounds.minY + graphHeight / 2;
2866
+ const scale = (1 - margin) / Math.max(graphWidth / this.width, graphHeight / this.height);
2867
+ const clampedScale = Math.min(scale, this.graphSettings.zoomExtent?.[1] ?? scale);
2868
+ const target = d3Zoom.zoomIdentity
2869
+ .translate(this.width / 2, this.height / 2)
2870
+ .scale(clampedScale)
2871
+ .translate(-graphCenterX, -graphCenterY);
2872
+ if (!this.graphSettings.zoomAnimation) {
2873
+ this.areaTransform = target;
2874
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
2875
+ this.clearCache(true);
2876
+ this.tick();
2877
+ return;
2878
+ }
2879
+ this.animateZoom(area, target, this.areaTransform, duration);
2880
+ }
2881
+ focusOnNode(nodeId, scale = this.graphSettings.zoomToNodeScale, duration = this.graphSettings.zoomAnimationDuration) {
2882
+ const area = this.area;
2883
+ if (!area)
2884
+ return;
2885
+ const node = this.nodes.find((n) => n.id === nodeId);
2886
+ if (!node)
2887
+ return;
2888
+ if (node.x == undefined || node.y == undefined)
2889
+ return;
2890
+ const target = d3Zoom.zoomIdentity
2891
+ .translate(this.width / 2, this.height / 2)
2892
+ .scale(scale)
2893
+ .translate(-node.x, -node.y);
2894
+ if (!this.graphSettings.zoomAnimation) {
2895
+ this.areaTransform = target;
2896
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
2897
+ this.clearCache(true);
2898
+ this.tick();
2899
+ return;
2900
+ }
2901
+ this.animateZoom(area, target, this.areaTransform, duration);
2902
+ }
2903
+ animateHighlight(node, link, baseDuration = this.highlightSettings.highlightDuration) {
2904
+ let positive = true;
2905
+ if (node && (this.highlightedNode !== node || !this.highlightPositive)) {
2906
+ this.highlightedNode = node;
2907
+ this.highlightedNeighbors = new Set(node.neighbors);
2908
+ this.particles = [];
2909
+ this.highlightedLink = null;
2910
+ this.highlightStart = performance.now();
2911
+ }
2912
+ else if (link && (this.highlightedLink !== link || !this.highlightPositive)) {
2913
+ const { sourceId, targetId } = extractLinkPointIds(link);
2914
+ this.highlightProgress = 0;
2915
+ this.highlightedLink = link;
2916
+ this.highlightedNeighbors = new Set([sourceId, targetId]);
2917
+ this.particles = [];
2918
+ this.highlightedNode = null;
2919
+ this.highlightStart = performance.now();
2920
+ }
2921
+ else if (!node && !link && this.highlightPositive) {
2922
+ positive = false;
2923
+ }
2924
+ else {
2925
+ return;
2926
+ }
2927
+ if (this.highlightController) {
2928
+ this.highlightController.abort();
2929
+ }
2930
+ const controller = new AbortController();
2931
+ this.highlightPositive = positive;
2932
+ this.highlightController = controller;
2933
+ const startTime = performance.now();
2934
+ const startProgress = this.highlightProgress;
2935
+ const targetProgress = positive ? 1 : 0;
2936
+ const delta = targetProgress - startProgress;
2937
+ const duration = baseDuration * Math.abs(delta);
2938
+ const animate = () => {
2939
+ if (controller.signal.aborted)
2940
+ return;
2941
+ const elapsed = performance.now() - startTime;
2942
+ const t = Math.min(elapsed / duration, 1);
2943
+ const current = startProgress + delta * t;
2944
+ const eased = current < 0.5 ? 4 * current * current * current : 1 - (-2 * current + 2) ** 3 / 2;
2945
+ this.highlightProgress = eased;
2946
+ if (t < 1 || positive) {
2947
+ requestAnimationFrame(animate);
2948
+ this.draw();
2949
+ }
2950
+ else {
2951
+ this.highlightedNode = null;
2952
+ this.highlightedLink = null;
2953
+ this.highlightedNeighbors = null;
2954
+ this.highlightStart = null;
2955
+ this.particles = [];
2956
+ this.tick();
2957
+ }
2958
+ };
2959
+ requestAnimationFrame(animate);
2960
+ }
2961
+ animateZoom(area, target, start, duration) {
2962
+ this._zoomAnimating = true;
2963
+ const startTime = performance.now();
2964
+ const animate = () => {
2965
+ const elapsed = performance.now() - startTime;
2966
+ const t = Math.min(elapsed / duration, 1);
2967
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
2968
+ const x = start.x + (target.x - start.x) * eased;
2969
+ const y = start.y + (target.y - start.y) * eased;
2970
+ const k = start.k + (target.k - start.k) * eased;
2971
+ this.areaTransform = new d3Zoom.ZoomTransform(k, x, y);
2972
+ d3Zoom.zoom().transform(d3Selection.select(area), this.areaTransform);
2973
+ updateLinkCache.call(this);
2974
+ updateNodeCache.call(this);
2975
+ this.tick();
2976
+ if (t < 1) {
2977
+ requestAnimationFrame(animate);
2978
+ }
2979
+ else {
2980
+ this._zoomAnimating = false;
2981
+ this.clearCache(true);
2982
+ this.tick();
2983
+ }
2984
+ };
2985
+ requestAnimationFrame(animate);
2986
+ }
2531
2987
  changeData(options, alpha = 0.5, clearCache = true) {
2532
2988
  if (options.links != undefined)
2533
2989
  this.links = options.links;
2534
2990
  if (options.nodes != undefined)
2535
2991
  this.nodes = options.nodes;
2536
- if (options.nodes != undefined || options.links != undefined)
2992
+ if (options.nodes != undefined || options.links != undefined) {
2537
2993
  this.updateData(alpha, clearCache);
2994
+ }
2538
2995
  }
2539
2996
  changeSettings(options, clearCache = true) {
2540
2997
  if (options.graphSettings) {
@@ -2609,31 +3066,76 @@ class GraphCanvas {
2609
3066
  }
2610
3067
  clearCache(keys) {
2611
3068
  if (keys === true) {
2612
- updateNodeCache.call(this, true);
2613
- updateLinkCache.call(this);
3069
+ this.nodeOptionsCache.length = 0;
3070
+ this.linkOptionsCache.length = 0;
3071
+ this.cachedNodeLabel.length = 0;
3072
+ this.cachedNodeText.length = 0;
2614
3073
  }
2615
3074
  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);
3075
+ for (const key of keys) {
3076
+ switch (key) {
3077
+ case GRAPH_CACHE_TYPE.NodeText: {
3078
+ this.cachedNodeText.length = 0;
3079
+ break;
3080
+ }
3081
+ case GRAPH_CACHE_TYPE.NodeLabel: {
3082
+ this.cachedNodeLabel.length = 0;
3083
+ break;
3084
+ }
3085
+ case GRAPH_CACHE_TYPE.NodeOptions: {
3086
+ this.nodeOptionsCache.length = 0;
3087
+ break;
3088
+ }
3089
+ case GRAPH_CACHE_TYPE.LinkOptions: {
3090
+ this.linkOptionsCache.length = 0;
3091
+ break;
3092
+ }
3093
+ }
2627
3094
  }
2628
3095
  }
3096
+ updateNodeCache.call(this);
3097
+ updateLinkCache.call(this);
2629
3098
  }
2630
3099
  tick() {
2631
- if (!this.simulationWorking && !this.highlightWorking)
3100
+ if (!this.simulationWorking)
2632
3101
  this.draw();
2633
3102
  }
2634
- restart(alpha) {
2635
- if (this.simulation)
3103
+ restart(alpha, options) {
3104
+ if (!this.simulation)
3105
+ return;
3106
+ const settings = {
3107
+ precompute: options?.precompute ?? this.forceSettings.precompute,
3108
+ precomputeMaxTimeMs: options?.precomputeMaxTimeMs ?? this.forceSettings?.precomputeMaxTimeMs,
3109
+ precomputeMaxTicks: options?.precomputeMaxTicks ?? this.forceSettings?.precomputeMaxTicks ?? 300,
3110
+ precomputeDisableForcesAfter: options?.precomputeDisableForcesAfter ??
3111
+ this.forceSettings?.precomputeDisableForcesAfter ??
3112
+ false,
3113
+ };
3114
+ if (!settings.precompute) {
2636
3115
  this.simulation.alpha(alpha ?? 1).restart();
3116
+ return;
3117
+ }
3118
+ if (settings.precomputeDisableForcesAfter) {
3119
+ this.forceSettings = forceSettingsGetter({ forces: true }, this.forceSettings);
3120
+ }
3121
+ initSimulationForces.call(this);
3122
+ this.simulation.stop();
3123
+ this.simulation.alpha(alpha ?? 1);
3124
+ const startTime = performance.now();
3125
+ let ticks = 0;
3126
+ while (performance.now() - startTime < settings.precomputeMaxTimeMs &&
3127
+ ticks < settings.precomputeMaxTicks) {
3128
+ this.simulation.tick(1);
3129
+ ticks++;
3130
+ if (this.simulation.alpha() <= this.simulation.alphaMin())
3131
+ break;
3132
+ }
3133
+ if (settings.precomputeDisableForcesAfter) {
3134
+ this.forceSettings = forceSettingsGetter({ forces: false }, this.forceSettings);
3135
+ }
3136
+ initSimulationForces.call(this);
3137
+ this.simulation.restart();
3138
+ this.tick();
2637
3139
  }
2638
3140
  start() {
2639
3141
  if (this.simulation)
@@ -2670,8 +3172,7 @@ class GraphCanvas {
2670
3172
  updateSimulation() {
2671
3173
  if (this.simulation) {
2672
3174
  initSimulationForces.call(this);
2673
- this.simulation.alpha(1);
2674
- this.simulation.restart();
3175
+ this.restart(1);
2675
3176
  }
2676
3177
  }
2677
3178
  clearState() {
@@ -2680,7 +3181,8 @@ class GraphCanvas {
2680
3181
  this.highlightedLink = null;
2681
3182
  this.highlightedNeighbors = null;
2682
3183
  this.highlightProgress = 0;
2683
- this.highlightWorking = false;
3184
+ this.highlightStart = null;
3185
+ this.highlightPositive = false;
2684
3186
  }
2685
3187
  updateData(alpha = 0.5, clearCache = true) {
2686
3188
  if (clearCache) {
@@ -2688,9 +3190,7 @@ class GraphCanvas {
2688
3190
  }
2689
3191
  if (this.simulation) {
2690
3192
  initCollideForce.call(this, false);
2691
- this.simulation
2692
- .nodes(this.nodes)
2693
- .force("link", d3Force.forceLink(this.links)
3193
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2694
3194
  .id(this.nodeSettings.idGetter.bind(this))
2695
3195
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2696
3196
  ? this.forceSettings.linkDistance
@@ -2700,16 +3200,17 @@ class GraphCanvas {
2700
3200
  : 0)
2701
3201
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2702
3202
  ? this.forceSettings.linkIterations
2703
- : 0))
2704
- .alpha(alpha)
2705
- .restart();
3203
+ : 0));
3204
+ this.restart(alpha);
3205
+ initZoom.call(this, this.areaTransform);
2706
3206
  }
2707
3207
  }
2708
3208
  init() {
2709
3209
  initArea.call(this);
2710
- updateNodeCache.call(this, true);
3210
+ updateNodeCache.call(this);
2711
3211
  updateLinkCache.call(this);
2712
3212
  initSimulation.call(this);
3213
+ this.restart(1);
2713
3214
  initDnd.call(this);
2714
3215
  initZoom.call(this);
2715
3216
  initResize.call(this);
@@ -2875,22 +3376,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
2875
3376
  label: "Расширение границы связи для курсора",
2876
3377
  },
2877
3378
  {
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,
3379
+ id: "highlightDuration",
3380
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3381
+ max: 1000,
3382
+ min: 10,
2891
3383
  step: 1,
2892
3384
  type: "range",
2893
- label: "Скорость применения анимации в кадрах",
3385
+ label: "Скорость анимации",
2894
3386
  },
2895
3387
  ];
2896
3388
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3363,19 +3855,22 @@ const LINK_SETTINGS_CONTROLS = [
3363
3855
  initialValue: LINK_SETTINGS.particles,
3364
3856
  },
3365
3857
  {
3366
- id: "particleFlexSpeed",
3367
- type: "checkbox",
3368
- label: "Гибкий расчет скорости частицы",
3369
- initialValue: LINK_SETTINGS.particleFlexSpeed,
3858
+ id: "particleSpeedByDistance",
3859
+ initialValue: LINK_SETTINGS.particleSpeedByDistance,
3860
+ max: 500,
3861
+ min: 1,
3862
+ step: 1,
3863
+ type: "range",
3864
+ label: "Скорость частицы на один пиксель",
3370
3865
  },
3371
3866
  {
3372
- id: "particleFlexSpeedCoefficient",
3373
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3374
- max: 10,
3375
- min: 0.1,
3376
- step: 0.1,
3867
+ id: "particleCountByDistance",
3868
+ initialValue: LINK_SETTINGS.particleCountByDistance,
3869
+ max: 250,
3870
+ min: 1,
3871
+ step: 1,
3377
3872
  type: "range",
3378
- label: "Коэффициент скорости частицы относительно расстояния",
3873
+ label: "Количество пикселей для появления частицы",
3379
3874
  },
3380
3875
  ];
3381
3876
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3470,24 +3965,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3470
3965
  label: "Радиус",
3471
3966
  initialValue: LINK_OPTIONS.particleRadius,
3472
3967
  },
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
3968
  {
3492
3969
  id: "particleBorderColor",
3493
3970
  type: "color",
@@ -3921,10 +4398,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
3921
4398
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
3922
4399
  exports.NODE_SETTINGS = NODE_SETTINGS;
3923
4400
  exports.NODE_SETTINGS_CONTROLS = NODE_SETTINGS_CONTROLS;
4401
+ exports.PERFOMANCE_NODE_OPTIONS = PERFOMANCE_NODE_OPTIONS;
4402
+ exports.PERFORMANCE_FORCE_SETTINGS = PERFORMANCE_FORCE_SETTINGS;
4403
+ exports.PERFORMANCE_GRAPH_SETTINGS = PERFORMANCE_GRAPH_SETTINGS;
4404
+ exports.PERFORMANCE_HIGHLIGHT_SETTINGS = PERFORMANCE_HIGHLIGHT_SETTINGS;
4405
+ exports.PERFORMANCE_LINK_OPTIONS = PERFORMANCE_LINK_OPTIONS;
4406
+ exports.PERFORMANCE_LINK_SETTINGS = PERFORMANCE_LINK_SETTINGS;
4407
+ exports.PERFORMANCE_NODE_SETTINGS = PERFORMANCE_NODE_SETTINGS;
3924
4408
  exports.animationByProgress = animationByProgress;
4409
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
3925
4410
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
3926
4411
  exports.colorGetter = colorGetter;
3927
4412
  exports.colorToRgb = colorToRgb;
4413
+ exports.computeGraphBounds = computeGraphBounds;
3928
4414
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
3929
4415
  exports.drawText = drawText;
3930
4416
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -3936,6 +4422,7 @@ exports.getDrawNode = getDrawNode;
3936
4422
  exports.getParticlePosition = getParticlePosition;
3937
4423
  exports.graphSettingsGetter = graphSettingsGetter;
3938
4424
  exports.highlightSettingsGetter = highlightSettingsGetter;
4425
+ exports.isEmptyObject = isEmptyObject;
3939
4426
  exports.isNodeVisible = isNodeVisible;
3940
4427
  exports.isOverlapsNode = isOverlapsNode;
3941
4428
  exports.linkByPointerGetter = linkByPointerGetter;