@krainovsd/graph 0.13.0-beta3 → 0.14.0-beta-7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/lib/cjs/index.cjs +1044 -528
  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 +303 -78
  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 +14 -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
1612
  }
1528
- if (particles.length >= 2) {
1529
- particles[0].prev = particles[particles.length - 1];
1530
- particles[particles.length - 1].next = particles[0];
1531
- }
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,210 @@ 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
+ this.highlightController?.abort();
2333
+ this.highlightedNode = null;
2334
+ this.highlightedLink = null;
2335
+ this.highlightedNeighbors = null;
2336
+ this.highlightProgress = 0;
2337
+ this.highlightStart = null;
2338
+ this.highlightPositive = false;
2339
+ return;
2340
+ }
2341
+ this.updateSize();
2342
+ requestAnimationFrame(() => {
2343
+ this.updateSize();
2344
+ });
2320
2345
  }, { signal: abortController.signal });
2321
2346
  observer.observe(this.area);
2322
2347
  }
2323
2348
 
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
- }
2349
+ function initSelection() {
2350
+ if (!this.area)
2351
+ throw new Error("bad init data");
2352
+ this.isSelecting = false;
2353
+ this.selectionRect = null;
2354
+ let selectedNodesSet = new Set();
2355
+ let selectedLinksSet = new Set();
2356
+ let localSelection = false;
2357
+ function onMouseDown(event) {
2358
+ if (event.button !== 0 || !event.shiftKey)
2359
+ return;
2360
+ if (!this.area)
2361
+ return;
2362
+ event.stopPropagation();
2363
+ event.preventDefault();
2364
+ const [startX, startY] = this.getPointerAreaPosition(event);
2365
+ this.isSelecting = true;
2366
+ localSelection = true;
2367
+ this.selectionRect = { x1: startX, y1: startY, x2: startX, y2: startY };
2368
+ selectedNodesSet = new Set();
2369
+ selectedLinksSet = new Set();
2370
+ const controller = new AbortController();
2371
+ this.area.addEventListener("mousemove", onMouseMove.bind(this), {
2372
+ signal: controller.signal,
2340
2373
  });
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);
2374
+ this.area.addEventListener("mouseup", () => {
2375
+ onMouseUp.call(this, controller);
2376
+ }, {
2377
+ signal: controller.signal,
2378
+ });
2379
+ window.addEventListener("keyup", (event) => {
2380
+ if (event.key === "Shift") {
2381
+ onMouseUp.call(this, controller);
2382
+ }
2383
+ }, { signal: controller.signal });
2372
2384
  }
2373
- if (this.forceSettings.forces && this.forceSettings.xForce) {
2374
- this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2385
+ function onMouseMove(event) {
2386
+ if (!this.isSelecting || !this.selectionRect)
2387
+ return;
2388
+ event.stopPropagation();
2389
+ const [x, y] = this.getPointerAreaPosition(event);
2390
+ this.selectionRect.x2 = x;
2391
+ this.selectionRect.y2 = y;
2392
+ const rect = normalizeRect(this.selectionRect);
2393
+ const newNodes = new Set();
2394
+ for (let i = 0; i < this.nodes.length; i++) {
2395
+ const node = this.nodes[i];
2396
+ if (node.x == undefined || node.y == undefined)
2397
+ continue;
2398
+ if (node.visible === false)
2399
+ continue;
2400
+ const cache = this.nodeOptionsCache[i];
2401
+ if (isNodeInRect(node, rect, cache)) {
2402
+ newNodes.add(node);
2403
+ }
2404
+ }
2405
+ const newLinks = new Set();
2406
+ for (const link of this.links) {
2407
+ const source = link.source;
2408
+ const target = link.target;
2409
+ if (newNodes.has(source) && newNodes.has(target)) {
2410
+ newLinks.add(link);
2411
+ }
2412
+ }
2413
+ for (const node of selectedNodesSet) {
2414
+ if (!newNodes.has(node)) {
2415
+ node._selected = false;
2416
+ this.listeners.onSelectionOut?.call(this, node, undefined);
2417
+ }
2418
+ }
2419
+ for (const link of selectedLinksSet) {
2420
+ if (!newLinks.has(link)) {
2421
+ link._selected = false;
2422
+ this.listeners.onSelectionOut?.call(this, undefined, link);
2423
+ }
2424
+ }
2425
+ for (const node of newNodes) {
2426
+ if (!selectedNodesSet.has(node)) {
2427
+ node._selected = true;
2428
+ this.listeners.onSelectionIn?.call(this, node, undefined);
2429
+ }
2430
+ }
2431
+ for (const link of newLinks) {
2432
+ if (!selectedLinksSet.has(link)) {
2433
+ link._selected = true;
2434
+ this.listeners.onSelectionIn?.call(this, undefined, link);
2435
+ }
2436
+ }
2437
+ selectedNodesSet = newNodes;
2438
+ selectedLinksSet = newLinks;
2439
+ this.draw();
2375
2440
  }
2376
- if (this.forceSettings.forces && this.forceSettings.yForce) {
2377
- this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2441
+ function onMouseUp(controller) {
2442
+ controller.abort();
2443
+ if (!this.isSelecting)
2444
+ return;
2445
+ const nodes = Array.from(selectedNodesSet);
2446
+ const links = Array.from(selectedLinksSet);
2447
+ for (const node of nodes) {
2448
+ node._selected = false;
2449
+ }
2450
+ for (const link of links) {
2451
+ link._selected = false;
2452
+ }
2453
+ this.isSelecting = false;
2454
+ this.selectionRect = null;
2455
+ selectedNodesSet = new Set();
2456
+ selectedLinksSet = new Set();
2457
+ this.listeners.onSelectionEnd?.call(this, nodes, links);
2458
+ this.tick();
2378
2459
  }
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));
2460
+ function onSelectionEnd(event) {
2461
+ if (localSelection) {
2462
+ event.stopImmediatePropagation();
2463
+ localSelection = false;
2464
+ }
2384
2465
  }
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));
2466
+ this.area.addEventListener("mousedown", onMouseDown.bind(this), {
2467
+ signal: this.eventAbortController.signal,
2468
+ });
2469
+ this.area.addEventListener("click", onSelectionEnd, { signal: this.eventAbortController.signal });
2470
+ }
2471
+ function normalizeRect(rect) {
2472
+ const x = Math.min(rect.x1, rect.x2);
2473
+ const y = Math.min(rect.y1, rect.y2);
2474
+ const width = Math.abs(rect.x2 - rect.x1);
2475
+ const height = Math.abs(rect.y2 - rect.y1);
2476
+ return { x, y, width, height };
2477
+ }
2478
+ function isNodeInRect(node, rect, cache) {
2479
+ const nx = node.x;
2480
+ const ny = node.y;
2481
+ if (nx == undefined || ny == undefined)
2482
+ return false;
2483
+ if (cache?.shape === "circle") {
2484
+ const r = cache.radius ?? 0;
2485
+ const closestX = Math.max(rect.x, Math.min(nx, rect.x + rect.width));
2486
+ const closestY = Math.max(rect.y, Math.min(ny, rect.y + rect.height));
2487
+ const dx = nx - closestX;
2488
+ const dy = ny - closestY;
2489
+ return dx * dx + dy * dy <= r * r;
2387
2490
  }
2388
- initCollideForce.call(this, true);
2491
+ const hw = (cache?.width ?? 0) / 2;
2492
+ const hh = (cache?.height ?? 0) / 2;
2493
+ return (nx - hw >= rect.x &&
2494
+ nx + hw <= rect.x + rect.width &&
2495
+ ny - hh >= rect.y &&
2496
+ ny + hh <= rect.y + rect.height);
2389
2497
  }
