@krainovsd/graph 0.13.0-beta3 → 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 +1040 -533
  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 -10
  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 +5 -2
  38. package/lib/esm/module/GraphCanvas/lib/utils/calculate-curve-link-position-by-node.js.map +1 -1
  39. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-node.js +5 -2
  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 +1 -5
  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 +47 -62
  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 +45 -72
  66. package/lib/esm/module/GraphCanvas/slices/init-pointer.js.map +1 -1
  67. package/lib/esm/module/GraphCanvas/slices/init-resize.js +6 -2
  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 +33 -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 +106 -56
  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;
@@ -735,47 +783,54 @@ function isNodeVisible(opts) {
735
783
  }
736
784
  }
737
785
 
786
+ const FRAME_INTERVAL = 1000 / 60;
738
787
  function getParticlePosition(opts) {
739
- const prevStepDifference = opts.particle.step - (opts.particle.prev?.step ?? 0);
740
- const nextStepDifference = (opts.particle.next?.step ?? 0) - opts.particle.step;
741
- const needWait = opts.particle.next &&
742
- opts.particle.next.step > opts.particle.step &&
743
- nextStepDifference <= opts.distance;
744
- const needSpeed = opts.particle.prev &&
745
- opts.particle.prev.step !== 0 &&
746
- opts.particle.prev.step < opts.particle.step &&
747
- prevStepDifference < opts.distance;
748
- if (opts.particle.step === 0 && needWait) {
749
- opts.particle.x = undefined;
750
- opts.particle.y = undefined;
751
- 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;
752
794
  }
753
- const remainingSteps = opts.totalSteps - opts.particle.step;
754
- const progress = opts.particle.step / opts.totalSteps;
755
- if (remainingSteps <= 0) {
756
- opts.particle.x = opts.xEnd;
757
- opts.particle.y = opts.yEnd;
758
- opts.particle.step = 0;
759
- 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;
760
800
  }
761
- const dx = opts.xEnd - opts.xStart;
762
- const dy = opts.yEnd - opts.yStart;
763
- const newX = opts.xStart + dx * progress;
764
- const newY = opts.yStart + dy * progress;
765
- opts.particle.x = newX;
766
- opts.particle.y = newY;
767
- if (needSpeed) {
768
- 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;
769
812
  }
770
813
  else {
771
- 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;
772
818
  }
773
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
+ }
774
832
 
775
- function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, curve, }) {
776
- if (!areaRect)
777
- return undefined;
778
- const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
833
+ function linkByPointerGetter({ pointerX, pointerY, links, linkHoverExtraZone, curve, }) {
779
834
  return d3Array.greatest(links, (link) => {
780
835
  if (!jsHelpers.isObject(link.source) ||
781
836
  !jsHelpers.isObject(link.target) ||
@@ -1046,22 +1101,38 @@ function extractLinkPointIds(link) {
1046
1101
  return { sourceId, targetId };
1047
1102
  }
1048
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
+
1049
1113
  const GRAPH_SETTINGS = {
1050
- zoomExtent: [0.3, 10],
1051
- translateExtent: [[], []],
1052
- translateExtentEnable: true,
1053
- 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,
1054
1123
  dragPlaceCoefficient: dragPlaceCoefficientGetter,
1055
1124
  zoomInitial: null,
1056
1125
  showDrawTime: true,
1057
1126
  showDrawTimeEveryTick: false,
1058
1127
  };
1128
+ const PERFORMANCE_GRAPH_SETTINGS = {
1129
+ ...GRAPH_SETTINGS,
1130
+ };
1059
1131
 
1060
1132
  const HIGHLIGHT_SETTINGS = {
1061
1133
  highlightByHoverNode: true,
1062
1134
  highlightByHoverLink: true,
1063
- highlightUpFrames: 5,
1064
- highlightDownFrames: 5,
1135
+ highlightDuration: 150,
1065
1136
  linkHoverExtraZone: 2,
1066
1137
  /** Node */
1067
1138
  highlightByNodeOnlyRoot: true,
@@ -1116,37 +1187,51 @@ const HIGHLIGHT_SETTINGS = {
1116
1187
  highlightByLinkForArrowBorderColor: null,
1117
1188
  highlightByLinkForArrowBorderSizingAdditional: 0,
1118
1189
  };
1190
+ const PERFORMANCE_HIGHLIGHT_SETTINGS = {
1191
+ ...HIGHLIGHT_SETTINGS,
1192
+ };
1119
1193
 
1120
1194
  const LINK_SETTINGS = {
1195
+ smartCache: true,
1121
1196
  prettyDraw: true,
1122
- curve: false,
1197
+ curve: true,
1123
1198
  linkScaleSwitch: 1,
1124
- linkColorAfterScaleSwitch: "#000000FF",
1199
+ linkColorAfterScaleSwitch: "#C5C5C5FF",
1125
1200
  linkColorBeforeScaleSwitch: "#999",
1126
1201
  linkWidthAfterScaleSwitch: 0.1,
1127
1202
  linkWidthBeforeScaleSwitch: 1,
1128
1203
  arrow: true,
1129
1204
  arrowByHighlight: true,
1130
1205
  particles: true,
1131
- particleFlexSpeed: true,
1132
- particleFlexSpeedCoefficient: 4,
1206
+ // spawn one more particle for N px
1207
+ particleCountByDistance: 25,
1208
+ // speed for 1px
1209
+ particleSpeedByDistance: 50,
1133
1210
  };
1134
1211
  const LINK_OPTIONS = {
1135
1212
  alpha: 1,
1136
1213
  arrowAlpha: 1,
1137
1214
  arrowSize: 2,
1138
- arrowBorderColor: "#000000FF",
1215
+ arrowBorderColor: "#C5C5C5FF",
1139
1216
  arrowBorderWidth: 0.1,
1140
1217
  particleAlpha: 1,
1141
- particleColor: "#000000FF",
1142
- particleBorderColor: "#000000FF",
1218
+ particleColor: "#C5C5C5FF",
1219
+ particleBorderColor: "#C5C5C5FF",
1143
1220
  particleBorderWidth: 0.1,
1144
- particleCount: 2,
1145
1221
  particleRadius: 0.5,
1146
- 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,
1147
1230
  };
1148
1231
 
1149
1232
  const NODE_SETTINGS = {
1233
+ smartCache: true,
1234
+ textScaleSteps: [],
1150
1235
  nodeRadiusFlexible: true,
1151
1236
  nodeRadiusLinkCountForStep: 5,
1152
1237
  nodeRadiusIncrementByStep: 1,
@@ -1183,14 +1268,14 @@ const NODE_OPTIONS = {
1183
1268
  textShiftX: 0,
1184
1269
  textFont: "Arial",
1185
1270
  textAlign: "center",
1186
- textColor: "#333",
1271
+ textColor: "#d2d2d2",
1187
1272
  textStyle: "normal",
1188
1273
  textWeight: 500,
1189
1274
  textGap: 0,
1190
1275
  label: null,
1191
1276
  labelAlpha: 1,
1192
1277
  labelAlign: "center",
1193
- labelColor: "#333",
1278
+ labelColor: "#ffffff",
1194
1279
  labelFont: "Arial",
1195
1280
  labelGap: 0,
1196
1281
  labelSize: 3.5,
@@ -1198,6 +1283,14 @@ const NODE_OPTIONS = {
1198
1283
  labelWeight: 500,
1199
1284
  labelWidth: 20,
1200
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
+ };
1201
1294
 
1202
1295
  const GRAPH_CACHE_TYPE = {
1203
1296
  NodeOptions: "nodeOptions",
@@ -1225,11 +1318,11 @@ function initArea() {
1225
1318
  if (!this.area)
1226
1319
  throw new Error("couldn't create canvas");
1227
1320
  this.container.appendChild(this.area);
1228
- this.areaRect = this.area.getBoundingClientRect();
1229
1321
  this.context = this.area.getContext("2d");
1230
1322
  if (!this.context)
1231
1323
  throw new Error("couldn't create canvas context");
1232
1324
  this.context.scale(this.dpi, this.dpi);
1325
+ this.updateRect();
1233
1326
  }
1234
1327
  }
1235
1328
 
@@ -1241,10 +1334,8 @@ function initDnd() {
1241
1334
  if (this.listeners.onDragSubject) {
1242
1335
  return this.listeners.onDragSubject.call(this, event);
1243
1336
  }
1244
- if (!this.areaRect)
1245
- return;
1246
1337
  const mouseEvent = event.sourceEvent;
1247
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1338
+ const [pointerX, pointerY] = this.getPointerAreaPosition(mouseEvent);
1248
1339
  return d3Array.greatest(this.nodes, (node) => {
1249
1340
  if (!node.x || !node.y || (jsHelpers.isBoolean(node.drag) && !node.drag))
1250
1341
  return undefined;
@@ -1260,12 +1351,16 @@ function initDnd() {
1260
1351
  if (this.simulation)
1261
1352
  this.simulation.alphaTarget(0.3).restart();
1262
1353
  }
1263
- if (!this.areaRect)
1264
- return;
1265
1354
  const mouseEvent = event.sourceEvent;
1266
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1267
- event.subject.fx = pointerX;
1268
- 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
+ }
1269
1364
  this.listeners.onMoveDragFinished?.call?.(this, event);
1270
1365
  })
1271
1366
  .on("end", (event) => {
@@ -1273,10 +1368,16 @@ function initDnd() {
1273
1368
  if (!event.active && this.simulation)
1274
1369
  this.simulation.alphaTarget(0);
1275
1370
  const sourceEvent = event.sourceEvent;
1276
- if (sourceEvent.altKey && this.areaRect) {
1277
- const [pointerX, pointerY] = pointerGetter(sourceEvent, this.areaRect, this.areaTransform);
1278
- event.subject.fx = pointerX;
1279
- 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
+ }
1280
1381
  }
1281
1382
  else {
1282
1383
  event.subject.fx = null;
@@ -1324,7 +1425,7 @@ function adjustPoint(x, y, xControl, yControl, radius) {
1324
1425
  }
1325
1426
 
1326
1427
  function getDrawLink() {
1327
- return function drawLink(link) {
1428
+ return function drawLink(link, index) {
1328
1429
  if (!this.context ||
1329
1430
  typeof link.source !== "object" ||
1330
1431
  typeof link.target !== "object" ||
@@ -1339,8 +1440,7 @@ function getDrawLink() {
1339
1440
  return;
1340
1441
  if (!link.source._visible && !link.target._visible)
1341
1442
  return;
1342
- const id = `${link.target.id}${link.source.id}`;
1343
- const linkOptions = this.linkOptionsCache[id];
1443
+ const linkOptions = this.linkOptionsCache[index];
1344
1444
  if (!linkOptions)
1345
1445
  return;
1346
1446
  if (linkOptions.drawLink) {
@@ -1355,10 +1455,12 @@ function getDrawLink() {
1355
1455
  let arrowSize = linkOptions.arrowSize;
1356
1456
  let arrowBorderWidth = linkOptions.arrowBorderWidth;
1357
1457
  let arrowBorderColor = linkOptions.arrowBorderColor;
1458
+ const currentNodeHighlighted = this.highlightedNode &&
1459
+ (this.highlightedNode.id == link.source.id || this.highlightedNode.id == link.target.id);
1358
1460
  /** Highlight */
1359
1461
  if (this.highlightedNeighbors && this.highlightedNode) {
1360
1462
  /** By Node Not Highlight */
1361
- if (this.highlightedNode.id != link.source.id && this.highlightedNode.id != link.target.id) {
1463
+ if (!currentNodeHighlighted) {
1362
1464
  const fadeOptions = linkFade({
1363
1465
  arrow: this.linkSettings.arrow,
1364
1466
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1392,9 +1494,10 @@ function getDrawLink() {
1392
1494
  arrowBorderColor = highlightOptions.arrowBorderColor;
1393
1495
  }
1394
1496
  }
1497
+ const currentLinkHighlighted = this.highlightedLink === link;
1395
1498
  if (this.highlightedNeighbors && this.highlightedLink) {
1396
1499
  /** By Link Not Highlight */
1397
- if (this.highlightedLink !== link) {
1500
+ if (!currentLinkHighlighted) {
1398
1501
  const fadeOptions = linkFade({
1399
1502
  arrow: this.linkSettings.arrow,
1400
1503
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1446,8 +1549,8 @@ function getDrawLink() {
1446
1549
  const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1447
1550
  if (!this.linkSettings.curve) {
1448
1551
  if (this.linkSettings.prettyDraw ||
1449
- this.linkSettings.particleFlexSpeed ||
1450
- (this.linkSettings.arrow && arrowAlpha > 0)) {
1552
+ (this.linkSettings.arrow && arrowAlpha > 0) ||
1553
+ (this.particles && (currentNodeHighlighted || currentLinkHighlighted))) {
1451
1554
  const position = calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, isHasArrow ? arrowSize : 0);
1452
1555
  xStart = position.xStart;
1453
1556
  xEnd = position.xEnd;
@@ -1485,6 +1588,7 @@ function getDrawLink() {
1485
1588
  link._cy = position.yControl;
1486
1589
  link._ax = position.xEndArrow;
1487
1590
  link._ay = position.yEndArrow;
1591
+ linkDistance = approximateQuadraticBezierLength(position.xStart, position.yStart, position.xControl, position.yControl, position.xEnd, position.yEnd);
1488
1592
  this.context.beginPath();
1489
1593
  this.context.moveTo(position.xStart, position.yStart);
1490
1594
  this.context.quadraticCurveTo(position.xControl, position.yControl, position.xEnd, position.yEnd);
@@ -1495,69 +1599,46 @@ function getDrawLink() {
1495
1599
  this.context.stroke();
1496
1600
  }
1497
1601
  /** Particle */
1498
- if (this.linkSettings.particles &&
1499
- ((this.highlightedNode &&
1500
- (this.highlightedNode.id === link.source.id ||
1501
- this.highlightedNode.id === link.target.id)) ||
1502
- (this.highlightedLink && this.highlightedLink === link))) {
1503
- const particleSteps = this.linkSettings.particleFlexSpeed
1504
- ? linkDistance <= 0
1505
- ? 0
1506
- : linkDistance * this.linkSettings.particleFlexSpeedCoefficient
1507
- : linkOptions.particleSteps;
1508
- const particleCount = linkOptions.particleCount;
1509
- if (!this.particles[id]) {
1510
- const sourceId = link.source.id;
1511
- 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));
1512
1606
  const particles = [];
1513
- let prevParticle;
1514
1607
  for (let i = 0; i < particleCount; i++) {
1515
1608
  const particle = {
1516
- step: 0,
1517
- sourceId,
1518
- targetId,
1519
- prev: prevParticle,
1520
- next: undefined,
1521
1609
  index: i,
1522
1610
  };
1523
- if (prevParticle)
1524
- prevParticle.next = particle;
1525
1611
  particles.push(particle);
1526
- prevParticle = particle;
1527
- }
1528
- if (particles.length >= 2) {
1529
- particles[0].prev = particles[particles.length - 1];
1530
- particles[particles.length - 1].next = particles[0];
1531
1612
  }
1532
- this.particles[id] = particles;
1613
+ this.particles[index] = particles;
1533
1614
  }
1534
- if (particleSteps !== 0) {
1535
- this.particles[id].forEach((particle) => {
1536
- if (!this.context)
1537
- return;
1538
- const distance = particleSteps / particleCount;
1539
- getParticlePosition({
1540
- distance,
1541
- particle,
1542
- totalSteps: particleSteps,
1543
- xEnd,
1544
- xStart,
1545
- yEnd,
1546
- yStart,
1547
- });
1548
- if (particle.x != undefined && particle.y != undefined) {
1549
- this.context.beginPath();
1550
- this.context.strokeStyle = linkOptions.particleBorderColor;
1551
- this.context.lineWidth = linkOptions.particleBorderWidth;
1552
- this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1553
- this.context.fillStyle = linkOptions.particleColor;
1554
- this.context.fill();
1555
- if (linkOptions.particleBorderWidth > 0) {
1556
- this.context.stroke();
1557
- }
1558
- }
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,
1559
1629
  });
1560
- }
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
+ });
1561
1642
  }
1562
1643
  /** Arrow */
1563
1644
  if (this.linkSettings.arrow && arrowAlpha > 0) {
@@ -1641,7 +1722,7 @@ function getTextLines({ context, textAlign, textColor, textFont, textStyle, text
1641
1722
  }
1642
1723
 
1643
1724
  function getDrawNode(nodeRenders, textRenders) {
1644
- return function drawNode(node) {
1725
+ return function drawNode(node, index) {
1645
1726
  if (!this.context || !node.x || !node.y)
1646
1727
  return;
1647
1728
  if (node.visible != undefined && !node.visible) {
@@ -1650,7 +1731,7 @@ function getDrawNode(nodeRenders, textRenders) {
1650
1731
  node._height = 0;
1651
1732
  return;
1652
1733
  }
1653
- const nodeOptions = this.nodeOptionsCache[node.id];
1734
+ const nodeOptions = this.nodeOptionsCache[index];
1654
1735
  if (!nodeOptions)
1655
1736
  return;
1656
1737
  if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
@@ -1809,7 +1890,7 @@ function getDrawNode(nodeRenders, textRenders) {
1809
1890
  this.context.lineWidth = borderWidth;
1810
1891
  this.context.strokeStyle = borderColor;
1811
1892
  this.context.fillStyle = color;
1812
- const labelLines = this.cachedNodeLabel[node.id];
1893
+ const labelLines = this.cachedNodeLabel[index];
1813
1894
  switch (nodeOptions.shape) {
1814
1895
  case "circle": {
1815
1896
  if (!node.image) {
@@ -1935,7 +2016,7 @@ function getDrawNode(nodeRenders, textRenders) {
1935
2016
  });
1936
2017
  }
1937
2018
  /** Text draw */
1938
- const textLines = this.cachedNodeText[node.id];
2019
+ const textLines = this.cachedNodeText[index];
1939
2020
  if (nodeOptions.textVisible && nodeOptions.text && textLines) {
1940
2021
  textRenders.push(() => {
1941
2022
  if (nodeOptions.textDraw) {
@@ -1995,57 +2076,7 @@ function getDrawNode(nodeRenders, textRenders) {
1995
2076
  }
1996
2077
 
1997
2078
  function initDraw() {
1998
- let hasHighlight = false;
1999
- function calculateHighlight(recursive) {
2000
- if (hasHighlight && !recursive)
2001
- return;
2002
- /** animation up */
2003
- if (!this.highlightWorking && this.highlightProgress > 0) {
2004
- const highlightDownStep = 1 / this.highlightSettings.highlightDownFrames;
2005
- this.highlightProgress -= highlightDownStep;
2006
- if (!this.simulationWorking) {
2007
- hasHighlight = true;
2008
- return void requestAnimationFrame(() => this.draw(true));
2009
- }
2010
- if (!this.linkSettings.particles) {
2011
- hasHighlight = false;
2012
- return;
2013
- }
2014
- }
2015
- /** animation down */
2016
- if (this.highlightWorking && this.highlightProgress < 1) {
2017
- const highlightUpStep = 1 / this.highlightSettings.highlightUpFrames;
2018
- this.highlightProgress += highlightUpStep;
2019
- if (!this.simulationWorking) {
2020
- hasHighlight = true;
2021
- return void requestAnimationFrame(() => this.draw(true));
2022
- }
2023
- if (!this.linkSettings.particles) {
2024
- hasHighlight = false;
2025
- return;
2026
- }
2027
- }
2028
- /** animation active */
2029
- if (this.linkSettings.particles && this.highlightWorking && !this.simulationWorking) {
2030
- hasHighlight = true;
2031
- return void requestAnimationFrame(() => this.draw(true));
2032
- }
2033
- /** animation stop */
2034
- if (!this.highlightWorking && this.highlightProgress <= 0) {
2035
- if (this.highlightedNeighbors || this.highlightedNode || this.highlightedLink) {
2036
- this.highlightedNeighbors = null;
2037
- this.highlightedNode = null;
2038
- this.highlightedLink = null;
2039
- this.particles = {};
2040
- if (!this.simulationWorking) {
2041
- hasHighlight = true;
2042
- return void requestAnimationFrame(() => this.draw(true));
2043
- }
2044
- }
2045
- }
2046
- hasHighlight = false;
2047
- }
2048
- function draw(recursive = false) {
2079
+ function draw() {
2049
2080
  if (!this.context)
2050
2081
  return;
2051
2082
  if (this.listeners.onDraw) {
@@ -2065,8 +2096,20 @@ function initDraw() {
2065
2096
  nodeRenders.forEach((render) => render());
2066
2097
  textRenders.forEach((render) => render());
2067
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
+ }
2068
2112
  this.listeners.onDrawFinished?.call?.(this);
2069
- calculateHighlight.bind(this)(recursive);
2070
2113
  }
2071
2114
  if (this.graphSettings.showDrawTime) {
2072
2115
  return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
@@ -2078,7 +2121,7 @@ function initPointer() {
2078
2121
  if (!this.area || !this.nodes)
2079
2122
  throw new Error("bad init data");
2080
2123
  function onHover(event) {
2081
- if (!this.area)
2124
+ if (!this.area || this.isSelecting)
2082
2125
  return;
2083
2126
  let currentNode;
2084
2127
  let currentLink;
@@ -2087,10 +2130,10 @@ function initPointer() {
2087
2130
  let highlightNode = true;
2088
2131
  let highlightLink = true;
2089
2132
  if (checkHighlightNode) {
2133
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2090
2134
  currentNode = nodeByPointerGetter({
2091
- areaRect: this.areaRect,
2092
- areaTransform: this.areaTransform,
2093
- mouseEvent: event,
2135
+ pointerX,
2136
+ pointerY,
2094
2137
  nodes: this.nodes,
2095
2138
  });
2096
2139
  if (currentNode?.highlight != undefined)
@@ -2100,11 +2143,11 @@ function initPointer() {
2100
2143
  this.area.style.cursor = "pointer";
2101
2144
  }
2102
2145
  else if (checkHighlightLink) {
2146
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2103
2147
  currentLink = linkByPointerGetter({
2148
+ pointerX,
2149
+ pointerY,
2104
2150
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2105
- areaRect: this.areaRect,
2106
- areaTransform: this.areaTransform,
2107
- mouseEvent: event,
2108
2151
  links: this.links,
2109
2152
  curve: this.linkSettings.curve,
2110
2153
  });
@@ -2120,53 +2163,23 @@ function initPointer() {
2120
2163
  else {
2121
2164
  this.area.style.cursor = "default";
2122
2165
  }
2123
- if (currentNode && highlightNode && this.highlightedNode !== currentNode) {
2124
- this.highlightedNode = currentNode;
2125
- this.highlightedLink = null;
2126
- this.highlightedNeighbors = new Set(this.highlightedNode?.neighbors ?? []);
2127
- this.highlightWorking = true;
2128
- if (!this.simulationWorking)
2129
- requestAnimationFrame(() => {
2130
- this.draw();
2131
- });
2132
- }
2133
- else if (currentLink &&
2134
- highlightLink &&
2135
- checkType(currentLink.source, lodash.isObject(currentLink.source)) &&
2136
- checkType(currentLink.target, lodash.isObject(currentLink.target)) &&
2137
- this.highlightedLink !== currentLink) {
2138
- this.highlightProgress = 0;
2139
- this.highlightedLink = currentLink;
2140
- this.highlightedNode = null;
2141
- this.highlightedNeighbors = new Set([currentLink.source.id, currentLink.target.id]);
2142
- this.highlightWorking = true;
2143
- if (!this.simulationWorking)
2144
- requestAnimationFrame(() => {
2145
- this.draw();
2146
- });
2147
- }
2148
- else if (!currentNode && !currentLink && (this.highlightedNode || this.highlightedLink)) {
2149
- this.highlightWorking = false;
2150
- if (!this.simulationWorking)
2151
- requestAnimationFrame(() => {
2152
- this.draw();
2153
- });
2154
- }
2166
+ this.animateHighlight(highlightNode ? currentNode : undefined, highlightLink ? currentLink : undefined);
2155
2167
  if (!this.listeners.onMove)
2156
2168
  return;
2157
- if (!currentNode && !checkHighlightNode)
2169
+ if (!currentNode && !checkHighlightNode) {
2170
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2158
2171
  currentNode = nodeByPointerGetter({
2159
- areaRect: this.areaRect,
2160
- areaTransform: this.areaTransform,
2161
- mouseEvent: event,
2172
+ pointerX,
2173
+ pointerY,
2162
2174
  nodes: this.nodes,
2163
2175
  });
2176
+ }
2164
2177
  if (!currentNode && (!checkHighlightNode || (!checkHighlightLink && !currentLink))) {
2178
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2165
2179
  currentLink = linkByPointerGetter({
2180
+ pointerX,
2181
+ pointerY,
2166
2182
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2167
- areaRect: this.areaRect,
2168
- areaTransform: this.areaTransform,
2169
- mouseEvent: event,
2170
2183
  links: this.links,
2171
2184
  curve: this.linkSettings.curve,
2172
2185
  });
@@ -2180,18 +2193,18 @@ function initPointer() {
2180
2193
  !("button" in event) ||
2181
2194
  event.button !== 1)
2182
2195
  return;
2196
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2183
2197
  const currentNode = nodeByPointerGetter({
2184
- areaRect: this.areaRect,
2185
- areaTransform: this.areaTransform,
2186
- mouseEvent: event,
2198
+ pointerX,
2199
+ pointerY,
2187
2200
  nodes: this.nodes,
2188
2201
  });
2189
2202
  if (!currentNode) {
2203
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2190
2204
  const currentLink = linkByPointerGetter({
2205
+ pointerX,
2206
+ pointerY,
2191
2207
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2192
- areaRect: this.areaRect,
2193
- areaTransform: this.areaTransform,
2194
- mouseEvent: event,
2195
2208
  links: this.links,
2196
2209
  curve: this.linkSettings.curve,
2197
2210
  });
@@ -2202,18 +2215,18 @@ function initPointer() {
2202
2215
  function onRightClick(event) {
2203
2216
  if (!this.listeners.onContextMenu)
2204
2217
  return;
2218
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2205
2219
  const currentNode = nodeByPointerGetter({
2206
- areaRect: this.areaRect,
2207
- areaTransform: this.areaTransform,
2208
- mouseEvent: event,
2220
+ pointerX,
2221
+ pointerY,
2209
2222
  nodes: this.nodes,
2210
2223
  });
2211
2224
  if (!currentNode) {
2225
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2212
2226
  const currentLink = linkByPointerGetter({
2227
+ pointerX,
2228
+ pointerY,
2213
2229
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2214
- areaRect: this.areaRect,
2215
- areaTransform: this.areaTransform,
2216
- mouseEvent: event,
2217
2230
  links: this.links,
2218
2231
  curve: this.linkSettings.curve,
2219
2232
  });
@@ -2224,18 +2237,18 @@ function initPointer() {
2224
2237
  function onDoubleClick(event) {
2225
2238
  if (!this.listeners.onDoubleClick)
2226
2239
  return;
2240
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2227
2241
  const currentNode = nodeByPointerGetter({
2228
- areaRect: this.areaRect,
2229
- areaTransform: this.areaTransform,
2230
- mouseEvent: event,
2242
+ pointerX,
2243
+ pointerY,
2231
2244
  nodes: this.nodes,
2232
2245
  });
2233
2246
  if (!currentNode) {
2247
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2234
2248
  const currentLink = linkByPointerGetter({
2249
+ pointerX,
2250
+ pointerY,
2235
2251
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2236
- areaRect: this.areaRect,
2237
- areaTransform: this.areaTransform,
2238
- mouseEvent: event,
2239
2252
  links: this.links,
2240
2253
  curve: this.linkSettings.curve,
2241
2254
  });
@@ -2246,18 +2259,18 @@ function initPointer() {
2246
2259
  function onClick(event) {
2247
2260
  if (this.isDragging || !this.listeners.onClick || ("button" in event && event.button !== 0))
2248
2261
  return;
2262
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2249
2263
  const currentNode = nodeByPointerGetter({
2250
- areaRect: this.areaRect,
2251
- areaTransform: this.areaTransform,
2252
- mouseEvent: event,
2264
+ pointerX,
2265
+ pointerY,
2253
2266
  nodes: this.nodes,
2254
2267
  });
2255
2268
  if (!currentNode) {
2269
+ const [pointerX, pointerY] = this.getPointerAreaPosition(event);
2256
2270
  const currentLink = linkByPointerGetter({
2271
+ pointerX,
2272
+ pointerY,
2257
2273
  linkHoverExtraZone: this.highlightSettings.linkHoverExtraZone,
2258
- areaRect: this.areaRect,
2259
- areaTransform: this.areaTransform,
2260
- mouseEvent: event,
2261
2274
  links: this.links,
2262
2275
  curve: this.linkSettings.curve,
2263
2276
  });
@@ -2315,138 +2328,202 @@ function initResize() {
2315
2328
  signal: abortController.signal,
2316
2329
  });
2317
2330
  document.addEventListener("visibilitychange", () => {
2318
- if (!document.hidden)
2319
- this.updateRect();
2331
+ if (document.hidden)
2332
+ return;
2333
+ this.updateSize();
2334
+ requestAnimationFrame(() => {
2335
+ this.updateSize();
2336
+ });
2320
2337
  }, { signal: abortController.signal });
2321
2338
  observer.observe(this.area);
2322
2339
  }
2323
2340
 
2324
- function initSimulation() {
2325
- if (!this.simulation) {
2326
- this.simulation = d3Force.forceSimulation()
2327
- .nodes(this.nodes)
2328
- .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2329
- .on("tick", () => {
2330
- this.draw();
2331
- })
2332
- .on("end", () => {
2333
- this.listeners.onSimulationEnd?.call?.(this);
2334
- if (this.graphSettings.showDrawTime) {
2335
- getDrawTime();
2336
- // eslint-disable-next-line no-console
2337
- console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2338
- resetDrawTime();
2339
- }
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,
2340
2365
  });
2341
- initSimulationForces.call(this);
2342
- }
2343
- }
2344
- function initSimulationForces() {
2345
- if (!this.simulation)
2346
- return;
2347
- const linkForce = this.simulation.force("link");
2348
- if (!linkForce)
2349
- return;
2350
- if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2351
- this.simulation.force("x", null);
2352
- }
2353
- if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2354
- this.simulation.force("y", null);
2355
- }
2356
- if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2357
- this.simulation.force("charge")) {
2358
- this.simulation.force("charge", null);
2359
- }
2360
- if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2361
- this.simulation.force("center")) {
2362
- this.simulation.force("center", null);
2363
- }
2364
- if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2365
- linkForce.distance(0).strength(0).iterations(0);
2366
- }
2367
- if (this.forceSettings.forces && this.forceSettings.linkForce) {
2368
- linkForce
2369
- .distance(this.forceSettings.linkDistance)
2370
- .strength(this.forceSettings.linkStrength)
2371
- .iterations(this.forceSettings.linkIterations);
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 });
2372
2376
  }
2373
- if (this.forceSettings.forces && this.forceSettings.xForce) {
2374
- this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
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();
2375
2432
  }
2376
- if (this.forceSettings.forces && this.forceSettings.yForce) {
2377
- this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
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();
2378
2451
  }
2379
- if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2380
- this.simulation.force("charge", d3Force.forceManyBody()
2381
- .strength(this.forceSettings.chargeStrength)
2382
- .distanceMax(Infinity)
2383
- .distanceMin(this.forceSettings.chargeDistanceMin));
2452
+ function onSelectionEnd(event) {
2453
+ if (localSelection) {
2454
+ event.stopImmediatePropagation();
2455
+ localSelection = false;
2456
+ }
2384
2457
  }
2385
- if (this.forceSettings.forces && this.forceSettings.centerForce) {
2386
- this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
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;
2387
2482
  }
2388
- initCollideForce.call(this, true);
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);
2389
2489
  }
2390
- function initCollideForce(forceUpdate) {
2391
- if (!this.simulation)
2490
+
2491
+ function updateLinkCache() {
2492
+ if (!this.context)
2392
2493
  return;
2393
- if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2394
- this.simulation.force("collide")) {
2395
- this.simulation.force("collide", null);
2396
- }
2397
- if (this.forceSettings.forces && this.forceSettings.collideForce) {
2398
- const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2399
- const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2400
- const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2401
- if (isMaxCollideNodes && isMaxCollideLinks) {
2402
- this.simulation.force("collide", null);
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;
2403
2503
  }
2404
- else if (!this.simulation.force("collide") || forceUpdate) {
2405
- this.simulation.force("collide", d3Force.forceCollide()
2406
- .radius((node, index) => {
2407
- const nodeOptions = this.nodeOptionsCache[node.id];
2408
- if (!nodeOptions)
2409
- return 0;
2410
- switch (nodeOptions.shape) {
2411
- case "circle": {
2412
- if (this.forceSettings.collideRadius) {
2413
- return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2414
- }
2415
- return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2416
- }
2417
- case "square": {
2418
- return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2419
- this.forceSettings.collideAdditionalRadius);
2420
- }
2421
- case "text": {
2422
- return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2423
- this.forceSettings.collideAdditionalRadius);
2424
- }
2425
- default: {
2426
- if (this.forceSettings.collideRadius) {
2427
- return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2428
- }
2429
- return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2430
- }
2431
- }
2432
- })
2433
- .strength(this.forceSettings.collideStrength)
2434
- .iterations(this.forceSettings.collideIterations));
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
+ }
2435
2513
  }
2514
+ return;
2436
2515
  }
2437
- }
2438
-
2439
- function updateLinkCache() {
2440
- this.linkOptionsCache = {};
2516
+ this._lastLinkZoomK = current;
2517
+ this.linkOptionsCache.length = 0;
2441
2518
  const groupMap = {};
2442
2519
  const groupSelfMap = {};
2443
2520
  for (let i = 0; i < this.links.length; i++) {
2444
2521
  const link = this.links[i];
2445
2522
  const { sourceId, targetId } = extractLinkPointIds(link);
2446
2523
  const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2447
- const id = `${targetId}${sourceId}`;
2448
- this.linkOptionsCache[id] = linkOptions;
2524
+ this.linkOptionsCache[i] = linkOptions;
2449
2525
  if (this.linkSettings.curve) {
2526
+ const id = `${targetId}${sourceId}`;
2450
2527
  if (sourceId === targetId) {
2451
2528
  groupSelfMap[id] ??= 0;
2452
2529
  link._groupIndex = ++groupSelfMap[id];
@@ -2477,13 +2554,34 @@ function indexToPosition(n) {
2477
2554
  // return n & 1 ? -(n >> 1) : n >> 1;
2478
2555
  }
2479
2556
 
2480
- function updateNodeCache(types) {
2481
- this.nodeOptionsCache = {};
2557
+ function updateNodeCache() {
2482
2558
  if (!this.context)
2483
2559
  return;
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;
2577
+ }
2578
+ this.cachedNodeText.length = 0;
2579
+ text = true;
2580
+ }
2581
+ this._lastNodeZoomK = currentZoom;
2484
2582
  for (let i = 0; i < this.nodes.length; i++) {
2485
2583
  const node = this.nodes[i];
2486
- if (types === true || (jsHelpers.isObject(types) && types.options)) {
2584
+ if (options) {
2487
2585
  const nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2488
2586
  const radius = nodeOptions.shape === "circle"
2489
2587
  ? nodeRadiusGetter({
@@ -2534,11 +2632,19 @@ function updateNodeCache(types) {
2534
2632
  nodeOptions.height = node.visible === false ? 0 : height;
2535
2633
  nodeOptions.radius = node.visible === false ? 0 : radius;
2536
2634
  nodeOptions.labelSize = labelSize;
2537
- this.nodeOptionsCache[node.id] = nodeOptions;
2635
+ this.nodeOptionsCache[i] = nodeOptions;
2538
2636
  }
2539
- const nodeOptions = this.nodeOptionsCache[node.id];
2637
+ let nodeOptions = this.nodeOptionsCache[i];
2638
+ if (!nodeOptions)
2639
+ continue;
2540
2640
  /** label */
2541
- 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
+ }
2542
2648
  /** label in not text shape */
2543
2649
  if (nodeOptions.shape !== "text" && nodeOptions.label) {
2544
2650
  this.context.font = `${nodeOptions.labelStyle} normal ${nodeOptions.labelWeight} ${nodeOptions.labelSize}px ${nodeOptions.labelFont}`;
@@ -2546,7 +2652,7 @@ function updateNodeCache(types) {
2546
2652
  this.context.textAlign = nodeOptions.labelAlign;
2547
2653
  if (nodeOptions.labelWidth == undefined ||
2548
2654
  this.context.measureText(nodeOptions.label).width <= nodeOptions.labelWidth) {
2549
- this.cachedNodeLabel[node.id] = [nodeOptions.label];
2655
+ this.cachedNodeLabel[i] = [nodeOptions.label];
2550
2656
  }
2551
2657
  const { lines } = getTextLines({
2552
2658
  context: this.context,
@@ -2559,7 +2665,7 @@ function updateNodeCache(types) {
2559
2665
  textStyle: nodeOptions.labelStyle,
2560
2666
  textWeight: nodeOptions.labelWeight,
2561
2667
  });
2562
- this.cachedNodeLabel[node.id] = lines;
2668
+ this.cachedNodeLabel[i] = lines;
2563
2669
  /** label in text shape */
2564
2670
  }
2565
2671
  else if (nodeOptions.shape === "text" && nodeOptions.label) {
@@ -2583,18 +2689,29 @@ function updateNodeCache(types) {
2583
2689
  (lines.length - 1) * nodeOptions.labelGap +
2584
2690
  nodeOptions.labelYPadding;
2585
2691
  nodeOptions.width = maxSize + nodeOptions.labelXPadding;
2586
- this.cachedNodeLabel[node.id] = lines;
2692
+ this.cachedNodeLabel[i] = lines;
2693
+ }
2694
+ else {
2695
+ this.cachedNodeLabel[i] = [];
2587
2696
  }
2588
2697
  }
2589
2698
  /** text */
2590
- 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
+ }
2591
2712
  this.context.font = `${nodeOptions.textStyle} normal ${nodeOptions.textWeight} ${nodeOptions.textSize}px ${nodeOptions.textFont}`;
2592
2713
  this.context.fillStyle = nodeOptions.textColor;
2593
2714
  this.context.textAlign = nodeOptions.textAlign;
2594
- if (nodeOptions.textWidth == undefined ||
2595
- this.context.measureText(nodeOptions.text).width <= nodeOptions.textWidth) {
2596
- this.cachedNodeText[node.id] = [nodeOptions.text];
2597
- }
2598
2715
  const { lines } = getTextLines({
2599
2716
  context: this.context,
2600
2717
  maxWidth: nodeOptions.textWidth,
@@ -2606,49 +2723,233 @@ function updateNodeCache(types) {
2606
2723
  textStyle: nodeOptions.textStyle,
2607
2724
  textWeight: nodeOptions.textWeight,
2608
2725
  });
2609
- this.cachedNodeText[node.id] = lines;
2726
+ this.cachedNodeText[i] = lines;
2610
2727
  }
2611
2728
  }
2612
2729
  }
2613
2730
 
2731
+ const DEFAULT_ZOOM_EXTENT = [0.3, 10];
2732
+ const DEFAULT_TRANSLATE_EXTENT_COEFFICIENT = 8;
2733
+ const DEFAULT_MARGIN = 0.25;
2614
2734
  function initZoom(currentZoom) {
2615
2735
  if (!this.area)
2616
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;
2617
2741
  const zoomInstance = d3Zoom.zoom()
2618
- .scaleExtent(this.graphSettings.zoomExtent)
2742
+ .scaleExtent(zoomExtent)
2743
+ .filter((event) => {
2744
+ return !(event instanceof MouseEvent && event.shiftKey);
2745
+ })
2619
2746
  .on("zoom", (event) => {
2747
+ if (this._zoomAnimating)
2748
+ return;
2620
2749
  this.listeners.onZoom?.call?.(this, event);
2621
2750
  const oldTransform = this.areaTransform;
2622
2751
  this.areaTransform = event.transform;
2623
2752
  if (this.areaTransform.k !== oldTransform.k) {
2624
2753
  updateLinkCache.call(this);
2625
- updateNodeCache.call(this, true);
2754
+ updateNodeCache.call(this);
2626
2755
  }
2627
- if (!this.simulationWorking && !this.highlightWorking)
2628
- requestAnimationFrame(() => this.draw());
2756
+ this.tick();
2629
2757
  });
2630
- if (this.graphSettings.translateExtentEnable) {
2631
- const coefficient = this.graphSettings.translateExtentCoefficient;
2632
- const [coefficientX, coefficientY] = jsHelpers.isArray(coefficient)
2633
- ? coefficient
2634
- : [coefficient, coefficient];
2635
- const [[minX = -this.width * coefficientX, minY = -this.height * coefficientY], [maxX = this.width * coefficientX, maxY = this.height * coefficientY],] = this.graphSettings.translateExtent;
2636
- zoomInstance.translateExtent([
2637
- [minX, minY],
2638
- [maxX, maxY],
2639
- ]);
2640
- }
2758
+ zoomInstance.translateExtent(translateExtent);
2641
2759
  d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2642
2760
  const zoomInitial = currentZoom ?? this.graphSettings.zoomInitial;
2643
- 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);
2644
2762
  d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
2645
2763
  }
2646
-
2647
- class GraphCanvas {
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
+ }
2947
+
2948
+ class GraphCanvas {
2648
2949
  /** initial data */
2649
2950
  nodes;
2650
2951
  links;
2651
- particles = {};
2952
+ particles = [];
2652
2953
  width;
2653
2954
  height;
2654
2955
  root;
@@ -2665,19 +2966,30 @@ class GraphCanvas {
2665
2966
  context;
2666
2967
  simulation;
2667
2968
  areaTransform = d3Zoom.zoomIdentity;
2668
- areaRect;
2969
+ _translateExtent;
2669
2970
  draw;
2670
2971
  eventAbortController;
2671
- cachedNodeText = {};
2672
- cachedNodeLabel = {};
2673
- linkOptionsCache = {};
2674
- nodeOptionsCache = {};
2972
+ cachedNodeText = [];
2973
+ cachedNodeLabel = [];
2974
+ linkOptionsCache = [];
2975
+ nodeOptionsCache = [];
2675
2976
  isDragging = false;
2676
2977
  highlightedNode = null;
2677
2978
  highlightedLink = null;
2678
2979
  highlightedNeighbors = null;
2679
2980
  highlightProgress = 1;
2680
- 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
+ }
2681
2993
  get simulationWorking() {
2682
2994
  const simulationAlpha = this.simulation?.alpha?.() ?? 0;
2683
2995
  const simulationAlphaMin = this.simulation?.alphaMin?.() ?? 0;
@@ -2706,21 +3018,72 @@ class GraphCanvas {
2706
3018
  get dpi() {
2707
3019
  return devicePixelRatio;
2708
3020
  }
2709
- getData() {
3021
+ getData = () => {
2710
3022
  return {
2711
3023
  links: this.links,
2712
3024
  nodes: this.nodes,
2713
3025
  };
2714
- }
2715
- 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) => {
2716
3078
  if (options.links != undefined)
2717
3079
  this.links = options.links;
2718
3080
  if (options.nodes != undefined)
2719
3081
  this.nodes = options.nodes;
2720
- if (options.nodes != undefined || options.links != undefined)
2721
- this.updateData(alpha, clearCache);
2722
- }
2723
- 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) => {
2724
3087
  if (options.graphSettings) {
2725
3088
  this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
2726
3089
  this.draw = initDraw.call(this);
@@ -2767,16 +3130,15 @@ class GraphCanvas {
2767
3130
  initCollideForce.call(this, true);
2768
3131
  }
2769
3132
  if (options.forceSettings) {
2770
- return void this.updateSimulation();
3133
+ return void this.updateSimulation(precompute);
2771
3134
  }
2772
3135
  this.tick();
2773
- }
2774
- updateRect() {
2775
- if (!this.area)
2776
- return;
2777
- this.areaRect = this.area.getBoundingClientRect();
2778
- }
2779
- updateSize() {
3136
+ };
3137
+ updateRect = () => {
3138
+ // if (!this.area) return;
3139
+ // this.areaRect = this.area.getBoundingClientRect();
3140
+ };
3141
+ updateSize = () => {
2780
3142
  if (!this.area)
2781
3143
  return;
2782
3144
  const { width, height } = this.root.getBoundingClientRect();
@@ -2784,57 +3146,102 @@ class GraphCanvas {
2784
3146
  this.height = height;
2785
3147
  this.area.width = this.dpi * this.width;
2786
3148
  this.area.height = this.dpi * this.height;
2787
- this.areaRect = this.area.getBoundingClientRect();
3149
+ this.updateRect();
2788
3150
  this.context = this.area.getContext("2d");
2789
3151
  if (!this.context)
2790
3152
  throw new Error("couldn't create canvas context");
2791
3153
  this.context.scale(this.dpi, this.dpi);
2792
3154
  this.draw();
2793
- }
2794
- clearCache(keys) {
3155
+ };
3156
+ clearCache = (keys) => {
2795
3157
  if (keys === true) {
2796
- updateNodeCache.call(this, true);
2797
- updateLinkCache.call(this);
3158
+ this.nodeOptionsCache.length = 0;
3159
+ this.linkOptionsCache.length = 0;
3160
+ this.cachedNodeLabel.length = 0;
3161
+ this.cachedNodeText.length = 0;
2798
3162
  }
2799
3163
  else if (jsHelpers.isArray(keys)) {
2800
- if (keys.some((k) => k === GRAPH_CACHE_TYPE.NodeText ||
2801
- k === GRAPH_CACHE_TYPE.NodeLabel ||
2802
- k === GRAPH_CACHE_TYPE.NodeOptions)) {
2803
- updateNodeCache.call(this, {
2804
- label: keys.includes(GRAPH_CACHE_TYPE.NodeLabel),
2805
- options: keys.includes(GRAPH_CACHE_TYPE.NodeOptions),
2806
- text: keys.includes(GRAPH_CACHE_TYPE.NodeOptions),
2807
- });
2808
- }
2809
- if (keys.includes(GRAPH_CACHE_TYPE.LinkOptions)) {
2810
- 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
+ }
2811
3183
  }
2812
3184
  }
2813
- }
2814
- tick() {
2815
- if (!this.simulationWorking && !this.highlightWorking)
3185
+ updateNodeCache.call(this);
3186
+ updateLinkCache.call(this);
3187
+ };
3188
+ tick = () => {
3189
+ if (!this.simulationWorking)
2816
3190
  this.draw();
2817
- }
2818
- restart(alpha) {
2819
- 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) {
2820
3204
  this.simulation.alpha(alpha ?? 1).restart();
2821
- }
2822
- 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 = () => {
2823
3230
  if (this.simulation)
2824
3231
  this.simulation.alpha(1).restart();
2825
3232
  if (this.container)
2826
3233
  this.container.style.display = "block";
2827
- }
2828
- stop() {
3234
+ };
3235
+ stop = () => {
2829
3236
  if (this.simulation)
2830
3237
  this.simulation.stop();
2831
3238
  if (this.container)
2832
3239
  this.container.style.display = "none";
2833
- }
2834
- create() {
3240
+ };
3241
+ create = () => {
2835
3242
  this.init();
2836
- }
2837
- destroy() {
3243
+ };
3244
+ destroy = () => {
2838
3245
  if (this.simulation) {
2839
3246
  this.simulation.stop();
2840
3247
  this.simulation = undefined;
@@ -2842,39 +3249,79 @@ class GraphCanvas {
2842
3249
  this.clearHTMLElements();
2843
3250
  this.clearState();
2844
3251
  this.clearCache(true);
2845
- }
2846
- 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 = () => {
2847
3279
  this.root.replaceChildren();
2848
3280
  this.area = undefined;
2849
3281
  this.context = undefined;
2850
3282
  this.container = undefined;
2851
3283
  this.eventAbortController.abort();
2852
3284
  this.eventAbortController = new AbortController();
2853
- }
2854
- updateSimulation() {
3285
+ };
3286
+ updateSimulation = (precompute = false) => {
2855
3287
  if (this.simulation) {
2856
3288
  initSimulationForces.call(this);
2857
- this.simulation.alpha(1);
2858
- this.simulation.restart();
3289
+ this.restart(1, { precompute });
2859
3290
  }
2860
- }
2861
- clearState() {
3291
+ };
3292
+ clearState = () => {
2862
3293
  this.isDragging = false;
2863
3294
  this.highlightedNode = null;
2864
3295
  this.highlightedLink = null;
2865
3296
  this.highlightedNeighbors = null;
2866
3297
  this.highlightProgress = 0;
2867
- this.highlightWorking = false;
2868
- }
2869
- 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) => {
2870
3319
  if (clearCache) {
2871
3320
  this.clearCache(clearCache);
2872
3321
  }
2873
3322
  if (this.simulation) {
2874
3323
  initCollideForce.call(this, false);
2875
- this.simulation
2876
- .nodes(this.nodes)
2877
- .force("link", d3Force.forceLink(this.links)
3324
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2878
3325
  .id(this.nodeSettings.idGetter.bind(this))
2879
3326
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2880
3327
  ? this.forceSettings.linkDistance
@@ -2884,21 +3331,95 @@ class GraphCanvas {
2884
3331
  : 0)
2885
3332
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2886
3333
  ? this.forceSettings.linkIterations
2887
- : 0))
2888
- .alpha(alpha)
2889
- .restart();
3334
+ : 0));
3335
+ this.restart(alpha, { precompute });
3336
+ initZoom.call(this, this.areaTransform);
2890
3337
  }
2891
- }
2892
- init() {
2893
- initArea.call(this);
2894
- updateNodeCache.call(this, true);
2895
- updateLinkCache.call(this);
2896
- initSimulation.call(this);
2897
- initDnd.call(this);
2898
- initZoom.call(this);
2899
- initResize.call(this);
2900
- initPointer.call(this);
2901
- }
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
+ };
2902
3423
  }
2903
3424
 
2904
3425
  const FORCE_CONTROLS = [
@@ -3059,22 +3580,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
3059
3580
  label: "Расширение границы связи для курсора",
3060
3581
  },
3061
3582
  {
3062
- id: "highlightDownFrames",
3063
- initialValue: HIGHLIGHT_SETTINGS.highlightDownFrames,
3064
- max: 60,
3065
- min: 1,
3066
- step: 1,
3067
- type: "range",
3068
- label: "Скорость отмены анимации в кадрах",
3069
- },
3070
- {
3071
- id: "highlightUpFrames",
3072
- initialValue: HIGHLIGHT_SETTINGS.highlightUpFrames,
3073
- max: 60,
3074
- min: 1,
3583
+ id: "highlightDuration",
3584
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3585
+ max: 1000,
3586
+ min: 10,
3075
3587
  step: 1,
3076
3588
  type: "range",
3077
- label: "Скорость применения анимации в кадрах",
3589
+ label: "Скорость анимации",
3078
3590
  },
3079
3591
  ];
3080
3592
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3547,19 +4059,22 @@ const LINK_SETTINGS_CONTROLS = [
3547
4059
  initialValue: LINK_SETTINGS.particles,
3548
4060
  },
3549
4061
  {
3550
- id: "particleFlexSpeed",
3551
- type: "checkbox",
3552
- label: "Гибкий расчет скорости частицы",
3553
- 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: "Скорость частицы на один пиксель",
3554
4069
  },
3555
4070
  {
3556
- id: "particleFlexSpeedCoefficient",
3557
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3558
- max: 10,
3559
- min: 0.1,
3560
- step: 0.1,
4071
+ id: "particleCountByDistance",
4072
+ initialValue: LINK_SETTINGS.particleCountByDistance,
4073
+ max: 250,
4074
+ min: 1,
4075
+ step: 1,
3561
4076
  type: "range",
3562
- label: "Коэффициент скорости частицы относительно расстояния",
4077
+ label: "Количество пикселей для появления частицы",
3563
4078
  },
3564
4079
  ];
3565
4080
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3654,24 +4169,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3654
4169
  label: "Радиус",
3655
4170
  initialValue: LINK_OPTIONS.particleRadius,
3656
4171
  },
3657
- {
3658
- id: "particleCount",
3659
- type: "range",
3660
- max: 20,
3661
- min: 1,
3662
- step: 1,
3663
- label: "Количество",
3664
- initialValue: LINK_OPTIONS.particleCount,
3665
- },
3666
- {
3667
- id: "particleSteps",
3668
- type: "range",
3669
- max: 200,
3670
- min: 1,
3671
- step: 1,
3672
- label: "Количество кадров",
3673
- initialValue: LINK_OPTIONS.particleSteps,
3674
- },
3675
4172
  {
3676
4173
  id: "particleBorderColor",
3677
4174
  type: "color",
@@ -4105,10 +4602,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
4105
4602
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
4106
4603
  exports.NODE_SETTINGS = NODE_SETTINGS;
4107
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;
4108
4612
  exports.animationByProgress = animationByProgress;
4613
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
4109
4614
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
4110
4615
  exports.colorGetter = colorGetter;
4111
4616
  exports.colorToRgb = colorToRgb;
4617
+ exports.computeGraphBounds = computeGraphBounds;
4112
4618
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
4113
4619
  exports.drawText = drawText;
4114
4620
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -4120,6 +4626,7 @@ exports.getDrawNode = getDrawNode;
4120
4626
  exports.getParticlePosition = getParticlePosition;
4121
4627
  exports.graphSettingsGetter = graphSettingsGetter;
4122
4628
  exports.highlightSettingsGetter = highlightSettingsGetter;
4629
+ exports.isEmptyObject = isEmptyObject;
4123
4630
  exports.isNodeVisible = isNodeVisible;
4124
4631
  exports.isOverlapsNode = isOverlapsNode;
4125
4632
  exports.linkByPointerGetter = linkByPointerGetter;