2390
- function initCollideForce(forceUpdate) {
2391
- if (!this.simulation)
2498
+
2499
+ function updateLinkCache() {
2500
+ if (!this.context)
2392
2501
  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);
2502
+ const current = this.areaTransform.k;
2503
+ if (this.linkSettings.smartCache &&
2504
+ this._lastLinkZoomK !== undefined &&
2505
+ this.linkOptionsCache.length > 0) {
2506
+ const corner = this.linkSettings.linkScaleSwitch;
2507
+ const prev = this._lastLinkZoomK;
2508
+ this._lastLinkZoomK = current;
2509
+ if (!((prev <= corner && current >= corner) || (prev >= corner && current <= corner))) {
2510
+ return;
2403
2511
  }
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));
2512
+ for (let i = 0; i < this.links.length; i++) {
2513
+ const link = this.links[i];
2514
+ const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2515
+ const cache = this.linkOptionsCache[i];
2516
+ if (cache) {
2517
+ cache.color = linkOptions.color;
2518
+ cache.arrowColor = linkOptions.arrowColor;
2519
+ cache.width = linkOptions.width;
2520
+ }
2435
2521
  }
2522
+ return;
2436
2523
  }
2437
- }
2438
-
2439
- function updateLinkCache() {
2440
- this.linkOptionsCache = {};
2524
+ this._lastLinkZoomK = current;
2525
+ this.linkOptionsCache.length = 0;
2441
2526
  const groupMap = {};
2442
2527
  const groupSelfMap = {};
2443
2528
  for (let i = 0; i < this.links.length; i++) {
2444
2529
  const link = this.links[i];
2445
2530
  const { sourceId, targetId } = extractLinkPointIds(link);
2446
2531
  const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2447
- const id = `${targetId}${sourceId}`;
2448
- this.linkOptionsCache[id] = linkOptions;
2532
+ this.linkOptionsCache[i] = linkOptions;
2449
2533
  if (this.linkSettings.curve) {
2534
+ const id = `${targetId}${sourceId}`;
2450
2535
  if (sourceId === targetId) {
2451
2536
  groupSelfMap[id] ??= 0;
2452
2537
  link._groupIndex = ++groupSelfMap[id];
@@ -2477,13 +2562,34 @@ function indexToPosition(n) {
2477
2562
  // return n & 1 ? -(n >> 1) : n >> 1;
2478
2563
  }
2479
2564
 
2480
- function updateNodeCache(types) {
2481
- this.nodeOptionsCache = {};
2565
+ function updateNodeCache() {
2482
2566
  if (!this.context)
2483
2567
  return;
2568
+ const currentZoom = this.areaTransform.k;
2569
+ const options = this.nodeOptionsCache.length === 0;
2570
+ const label = this.cachedNodeLabel.length === 0;
2571
+ let text = this.cachedNodeText.length === 0;
2572
+ if (this.nodeSettings.smartCache &&
2573
+ this._lastNodeZoomK !== undefined &&
2574
+ !options &&
2575
+ !text &&
2576
+ !label) {
2577
+ const thresholds = this.nodeSettings.textScaleSteps;
2578
+ if (thresholds && thresholds.length > 0) {
2579
+ const min = Math.min(this._lastNodeZoomK, currentZoom);
2580
+ const max = Math.max(this._lastNodeZoomK, currentZoom);
2581
+ const crossed = thresholds.some((t) => t >= min && t <= max);
2582
+ this._lastNodeZoomK = currentZoom;
2583
+ if (!crossed)
2584
+ return;
2585
+ }
2586
+ this.cachedNodeText.length = 0;
2587
+ text = true;
2588
+ }
2589
+ this._lastNodeZoomK = currentZoom;
2484
2590
  for (let i = 0; i < this.nodes.length; i++) {
2485
2591
  const node = this.nodes[i];
2486
- if (types === true || (jsHelpers.isObject(types) && types.options)) {
2592
+ if (options) {
2487
2593
  const nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2488
2594
  const radius = nodeOptions.shape === "circle"
2489
2595
  ? nodeRadiusGetter({
@@ -2534,11 +2640,19 @@ function updateNodeCache(types) {
2534
2640
  nodeOptions.height = node.visible === false ? 0 : height;
2535
2641
  nodeOptions.radius = node.visible === false ? 0 : radius;
2536
2642
  nodeOptions.labelSize = labelSize;
2537
- this.nodeOptionsCache[node.id] = nodeOptions;
2643
+ this.nodeOptionsCache[i] = nodeOptions;
2538
2644
  }
2539
- const nodeOptions = this.nodeOptionsCache[node.id];
2645
+ let nodeOptions = this.nodeOptionsCache[i];
2646
+ if (!nodeOptions)
2647
+ continue;
2540
2648
  /** label */
2541
- if (types === true || (jsHelpers.isObject(types) && types.label)) {
2649
+ if (label) {
2650
+ if (!options) {
2651
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2652
+ const cache = this.nodeOptionsCache[i];
2653
+ if (cache)
2654
+ cache.label = nodeOptions.label;
2655
+ }
2542
2656
  /** label in not text shape */
2543
2657
  if (nodeOptions.shape !== "text" && nodeOptions.label) {
2544
2658
  this.context.font = `${nodeOptions.labelStyle} normal ${nodeOptions.labelWeight} ${nodeOptions.labelSize}px ${nodeOptions.labelFont}`;
@@ -2546,7 +2660,7 @@ function updateNodeCache(types) {
2546
2660
  this.context.textAlign = nodeOptions.labelAlign;
2547
2661
  if (nodeOptions.labelWidth == undefined ||
2548
2662
  this.context.measureText(nodeOptions.label).width <= nodeOptions.labelWidth) {
2549
- this.cachedNodeLabel[node.id] = [nodeOptions.label];
2663
+ this.cachedNodeLabel[i] = [nodeOptions.label];
2550
2664
  }
2551
2665
  const { lines } = getTextLines({
2552
2666
  context: this.context,
@@ -2559,7 +2673,7 @@ function updateNodeCache(types) {
2559
2673
  textStyle: nodeOptions.labelStyle,
2560
2674
  textWeight: nodeOptions.labelWeight,
2561
2675
  });
2562
- this.cachedNodeLabel[node.id] = lines;
2676
+ this.cachedNodeLabel[i] = lines;
2563
2677
  /** label in text shape */
2564
2678
  }
2565
2679
  else if (nodeOptions.shape === "text" && nodeOptions.label) {
@@ -2583,18 +2697,29 @@ function updateNodeCache(types) {
2583
2697
  (lines.length - 1) * nodeOptions.labelGap +
2584
2698
  nodeOptions.labelYPadding;
2585
2699
  nodeOptions.width = maxSize + nodeOptions.labelXPadding;
2586
- this.cachedNodeLabel[node.id] = lines;
2700
+ this.cachedNodeLabel[i] = lines;
2701
+ }
2702
+ else {
2703
+ this.cachedNodeLabel[i] = [];
2587
2704
  }
2588
2705
  }
2589
2706
  /** text */
2590
- if (nodeOptions.text && (types === true || (jsHelpers.isObject(types) && types.text))) {
2707
+ if (text) {
2708
+ if (!options) {
2709
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2710
+ const cache = this.nodeOptionsCache[i];
2711
+ if (cache) {
2712
+ cache.text = nodeOptions.text;
2713
+ cache.textVisible = nodeOptions.textVisible;
2714
+ }
2715
+ }
2716
+ if (!nodeOptions.text) {
2717
+ this.cachedNodeText[i] = [];
2718
+ return;
2719
+ }
2591
2720
  this.context.font = `${nodeOptions.textStyle} normal ${nodeOptions.textWeight} ${nodeOptions.textSize}px ${nodeOptions.textFont}`;
2592
2721
  this.context.fillStyle = nodeOptions.textColor;
2593
2722
  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
2723
  const { lines } = getTextLines({
2599
2724
  context: this.context,
2600
2725
  maxWidth: nodeOptions.textWidth,
@@ -2606,49 +2731,233 @@ function updateNodeCache(types) {
2606
2731
  textStyle: nodeOptions.textStyle,
2607
2732
  textWeight: nodeOptions.textWeight,
2608
2733
  });
2609
- this.cachedNodeText[node.id] = lines;
2734
+ this.cachedNodeText[i] = lines;
2610
2735
  }
2611
2736
  }
2612
2737
  }
2613
2738
 
2739
+ const DEFAULT_ZOOM_EXTENT = [0.3, 10];
2740
+ const DEFAULT_TRANSLATE_EXTENT_COEFFICIENT = 8;
2741
+ const DEFAULT_MARGIN = 0.25;
2614
2742
  function initZoom(currentZoom) {
2615
2743
  if (!this.area)
2616
2744
  throw new Error("bad init data");
2745
+ const bounds = computeGraphBounds(this.nodes);
2746
+ const zoomExtent = resolveZoomExtent(this.graphSettings.zoomExtent, this.width, this.height, this.graphSettings.zoomExtentMargin ?? DEFAULT_MARGIN, bounds);
2747
+ const translateExtent = resolveTranslateExtent(this.graphSettings.translateExtent, this.graphSettings.translateExtentCoefficient, this.graphSettings.translateExtentOverlap, this.width, this.height, zoomExtent, bounds);
2748
+ this._translateExtent = translateExtent;
2617
2749
  const zoomInstance = d3Zoom.zoom()
2618
- .scaleExtent(this.graphSettings.zoomExtent)
2750
+ .scaleExtent(zoomExtent)
2751
+ .filter((event) => {
2752
+ return !(event instanceof MouseEvent && event.shiftKey);
2753
+ })
2619
2754
  .on("zoom", (event) => {
2755
+ if (this._zoomAnimating)
2756
+ return;
2620
2757
  this.listeners.onZoom?.call?.(this, event);
2621
2758
  const oldTransform = this.areaTransform;
2622
2759
  this.areaTransform = event.transform;
2623
2760
  if (this.areaTransform.k !== oldTransform.k) {
2624
2761
  updateLinkCache.call(this);
2625
- updateNodeCache.call(this, true);
2762
+ updateNodeCache.call(this);
2626
2763
  }
2627
- if (!this.simulationWorking && !this.highlightWorking)
2628
- requestAnimationFrame(() => this.draw());
2764
+ this.tick();
2629
2765
  });
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
- }
2766
+ zoomInstance.translateExtent(translateExtent);
2641
2767
  d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2642
2768
  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);
2769
+ this.areaTransform = new d3Zoom.ZoomTransform(Math.max(zoomInitial?.k ?? 1, zoomExtent[0]), zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2644
2770
  d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
2645
2771
  }
2772
+ function resolveZoomExtent(userExtent, viewWidth, viewHeight, margin, bounds) {
2773
+ if (userExtent &&
2774
+ userExtent.length === 2 &&
2775
+ userExtent[0] != undefined &&
2776
+ userExtent[1] != undefined) {
2777
+ return userExtent;
2778
+ }
2779
+ if (!bounds)
2780
+ return DEFAULT_ZOOM_EXTENT;
2781
+ const graphWidth = bounds.maxX - bounds.minX;
2782
+ const graphHeight = bounds.maxY - bounds.minY;
2783
+ if (graphWidth === 0 || graphHeight === 0)
2784
+ return DEFAULT_ZOOM_EXTENT;
2785
+ const scaleToFit = (1 - margin) / Math.max(graphWidth / viewWidth, graphHeight / viewHeight);
2786
+ return [scaleToFit, DEFAULT_ZOOM_EXTENT[1]];
2787
+ }
2788
+ function resolveTranslateExtent(userExtent, userCoefficient, overlap, viewWidth, viewHeight, zoomExtent, bounds) {
2789
+ if (userExtent && userExtent.length >= 2 && jsHelpers.isArray(userExtent[0]) && jsHelpers.isArray(userExtent[1])) {
2790
+ return [
2791
+ [
2792
+ userExtent[0][0] ?? -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2793
+ userExtent[0][1] ?? -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2794
+ ],
2795
+ [
2796
+ userExtent[1][0] ?? viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2797
+ userExtent[1][1] ?? viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2798
+ ],
2799
+ ];
2800
+ }
2801
+ if (userCoefficient) {
2802
+ const [cx, cy] = jsHelpers.isArray(userCoefficient)
2803
+ ? userCoefficient
2804
+ : [userCoefficient, userCoefficient];
2805
+ return [
2806
+ [-viewWidth * cx, -viewHeight * cy],
2807
+ [viewWidth * cx, viewHeight * cy],
2808
+ ];
2809
+ }
2810
+ if (!bounds) {
2811
+ return [
2812
+ [
2813
+ -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2814
+ -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2815
+ ],
2816
+ [
2817
+ viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2818
+ viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2819
+ ],
2820
+ ];
2821
+ }
2822
+ const minZoom = zoomExtent[0];
2823
+ const zoomViewWidth = viewWidth / minZoom;
2824
+ const zoomViewHeight = viewHeight / minZoom;
2825
+ const graphWidth = bounds.maxX - bounds.minX;
2826
+ const graphHeight = bounds.maxY - bounds.minY;
2827
+ const minX = bounds.minX + overlap * graphWidth - zoomViewWidth;
2828
+ const maxX = bounds.maxX - overlap * graphWidth + zoomViewWidth;
2829
+ const minY = bounds.minY + overlap * graphHeight - zoomViewHeight;
2830
+ const maxY = bounds.maxY - overlap * graphHeight + zoomViewHeight;
2831
+ return [
2832
+ [minX, minY],
2833
+ [maxX, maxY],
2834
+ ];
2835
+ }
2836
+
2837
+ function initSimulation() {
2838
+ if (!this.simulation) {
2839
+ this.simulation = d3Force.forceSimulation()
2840
+ .nodes(this.nodes)
2841
+ .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2842
+ .on("tick", () => {
2843
+ this.draw();
2844
+ })
2845
+ .on("end", () => {
2846
+ this.listeners.onSimulationEnd?.call?.(this);
2847
+ if (this.area) {
2848
+ initZoom.call(this, this.areaTransform);
2849
+ }
2850
+ if (this.graphSettings.showDrawTime) {
2851
+ getDrawTime();
2852
+ // eslint-disable-next-line no-console
2853
+ console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2854
+ resetDrawTime();
2855
+ }
2856
+ })
2857
+ .stop();
2858
+ initSimulationForces.call(this);
2859
+ }
2860
+ }
2861
+ function initSimulationForces() {
2862
+ if (!this.simulation)
2863
+ return;
2864
+ const linkForce = this.simulation.force("link");
2865
+ if (!linkForce)
2866
+ return;
2867
+ if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2868
+ this.simulation.force("x", null);
2869
+ }
2870
+ if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2871
+ this.simulation.force("y", null);
2872
+ }
2873
+ if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2874
+ this.simulation.force("charge")) {
2875
+ this.simulation.force("charge", null);
2876
+ }
2877
+ if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2878
+ this.simulation.force("center")) {
2879
+ this.simulation.force("center", null);
2880
+ }
2881
+ if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2882
+ linkForce.distance(0).strength(0).iterations(0);
2883
+ }
2884
+ if (this.forceSettings.forces && this.forceSettings.linkForce) {
2885
+ linkForce
2886
+ .distance(this.forceSettings.linkDistance)
2887
+ .strength(this.forceSettings.linkStrength)
2888
+ .iterations(this.forceSettings.linkIterations);
2889
+ }
2890
+ if (this.forceSettings.forces && this.forceSettings.xForce) {
2891
+ this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2892
+ }
2893
+ if (this.forceSettings.forces && this.forceSettings.yForce) {
2894
+ this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2895
+ }
2896
+ if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2897
+ this.simulation.force("charge", d3Force.forceManyBody()
2898
+ .strength(this.forceSettings.chargeStrength)
2899
+ .distanceMax(Infinity)
2900
+ .distanceMin(this.forceSettings.chargeDistanceMin));
2901
+ }
2902
+ if (this.forceSettings.forces && this.forceSettings.centerForce) {
2903
+ this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2904
+ }
2905
+ initCollideForce.call(this, true);
2906
+ }
2907
+ function initCollideForce(forceUpdate) {
2908
+ if (!this.simulation)
2909
+ return;
2910
+ if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2911
+ this.simulation.force("collide")) {
2912
+ this.simulation.force("collide", null);
2913
+ }
2914
+ if (this.forceSettings.forces && this.forceSettings.collideForce) {
2915
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2916
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2917
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2918
+ if (isMaxCollideNodes && isMaxCollideLinks) {
2919
+ this.simulation.force("collide", null);
2920
+ }
2921
+ else if (!this.simulation.force("collide") || forceUpdate) {
2922
+ this.simulation.force("collide", d3Force.forceCollide()
2923
+ .radius((node, index) => {
2924
+ const nodeOptions = this.nodeOptionsCache[index];
2925
+ if (!nodeOptions)
2926
+ return 0;
2927
+ switch (nodeOptions.shape) {
2928
+ case "circle": {
2929
+ if (this.forceSettings.collideRadius) {
2930
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2931
+ }
2932
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2933
+ }
2934
+ case "square": {
2935
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2936
+ this.forceSettings.collideAdditionalRadius);
2937
+ }
2938
+ case "text": {
2939
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2940
+ this.forceSettings.collideAdditionalRadius);
2941
+ }
2942
+ default: {
2943
+ if (this.forceSettings.collideRadius) {
2944
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2945
+ }
2946
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2947
+ }
2948
+ }
2949
+ })
2950
+ .strength(this.forceSettings.collideStrength)
2951
+ .iterations(this.forceSettings.collideIterations));
2952
+ }
2953
+ }
2954
+ }
2646
2955
 
2647
2956
  class GraphCanvas {
2648
2957
  /** initial data */
2649
2958
  nodes;
2650
2959
  links;
2651
- particles = {};
2960
+ particles = [];
2652
2961
  width;
2653
2962
  height;
2654
2963
  root;
@@ -2665,19 +2974,30 @@ class GraphCanvas {
2665
2974
  context;
2666
2975
  simulation;
2667
2976
  areaTransform = d3Zoom.zoomIdentity;
2668
- areaRect;
2977
+ _translateExtent;
2669
2978
  draw;
2670
2979
  eventAbortController;
2671
- cachedNodeText = {};
2672
- cachedNodeLabel = {};
2673
- linkOptionsCache = {};
2674
- nodeOptionsCache = {};
2980
+ cachedNodeText = [];
2981
+ cachedNodeLabel = [];
2982
+ linkOptionsCache = [];
2983
+ nodeOptionsCache = [];
2675
2984
  isDragging = false;
2676
2985
  highlightedNode = null;
2677
2986
  highlightedLink = null;
2678
2987
  highlightedNeighbors = null;
2679
2988
  highlightProgress = 1;
2680
- highlightWorking = false;
2989
+ highlightStart = null;
2990
+ highlightPositive = false;
2991
+ highlightController;
2992
+ _lastNodeZoomK;
2993
+ _lastLinkZoomK;
2994
+ _zoomAnimating = false;
2995
+ isSelecting = false;
2996
+ selectionRect = null;
2997
+ areaRect;
2998
+ // protected get areaRect() {
2999
+ // return this.area?.getBoundingClientRect();
3000
+ // }
2681
3001
  get simulationWorking() {
2682
3002
  const simulationAlpha = this.simulation?.alpha?.() ?? 0;
2683
3003
  const simulationAlphaMin = this.simulation?.alphaMin?.() ?? 0;
@@ -2706,21 +3026,72 @@ class GraphCanvas {
2706
3026
  get dpi() {
2707
3027
  return devicePixelRatio;
2708
3028
  }
2709
- getData() {
3029
+ getData = () => {
2710
3030
  return {
2711
3031
  links: this.links,
2712
3032
  nodes: this.nodes,
2713
3033
  };
2714
- }
2715
- changeData(options, alpha = 0.5, clearCache = true) {
3034
+ };
3035
+ fitToView = (margin = this.graphSettings.zoomToFitMargin, duration = this.graphSettings.zoomAnimationDuration) => {
3036
+ const area = this.area;
3037
+ if (!area)
3038
+ return;
3039
+ const bounds = computeGraphBounds(this.nodes);
3040
+ if (!bounds)
3041
+ return;
3042
+ const graphWidth = bounds.maxX - bounds.minX;
3043
+ const graphHeight = bounds.maxY - bounds.minY;
3044
+ if (graphWidth === 0 || graphHeight === 0)
3045
+ return;
3046
+ const graphCenterX = bounds.minX + graphWidth / 2;
3047
+ const graphCenterY = bounds.minY + graphHeight / 2;
3048
+ const scale = (1 - margin) / Math.max(graphWidth / this.width, graphHeight / this.height);
3049
+ const clampedScale = Math.min(scale, this.graphSettings.zoomExtent?.[1] ?? scale);
3050
+ const target = d3Zoom.zoomIdentity
3051
+ .translate(this.width / 2, this.height / 2)
3052
+ .scale(clampedScale)
3053
+ .translate(-graphCenterX, -graphCenterY);
3054
+ if (!this.graphSettings.zoomAnimation) {
3055
+ this.areaTransform = target;
3056
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
3057
+ this.clearCache(true);
3058
+ this.tick();
3059
+ return;
3060
+ }
3061
+ this.animateZoom(area, target, this.areaTransform, duration);
3062
+ };
3063
+ focusOnNode = (nodeId, scale = this.graphSettings.zoomToNodeScale, duration = this.graphSettings.zoomAnimationDuration) => {
3064
+ const area = this.area;
3065
+ if (!area)
3066
+ return;
3067
+ const node = this.nodes.find((n) => n.id === nodeId);
3068
+ if (!node)
3069
+ return;
3070
+ if (node.x == undefined || node.y == undefined)
3071
+ return;
3072
+ const target = d3Zoom.zoomIdentity
3073
+ .translate(this.width / 2, this.height / 2)
3074
+ .scale(scale)
3075
+ .translate(-node.x, -node.y);
3076
+ if (!this.graphSettings.zoomAnimation) {
3077
+ this.areaTransform = target;
3078
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
3079
+ this.clearCache(true);
3080
+ this.tick();
3081
+ return;
3082
+ }
3083
+ this.animateZoom(area, target, this.areaTransform, duration);
3084
+ };
3085
+ changeData = (options, alpha = 0.5, clearCache = true, precompute = false) => {
2716
3086
  if (options.links != undefined)
2717
3087
  this.links = options.links;
2718
3088
  if (options.nodes != undefined)
2719
3089
  this.nodes = options.nodes;
2720
- if (options.nodes != undefined || options.links != undefined)
2721
- this.updateData(alpha, clearCache);
2722
- }
2723
- changeSettings(options, clearCache = true) {
3090
+ if (options.nodes != undefined || options.links != undefined) {
3091
+ this.updateData(alpha, clearCache, precompute);
3092
+ }
3093
+ };
3094
+ changeSettings = (options, clearCache = true, precompute = false) => {
2724
3095
  if (options.graphSettings) {
2725
3096
  this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
2726
3097
  this.draw = initDraw.call(this);
@@ -2767,16 +3138,16 @@ class GraphCanvas {
2767
3138
  initCollideForce.call(this, true);
2768
3139
  }
2769
3140
  if (options.forceSettings) {
2770
- return void this.updateSimulation();
3141
+ return void this.updateSimulation(precompute);
2771
3142
  }
2772
3143
  this.tick();
2773
- }
2774
- updateRect() {
3144
+ };
3145
+ updateRect = () => {
2775
3146
  if (!this.area)
2776
3147
  return;
2777
3148
  this.areaRect = this.area.getBoundingClientRect();
2778
- }
2779
- updateSize() {
3149
+ };
3150
+ updateSize = () => {
2780
3151
  if (!this.area)
2781
3152
  return;
2782
3153
  const { width, height } = this.root.getBoundingClientRect();
@@ -2784,57 +3155,102 @@ class GraphCanvas {
2784
3155
  this.height = height;
2785
3156
  this.area.width = this.dpi * this.width;
2786
3157
  this.area.height = this.dpi * this.height;
2787
- this.areaRect = this.area.getBoundingClientRect();
3158
+ this.updateRect();
2788
3159
  this.context = this.area.getContext("2d");
2789
3160
  if (!this.context)
2790
3161
  throw new Error("couldn't create canvas context");
2791
3162
  this.context.scale(this.dpi, this.dpi);
2792
3163
  this.draw();
2793
- }
2794
- clearCache(keys) {
3164
+ };
3165
+ clearCache = (keys) => {
2795
3166
  if (keys === true) {
2796
- updateNodeCache.call(this, true);
2797
- updateLinkCache.call(this);
3167
+ this.nodeOptionsCache.length = 0;
3168
+ this.linkOptionsCache.length = 0;
3169
+ this.cachedNodeLabel.length = 0;
3170
+ this.cachedNodeText.length = 0;
2798
3171
  }
2799
3172
  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);
3173
+ for (const key of keys) {
3174
+ switch (key) {
3175
+ case GRAPH_CACHE_TYPE.NodeText: {
3176
+ this.cachedNodeText.length = 0;
3177
+ break;
3178
+ }
3179
+ case GRAPH_CACHE_TYPE.NodeLabel: {
3180
+ this.cachedNodeLabel.length = 0;
3181
+ break;
3182
+ }
3183
+ case GRAPH_CACHE_TYPE.NodeOptions: {
3184
+ this.nodeOptionsCache.length = 0;
3185
+ break;
3186
+ }
3187
+ case GRAPH_CACHE_TYPE.LinkOptions: {
3188
+ this.linkOptionsCache.length = 0;
3189
+ break;
3190
+ }
3191
+ }
2811
3192
  }
2812
3193
  }
2813
- }
2814
- tick() {
2815
- if (!this.simulationWorking && !this.highlightWorking)
3194
+ updateNodeCache.call(this);
3195
+ updateLinkCache.call(this);
3196
+ };
3197
+ tick = () => {
3198
+ if (!this.simulationWorking)
2816
3199
  this.draw();
2817
- }
2818
- restart(alpha) {
2819
- if (this.simulation)
3200
+ };
3201
+ restart = (alpha, options) => {
3202
+ if (!this.simulation)
3203
+ return;
3204
+ const settings = {
3205
+ precompute: options?.precompute ?? this.forceSettings.precompute,
3206
+ precomputeMaxTimeMs: options?.precomputeMaxTimeMs ?? this.forceSettings?.precomputeMaxTimeMs,
3207
+ precomputeMaxTicks: options?.precomputeMaxTicks ?? this.forceSettings?.precomputeMaxTicks ?? 300,
3208
+ precomputeDisableForcesAfter: options?.precomputeDisableForcesAfter ??
3209
+ this.forceSettings?.precomputeDisableForcesAfter ??
3210
+ false,
3211
+ };
3212
+ if (!settings.precompute) {
2820
3213
  this.simulation.alpha(alpha ?? 1).restart();
2821
- }
2822
- start() {
3214
+ return;
3215
+ }
3216
+ if (settings.precomputeDisableForcesAfter) {
3217
+ this.forceSettings = forceSettingsGetter({ forces: true }, this.forceSettings);
3218
+ }
3219
+ initSimulationForces.call(this);
3220
+ this.simulation.stop();
3221
+ this.simulation.alpha(alpha ?? 1);
3222
+ const startTime = performance.now();
3223
+ let ticks = 0;
3224
+ while (performance.now() - startTime < settings.precomputeMaxTimeMs &&
3225
+ ticks < settings.precomputeMaxTicks) {
3226
+ this.simulation.tick(1);
3227
+ ticks++;
3228
+ if (this.simulation.alpha() <= this.simulation.alphaMin())
3229
+ break;
3230
+ }
3231
+ if (settings.precomputeDisableForcesAfter) {
3232
+ this.forceSettings = forceSettingsGetter({ forces: false }, this.forceSettings);
3233
+ }
3234
+ initSimulationForces.call(this);
3235
+ this.simulation.restart();
3236
+ this.tick();
3237
+ };
3238
+ start = () => {
2823
3239
  if (this.simulation)
2824
3240
  this.simulation.alpha(1).restart();
2825
3241
  if (this.container)
2826
3242
  this.container.style.display = "block";
2827
- }
2828
- stop() {
3243
+ };
3244
+ stop = () => {
2829
3245
  if (this.simulation)
2830
3246
  this.simulation.stop();
2831
3247
  if (this.container)
2832
3248
  this.container.style.display = "none";
2833
- }
2834
- create() {
3249
+ };
3250
+ create = () => {
2835
3251
  this.init();
2836
- }
2837
- destroy() {
3252
+ };
3253
+ destroy = () => {
2838
3254
  if (this.simulation) {
2839
3255
  this.simulation.stop();
2840
3256
  this.simulation = undefined;
@@ -2842,39 +3258,79 @@ class GraphCanvas {
2842
3258
  this.clearHTMLElements();
2843
3259
  this.clearState();
2844
3260
  this.clearCache(true);
2845
- }
2846
- clearHTMLElements() {
3261
+ };
3262
+ getPointerAreaPosition = (event) => {
3263
+ let localX;
3264
+ let localY;
3265
+ if ("offsetX" in event) {
3266
+ // localX = event.offsetX;
3267
+ // localY = event.offsetY;
3268
+ const rect = this.areaRect;
3269
+ if (!rect)
3270
+ return [0, 0];
3271
+ localX = event.clientX - rect.left;
3272
+ localY = event.clientY - rect.top;
3273
+ }
3274
+ else {
3275
+ const rect = this.areaRect;
3276
+ if (!rect)
3277
+ return [0, 0];
3278
+ const clientX = event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX;
3279
+ const clientY = event.touches[0]?.clientY ?? event.changedTouches[0]?.clientY;
3280
+ localX = clientX - rect.left;
3281
+ localY = clientY - rect.top;
3282
+ }
3283
+ const px = (localX - this.areaTransform.x) / this.areaTransform.k;
3284
+ const py = (localY - this.areaTransform.y) / this.areaTransform.k;
3285
+ return [px, py];
3286
+ };
3287
+ clearHTMLElements = () => {
2847
3288
  this.root.replaceChildren();
2848
3289
  this.area = undefined;
2849
3290
  this.context = undefined;
2850
3291
  this.container = undefined;
2851
3292
  this.eventAbortController.abort();
2852
3293
  this.eventAbortController = new AbortController();
2853
- }
2854
- updateSimulation() {
3294
+ };
3295
+ updateSimulation = (precompute = false) => {
2855
3296
  if (this.simulation) {
2856
3297
  initSimulationForces.call(this);
2857
- this.simulation.alpha(1);
2858
- this.simulation.restart();
3298
+ this.restart(1, { precompute });
2859
3299
  }
2860
- }
2861
- clearState() {
3300
+ };
3301
+ clearState = () => {
2862
3302
  this.isDragging = false;
2863
3303
  this.highlightedNode = null;
2864
3304
  this.highlightedLink = null;
2865
3305
  this.highlightedNeighbors = null;
2866
3306
  this.highlightProgress = 0;
2867
- this.highlightWorking = false;
2868
- }
2869
- updateData(alpha = 0.5, clearCache = true) {
3307
+ this.highlightStart = null;
3308
+ this.highlightPositive = false;
3309
+ this.isSelecting = false;
3310
+ this.selectionRect = null;
3311
+ };
3312
+ init = () => {
3313
+ initArea.call(this);
3314
+ updateNodeCache.call(this);
3315
+ updateLinkCache.call(this);
3316
+ initSimulation.call(this);
3317
+ this.restart(1);
3318
+ initDnd.call(this);
3319
+ initZoom.call(this);
3320
+ initResize.call(this);
3321
+ initSelection.call(this);
3322
+ initPointer.call(this);
3323
+ updateNodeCache.call(this);
3324
+ updateLinkCache.call(this);
3325
+ this.tick();
3326
+ };
3327
+ updateData = (alpha = 0.5, clearCache = true, precompute = false) => {
2870
3328
  if (clearCache) {
2871
3329
  this.clearCache(clearCache);
2872
3330
  }
2873
3331
  if (this.simulation) {
2874
3332
  initCollideForce.call(this, false);
2875
- this.simulation
2876
- .nodes(this.nodes)
2877
- .force("link", d3Force.forceLink(this.links)
3333
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2878
3334
  .id(this.nodeSettings.idGetter.bind(this))
2879
3335
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2880
3336
  ? this.forceSettings.linkDistance
@@ -2884,21 +3340,95 @@ class GraphCanvas {
2884
3340
  : 0)
2885
3341
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2886
3342
  ? this.forceSettings.linkIterations
2887
- : 0))
2888
- .alpha(alpha)
2889
- .restart();
3343
+ : 0));
3344
+ this.restart(alpha, { precompute });
3345
+ initZoom.call(this, this.areaTransform);
2890
3346
  }
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
- }
3347
+ };
3348
+ animateHighlight = (node, link, baseDuration = this.highlightSettings.highlightDuration) => {
3349
+ let positive = true;
3350
+ if (node && (this.highlightedNode !== node || !this.highlightPositive)) {
3351
+ this.highlightedNode = node;
3352
+ this.highlightedNeighbors = new Set(node.neighbors);
3353
+ this.particles = [];
3354
+ this.highlightedLink = null;
3355
+ this.highlightStart = performance.now();
3356
+ }
3357
+ else if (link && (this.highlightedLink !== link || !this.highlightPositive)) {
3358
+ const { sourceId, targetId } = extractLinkPointIds(link);
3359
+ this.highlightProgress = 0;
3360
+ this.highlightedLink = link;
3361
+ this.highlightedNeighbors = new Set([sourceId, targetId]);
3362
+ this.particles = [];
3363
+ this.highlightedNode = null;
3364
+ this.highlightStart = performance.now();
3365
+ }
3366
+ else if (!node && !link && this.highlightPositive) {
3367
+ positive = false;
3368
+ }
3369
+ else {
3370
+ return;
3371
+ }
3372
+ if (this.highlightController) {
3373
+ this.highlightController.abort();
3374
+ }
3375
+ const controller = new AbortController();
3376
+ this.highlightPositive = positive;
3377
+ this.highlightController = controller;
3378
+ const startTime = performance.now();
3379
+ const startProgress = this.highlightProgress;
3380
+ const targetProgress = positive ? 1 : 0;
3381
+ const delta = targetProgress - startProgress;
3382
+ const duration = baseDuration * Math.abs(delta);
3383
+ const animate = () => {
3384
+ if (controller.signal.aborted)
3385
+ return;
3386
+ const elapsed = performance.now() - startTime;
3387
+ const t = duration === 0 ? 1 : Math.min(elapsed / duration, 1);
3388
+ const current = startProgress + delta * t;
3389
+ const eased = current < 0.5 ? 4 * current * current * current : 1 - (-2 * current + 2) ** 3 / 2;
3390
+ this.highlightProgress = eased;
3391
+ if (t < 1 || positive) {
3392
+ requestAnimationFrame(animate);
3393
+ this.draw();
3394
+ }
3395
+ else {
3396
+ this.highlightedNode = null;
3397
+ this.highlightedLink = null;
3398
+ this.highlightedNeighbors = null;
3399
+ this.highlightStart = null;
3400
+ this.particles = [];
3401
+ this.tick();
3402
+ }
3403
+ };
3404
+ requestAnimationFrame(animate);
3405
+ };
3406
+ animateZoom = (area, target, start, duration) => {
3407
+ this._zoomAnimating = true;
3408
+ const startTime = performance.now();
3409
+ const animate = () => {
3410
+ const elapsed = performance.now() - startTime;
3411
+ const t = Math.min(elapsed / duration, 1);
3412
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
3413
+ const x = start.x + (target.x - start.x) * eased;
3414
+ const y = start.y + (target.y - start.y) * eased;
3415
+ const k = start.k + (target.k - start.k) * eased;
3416
+ this.areaTransform = new d3Zoom.ZoomTransform(k, x, y);
3417
+ d3Zoom.zoom().transform(d3Selection.select(area), this.areaTransform);
3418
+ updateLinkCache.call(this);
3419
+ updateNodeCache.call(this);
3420
+ this.tick();
3421
+ if (t < 1) {
3422
+ requestAnimationFrame(animate);
3423
+ }
3424
+ else {
3425
+ this._zoomAnimating = false;
3426
+ this.clearCache(true);
3427
+ this.tick();
3428
+ }
3429
+ };
3430
+ requestAnimationFrame(animate);
3431
+ };
2902
3432
  }
2903
3433
 
2904
3434
  const FORCE_CONTROLS = [
@@ -3059,22 +3589,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
3059
3589
  label: "Расширение границы связи для курсора",
3060
3590
  },
3061
3591
  {
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,
3592
+ id: "highlightDuration",
3593
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3594
+ max: 1000,
3595
+ min: 10,
3075
3596
  step: 1,
3076
3597
  type: "range",
3077
- label: "Скорость применения анимации в кадрах",
3598
+ label: "Скорость анимации",
3078
3599
  },
3079
3600
  ];
3080
3601
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3547,19 +4068,22 @@ const LINK_SETTINGS_CONTROLS = [
3547
4068
  initialValue: LINK_SETTINGS.particles,
3548
4069
  },
3549
4070
  {
3550
- id: "particleFlexSpeed",
3551
- type: "checkbox",
3552
- label: "Гибкий расчет скорости частицы",
3553
- initialValue: LINK_SETTINGS.particleFlexSpeed,
4071
+ id: "particleSpeedByDistance",
4072
+ initialValue: LINK_SETTINGS.particleSpeedByDistance,
4073
+ max: 500,
4074
+ min: 1,
4075
+ step: 1,
4076
+ type: "range",
4077
+ label: "Скорость частицы на один пиксель",
3554
4078
  },
3555
4079
  {
3556
- id: "particleFlexSpeedCoefficient",
3557
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3558
- max: 10,
3559
- min: 0.1,
3560
- step: 0.1,
4080
+ id: "particleCountByDistance",
4081
+ initialValue: LINK_SETTINGS.particleCountByDistance,
4082
+ max: 250,
4083
+ min: 1,
4084
+ step: 1,
3561
4085
  type: "range",
3562
- label: "Коэффициент скорости частицы относительно расстояния",
4086
+ label: "Количество пикселей для появления частицы",
3563
4087
  },
3564
4088
  ];
3565
4089
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3654,24 +4178,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3654
4178
  label: "Радиус",
3655
4179
  initialValue: LINK_OPTIONS.particleRadius,
3656
4180
  },
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
4181
  {
3676
4182
  id: "particleBorderColor",
3677
4183
  type: "color",
@@ -4105,10 +4611,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
4105
4611
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
4106
4612
  exports.NODE_SETTINGS = NODE_SETTINGS;
4107
4613
  exports.NODE_SETTINGS_CONTROLS = NODE_SETTINGS_CONTROLS;
4614
+ exports.PERFOMANCE_NODE_OPTIONS = PERFOMANCE_NODE_OPTIONS;
4615
+ exports.PERFORMANCE_FORCE_SETTINGS = PERFORMANCE_FORCE_SETTINGS;
4616
+ exports.PERFORMANCE_GRAPH_SETTINGS = PERFORMANCE_GRAPH_SETTINGS;
4617
+ exports.PERFORMANCE_HIGHLIGHT_SETTINGS = PERFORMANCE_HIGHLIGHT_SETTINGS;
4618
+ exports.PERFORMANCE_LINK_OPTIONS = PERFORMANCE_LINK_OPTIONS;
4619
+ exports.PERFORMANCE_LINK_SETTINGS = PERFORMANCE_LINK_SETTINGS;
4620
+ exports.PERFORMANCE_NODE_SETTINGS = PERFORMANCE_NODE_SETTINGS;
4108
4621
  exports.animationByProgress = animationByProgress;
4622
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
4109
4623
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
4110
4624
  exports.colorGetter = colorGetter;
4111
4625
  exports.colorToRgb = colorToRgb;
4626
+ exports.computeGraphBounds = computeGraphBounds;
4112
4627
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
4113
4628
  exports.drawText = drawText;
4114
4629
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -4120,6 +4635,7 @@ exports.getDrawNode = getDrawNode;
4120
4635
  exports.getParticlePosition = getParticlePosition;
4121
4636
  exports.graphSettingsGetter = graphSettingsGetter;
4122
4637
  exports.highlightSettingsGetter = highlightSettingsGetter;
4638
+ exports.isEmptyObject = isEmptyObject;
4123
4639
  exports.isNodeVisible = isNodeVisible;
4124
4640
  exports.isOverlapsNode = isOverlapsNode;
4125
4641
  exports.linkByPointerGetter = linkByPointerGetter;