@krainovsd/graph 0.13.0-beta3 → 0.14.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/lib/cjs/index.cjs +729 -426
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/esm/constants/force-controls.js +4 -2
  4. package/lib/esm/constants/force-controls.js.map +1 -1
  5. package/lib/esm/constants/highlight-controls.js +9 -16
  6. package/lib/esm/constants/highlight-controls.js.map +1 -1
  7. package/lib/esm/constants/link-controls.js +17 -30
  8. package/lib/esm/constants/link-controls.js.map +1 -1
  9. package/lib/esm/constants/node-controls.js +4 -2
  10. package/lib/esm/constants/node-controls.js.map +1 -1
  11. package/lib/esm/index.js +8 -7
  12. package/lib/esm/index.js.map +1 -1
  13. package/lib/esm/module/GraphCanvas/GraphCanvas.js +222 -34
  14. package/lib/esm/module/GraphCanvas/GraphCanvas.js.map +1 -1
  15. package/lib/esm/module/GraphCanvas/constants/force-settings.js +11 -1
  16. package/lib/esm/module/GraphCanvas/constants/force-settings.js.map +1 -1
  17. package/lib/esm/module/GraphCanvas/constants/graph-settings.js +17 -5
  18. package/lib/esm/module/GraphCanvas/constants/graph-settings.js.map +1 -1
  19. package/lib/esm/module/GraphCanvas/constants/highlight-settings.js +5 -3
  20. package/lib/esm/module/GraphCanvas/constants/highlight-settings.js.map +1 -1
  21. package/lib/esm/module/GraphCanvas/constants/index.js +5 -2
  22. package/lib/esm/module/GraphCanvas/constants/index.js.map +1 -1
  23. package/lib/esm/module/GraphCanvas/constants/link-settings.js +19 -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/slices/draw-links.js +47 -62
  50. package/lib/esm/module/GraphCanvas/slices/draw-links.js.map +1 -1
  51. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js +9 -4
  52. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js.map +1 -1
  53. package/lib/esm/module/GraphCanvas/slices/init-dnd.js +21 -4
  54. package/lib/esm/module/GraphCanvas/slices/init-dnd.js.map +1 -1
  55. package/lib/esm/module/GraphCanvas/slices/init-draw.js +1 -52
  56. package/lib/esm/module/GraphCanvas/slices/init-draw.js.map +1 -1
  57. package/lib/esm/module/GraphCanvas/slices/init-pointer.js +6 -34
  58. package/lib/esm/module/GraphCanvas/slices/init-pointer.js.map +1 -1
  59. package/lib/esm/module/GraphCanvas/slices/init-simulation.js +12 -2
  60. package/lib/esm/module/GraphCanvas/slices/init-simulation.js.map +1 -1
  61. package/lib/esm/module/GraphCanvas/slices/init-zoom.js +85 -16
  62. package/lib/esm/module/GraphCanvas/slices/init-zoom.js.map +1 -1
  63. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js +33 -3
  64. package/lib/esm/module/GraphCanvas/slices/update-link-cache.js.map +1 -1
  65. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js +61 -16
  66. package/lib/esm/module/GraphCanvas/slices/update-node-cache.js.map +1 -1
  67. package/lib/index.d.ts +68 -29
  68. package/package.json +3 -2
package/lib/cjs/index.cjs CHANGED
@@ -4,11 +4,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
5
  const jsHelpers = require('@krainovsd/js-helpers');
6
6
  const d3Force = require('d3-force');
7
+ const d3Selection = require('d3-selection');
7
8
  const d3Zoom = require('d3-zoom');
8
9
  const d3Array = require('d3-array');
9
- const d3Selection = require('d3-selection');
10
10
  const d3Drag = require('d3-drag');
11
- const lodash = require('lodash');
12
11
 
13
12
  const COMMON_SETTINGS = {
14
13
  nodeRadius: 5,
@@ -23,6 +22,10 @@ const FORCE_SETTINGS = {
23
22
  xForce: true,
24
23
  yForce: true,
25
24
  collideForce: true,
25
+ precompute: true,
26
+ precomputeMaxTicks: 300,
27
+ precomputeMaxTimeMs: 200,
28
+ precomputeDisableForcesAfter: false,
26
29
  centerPosition: {},
27
30
  centerStrength: 1,
28
31
  collideStrength: 0.1,
@@ -40,6 +43,12 @@ const FORCE_SETTINGS = {
40
43
  linkStrength: 1,
41
44
  collideRadius: null,
42
45
  };
46
+ const PERFORMANCE_FORCE_SETTINGS = {
47
+ ...FORCE_SETTINGS,
48
+ collideIterations: 1,
49
+ precomputeMaxTimeMs: 350,
50
+ precomputeDisableForcesAfter: true,
51
+ };
43
52
 
44
53
  function forceSettingsGetter(settings, prevSettings) {
45
54
  return {
@@ -59,17 +68,18 @@ function linkSettingsGetter(settings, prevSettings) {
59
68
  return { ...(prevSettings ?? LINK_SETTINGS), ...settings };
60
69
  }
61
70
  function linkOptionsGetter() {
71
+ const aboveScale = this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch;
62
72
  return {
63
73
  ...LINK_OPTIONS,
64
74
  drawExtraLink: null,
65
75
  drawLink: null,
66
- color: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
76
+ color: aboveScale
67
77
  ? this.linkSettings.linkColorAfterScaleSwitch
68
78
  : this.linkSettings.linkColorBeforeScaleSwitch,
69
- arrowColor: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
79
+ arrowColor: aboveScale
70
80
  ? this.linkSettings.linkColorAfterScaleSwitch
71
81
  : this.linkSettings.linkColorBeforeScaleSwitch,
72
- width: this.areaTransform && this.areaTransform.k > this.linkSettings.linkScaleSwitch
82
+ width: aboveScale
73
83
  ? this.linkSettings.linkWidthAfterScaleSwitch
74
84
  : this.linkSettings.linkWidthBeforeScaleSwitch,
75
85
  };
@@ -185,11 +195,20 @@ function resetDrawTime() {
185
195
  }
186
196
 
187
197
  function nodeSettingsGetter(settings, prevNodeSettings) {
188
- return {
198
+ const result = {
189
199
  ...(prevNodeSettings ?? NODE_SETTINGS),
190
200
  idGetter: nodeIdGetter,
191
201
  ...settings,
192
202
  };
203
+ if (result.smartCache && (!result.textScaleSteps || result.textScaleSteps.length === 0)) {
204
+ const step = (result.textScaleMax - result.textScaleMin) / 9;
205
+ const steps = [];
206
+ for (let i = 0; i < 10; i++) {
207
+ steps.push(result.textScaleMin + i * step);
208
+ }
209
+ result.textScaleSteps = steps;
210
+ }
211
+ return result;
193
212
  }
194
213
  const color = colorGetter();
195
214
  function nodeOptionsGetter(node) {
@@ -271,6 +290,30 @@ function highlightSettingsGetter(settings, prevSettings) {
271
290
  };
272
291
  }
273
292
 
293
+ function computeGraphBounds(nodes) {
294
+ if (!nodes.length)
295
+ return null;
296
+ let minX = Infinity;
297
+ let minY = Infinity;
298
+ let maxX = -Infinity;
299
+ let maxY = -Infinity;
300
+ for (const node of nodes) {
301
+ if (node.x === null || node.x === undefined || node.y === null || node.y === undefined)
302
+ continue;
303
+ if (node.visible === false)
304
+ continue;
305
+ let radius = node._radius;
306
+ radius ??= Math.max(node._width ?? 0, node._height ?? 0) / 2;
307
+ minX = Math.min(minX, node.x - radius);
308
+ minY = Math.min(minY, node.y - radius);
309
+ maxX = Math.max(maxX, node.x + radius);
310
+ maxY = Math.max(maxY, node.y + radius);
311
+ }
312
+ if (!isFinite(minX))
313
+ return null;
314
+ return { minX, minY, maxX, maxY };
315
+ }
316
+
274
317
  function pointerGetter(mouseEvent, areaRect, areaTransform) {
275
318
  const clientX = "clientX" in mouseEvent
276
319
  ? mouseEvent.clientX
@@ -735,42 +778,52 @@ function isNodeVisible(opts) {
735
778
  }
736
779
  }
737
780
 
781
+ const FRAME_INTERVAL = 1000 / 60;
738
782
  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;
783
+ const now = performance.now();
784
+ const particle = opts.particle;
785
+ if (particle._lastTime == undefined) {
786
+ particle._distanceTraveled = (opts.distance / opts.totalCount) * particle.index;
787
+ particle._lastTime = now;
788
+ particle._lastDistance = opts.distance;
752
789
  }
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;
790
+ const lastDistance = particle._lastDistance ?? 0;
791
+ if (lastDistance !== opts.distance && lastDistance > 0) {
792
+ const scale = opts.distance / lastDistance;
793
+ particle._distanceTraveled = ((particle._distanceTraveled ?? 0) * scale) % opts.distance;
794
+ particle._lastDistance = opts.distance;
760
795
  }
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;
796
+ const delta = Math.min(now - particle._lastTime, FRAME_INTERVAL);
797
+ particle._lastTime = now;
798
+ particle._distanceTraveled =
799
+ ((particle._distanceTraveled ?? 0) + opts.speed * delta) % opts.distance;
800
+ const progress = particle._distanceTraveled / opts.distance;
801
+ if (opts.xControl !== 0 && opts.yControl !== 0) {
802
+ const t = 1 - progress;
803
+ particle.x =
804
+ t * t * opts.xStart + 2 * t * progress * opts.xControl + progress * progress * opts.xEnd;
805
+ particle.y =
806
+ t * t * opts.yStart + 2 * t * progress * opts.yControl + progress * progress * opts.yEnd;
769
807
  }
770
808
  else {
771
- opts.particle.step++;
809
+ const dx = opts.xEnd - opts.xStart;
810
+ const dy = opts.yEnd - opts.yStart;
811
+ particle.x = opts.xStart + dx * progress;
812
+ particle.y = opts.yStart + dy * progress;
772
813
  }
773
814
  }
815
+ function approximateQuadraticBezierLength(x0, y0, x1, y1, x2, y2) {
816
+ const dx01 = x1 - x0;
817
+ const dy01 = y1 - y0;
818
+ const dx12 = x2 - x1;
819
+ const dy12 = y2 - y1;
820
+ const dx02 = x2 - x0;
821
+ const dy02 = y2 - y0;
822
+ const a = Math.sqrt(dx01 * dx01 + dy01 * dy01);
823
+ const b = Math.sqrt(dx12 * dx12 + dy12 * dy12);
824
+ const c = Math.sqrt(dx02 * dx02 + dy02 * dy02);
825
+ return (a + b + c) / 2;
826
+ }
774
827
 
775
828
  function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkHoverExtraZone, curve, }) {
776
829
  if (!areaRect)
@@ -1046,22 +1099,38 @@ function extractLinkPointIds(link) {
1046
1099
  return { sourceId, targetId };
1047
1100
  }
1048
1101
 
1102
+ function isEmptyObject(obj) {
1103
+ for (const key in obj) {
1104
+ if (Object.hasOwn(obj, key)) {
1105
+ return false;
1106
+ }
1107
+ }
1108
+ return true;
1109
+ }
1110
+
1049
1111
  const GRAPH_SETTINGS = {
1050
- zoomExtent: [0.3, 10],
1051
- translateExtent: [[], []],
1052
- translateExtentEnable: true,
1053
- translateExtentCoefficient: [3, 3],
1112
+ zoomAnimation: true,
1113
+ zoomAnimationDuration: 300,
1114
+ zoomToNodeScale: 5,
1115
+ zoomToFitMargin: 0.25,
1116
+ zoomExtent: null,
1117
+ zoomExtentMargin: 0.3,
1118
+ translateExtent: null,
1119
+ translateExtentCoefficient: null,
1120
+ translateExtentOverlap: 0.3,
1054
1121
  dragPlaceCoefficient: dragPlaceCoefficientGetter,
1055
1122
  zoomInitial: null,
1056
1123
  showDrawTime: true,
1057
1124
  showDrawTimeEveryTick: false,
1058
1125
  };
1126
+ const PERFORMANCE_GRAPH_SETTINGS = {
1127
+ ...GRAPH_SETTINGS,
1128
+ };
1059
1129
 
1060
1130
  const HIGHLIGHT_SETTINGS = {
1061
1131
  highlightByHoverNode: true,
1062
1132
  highlightByHoverLink: true,
1063
- highlightUpFrames: 5,
1064
- highlightDownFrames: 5,
1133
+ highlightDuration: 150,
1065
1134
  linkHoverExtraZone: 2,
1066
1135
  /** Node */
1067
1136
  highlightByNodeOnlyRoot: true,
@@ -1116,37 +1185,51 @@ const HIGHLIGHT_SETTINGS = {
1116
1185
  highlightByLinkForArrowBorderColor: null,
1117
1186
  highlightByLinkForArrowBorderSizingAdditional: 0,
1118
1187
  };
1188
+ const PERFORMANCE_HIGHLIGHT_SETTINGS = {
1189
+ ...HIGHLIGHT_SETTINGS,
1190
+ };
1119
1191
 
1120
1192
  const LINK_SETTINGS = {
1193
+ smartCache: true,
1121
1194
  prettyDraw: true,
1122
- curve: false,
1195
+ curve: true,
1123
1196
  linkScaleSwitch: 1,
1124
- linkColorAfterScaleSwitch: "#000000FF",
1197
+ linkColorAfterScaleSwitch: "#C5C5C5FF",
1125
1198
  linkColorBeforeScaleSwitch: "#999",
1126
1199
  linkWidthAfterScaleSwitch: 0.1,
1127
1200
  linkWidthBeforeScaleSwitch: 1,
1128
1201
  arrow: true,
1129
1202
  arrowByHighlight: true,
1130
1203
  particles: true,
1131
- particleFlexSpeed: true,
1132
- particleFlexSpeedCoefficient: 4,
1204
+ // spawn one more particle for N px
1205
+ particleCountByDistance: 25,
1206
+ // speed for 1px
1207
+ particleSpeedByDistance: 50,
1133
1208
  };
1134
1209
  const LINK_OPTIONS = {
1135
1210
  alpha: 1,
1136
1211
  arrowAlpha: 1,
1137
1212
  arrowSize: 2,
1138
- arrowBorderColor: "#000000FF",
1213
+ arrowBorderColor: "#C5C5C5FF",
1139
1214
  arrowBorderWidth: 0.1,
1140
1215
  particleAlpha: 1,
1141
- particleColor: "#000000FF",
1142
- particleBorderColor: "#000000FF",
1216
+ particleColor: "#C5C5C5FF",
1217
+ particleBorderColor: "#C5C5C5FF",
1143
1218
  particleBorderWidth: 0.1,
1144
- particleCount: 2,
1145
1219
  particleRadius: 0.5,
1146
- particleSteps: 60,
1220
+ };
1221
+ const PERFORMANCE_LINK_SETTINGS = {
1222
+ ...LINK_SETTINGS,
1223
+ particles: false,
1224
+ prettyDraw: false,
1225
+ };
1226
+ const PERFORMANCE_LINK_OPTIONS = {
1227
+ ...LINK_OPTIONS,
1147
1228
  };
1148
1229
 
1149
1230
  const NODE_SETTINGS = {
1231
+ smartCache: true,
1232
+ textScaleSteps: [],
1150
1233
  nodeRadiusFlexible: true,
1151
1234
  nodeRadiusLinkCountForStep: 5,
1152
1235
  nodeRadiusIncrementByStep: 1,
@@ -1183,14 +1266,14 @@ const NODE_OPTIONS = {
1183
1266
  textShiftX: 0,
1184
1267
  textFont: "Arial",
1185
1268
  textAlign: "center",
1186
- textColor: "#333",
1269
+ textColor: "#d2d2d2",
1187
1270
  textStyle: "normal",
1188
1271
  textWeight: 500,
1189
1272
  textGap: 0,
1190
1273
  label: null,
1191
1274
  labelAlpha: 1,
1192
1275
  labelAlign: "center",
1193
- labelColor: "#333",
1276
+ labelColor: "#ffffff",
1194
1277
  labelFont: "Arial",
1195
1278
  labelGap: 0,
1196
1279
  labelSize: 3.5,
@@ -1198,6 +1281,14 @@ const NODE_OPTIONS = {
1198
1281
  labelWeight: 500,
1199
1282
  labelWidth: 20,
1200
1283
  };
1284
+ const PERFORMANCE_NODE_SETTINGS = {
1285
+ ...NODE_SETTINGS,
1286
+ nodeRadiusFlexible: false,
1287
+ nodeSizeFlexible: false,
1288
+ };
1289
+ const PERFOMANCE_NODE_OPTIONS = {
1290
+ ...NODE_OPTIONS,
1291
+ };
1201
1292
 
1202
1293
  const GRAPH_CACHE_TYPE = {
1203
1294
  NodeOptions: "nodeOptions",
@@ -1264,8 +1355,14 @@ function initDnd() {
1264
1355
  return;
1265
1356
  const mouseEvent = event.sourceEvent;
1266
1357
  const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1267
- event.subject.fx = pointerX;
1268
- event.subject.fy = pointerY;
1358
+ if (this._translateExtent) {
1359
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1360
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1361
+ }
1362
+ else {
1363
+ event.subject.fx = pointerX;
1364
+ event.subject.fy = pointerY;
1365
+ }
1269
1366
  this.listeners.onMoveDragFinished?.call?.(this, event);
1270
1367
  })
1271
1368
  .on("end", (event) => {
@@ -1275,8 +1372,14 @@ function initDnd() {
1275
1372
  const sourceEvent = event.sourceEvent;
1276
1373
  if (sourceEvent.altKey && this.areaRect) {
1277
1374
  const [pointerX, pointerY] = pointerGetter(sourceEvent, this.areaRect, this.areaTransform);
1278
- event.subject.fx = pointerX;
1279
- event.subject.fy = pointerY;
1375
+ if (this._translateExtent) {
1376
+ event.subject.fx = Math.max(this._translateExtent[0][0], Math.min(this._translateExtent[1][0], pointerX));
1377
+ event.subject.fy = Math.max(this._translateExtent[0][1], Math.min(this._translateExtent[1][1], pointerY));
1378
+ }
1379
+ else {
1380
+ event.subject.fx = pointerX;
1381
+ event.subject.fy = pointerY;
1382
+ }
1280
1383
  }
1281
1384
  else {
1282
1385
  event.subject.fx = null;
@@ -1324,7 +1427,7 @@ function adjustPoint(x, y, xControl, yControl, radius) {
1324
1427
  }
1325
1428
 
1326
1429
  function getDrawLink() {
1327
- return function drawLink(link) {
1430
+ return function drawLink(link, index) {
1328
1431
  if (!this.context ||
1329
1432
  typeof link.source !== "object" ||
1330
1433
  typeof link.target !== "object" ||
@@ -1339,8 +1442,7 @@ function getDrawLink() {
1339
1442
  return;
1340
1443
  if (!link.source._visible && !link.target._visible)
1341
1444
  return;
1342
- const id = `${link.target.id}${link.source.id}`;
1343
- const linkOptions = this.linkOptionsCache[id];
1445
+ const linkOptions = this.linkOptionsCache[index];
1344
1446
  if (!linkOptions)
1345
1447
  return;
1346
1448
  if (linkOptions.drawLink) {
@@ -1355,10 +1457,12 @@ function getDrawLink() {
1355
1457
  let arrowSize = linkOptions.arrowSize;
1356
1458
  let arrowBorderWidth = linkOptions.arrowBorderWidth;
1357
1459
  let arrowBorderColor = linkOptions.arrowBorderColor;
1460
+ const currentNodeHighlighted = this.highlightedNode &&
1461
+ (this.highlightedNode.id == link.source.id || this.highlightedNode.id == link.target.id);
1358
1462
  /** Highlight */
1359
1463
  if (this.highlightedNeighbors && this.highlightedNode) {
1360
1464
  /** By Node Not Highlight */
1361
- if (this.highlightedNode.id != link.source.id && this.highlightedNode.id != link.target.id) {
1465
+ if (!currentNodeHighlighted) {
1362
1466
  const fadeOptions = linkFade({
1363
1467
  arrow: this.linkSettings.arrow,
1364
1468
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1392,9 +1496,10 @@ function getDrawLink() {
1392
1496
  arrowBorderColor = highlightOptions.arrowBorderColor;
1393
1497
  }
1394
1498
  }
1499
+ const currentLinkHighlighted = this.highlightedLink === link;
1395
1500
  if (this.highlightedNeighbors && this.highlightedLink) {
1396
1501
  /** By Link Not Highlight */
1397
- if (this.highlightedLink !== link) {
1502
+ if (!currentLinkHighlighted) {
1398
1503
  const fadeOptions = linkFade({
1399
1504
  arrow: this.linkSettings.arrow,
1400
1505
  arrowByHighlight: this.linkSettings.arrowByHighlight,
@@ -1446,8 +1551,8 @@ function getDrawLink() {
1446
1551
  const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
1447
1552
  if (!this.linkSettings.curve) {
1448
1553
  if (this.linkSettings.prettyDraw ||
1449
- this.linkSettings.particleFlexSpeed ||
1450
- (this.linkSettings.arrow && arrowAlpha > 0)) {
1554
+ (this.linkSettings.arrow && arrowAlpha > 0) ||
1555
+ (this.particles && (currentNodeHighlighted || currentLinkHighlighted))) {
1451
1556
  const position = calculateLinkPositionByNode(xStart, yStart, xEnd, yEnd, link.source, link.target, isHasArrow ? arrowSize : 0);
1452
1557
  xStart = position.xStart;
1453
1558
  xEnd = position.xEnd;
@@ -1485,6 +1590,7 @@ function getDrawLink() {
1485
1590
  link._cy = position.yControl;
1486
1591
  link._ax = position.xEndArrow;
1487
1592
  link._ay = position.yEndArrow;
1593
+ linkDistance = approximateQuadraticBezierLength(position.xStart, position.yStart, position.xControl, position.yControl, position.xEnd, position.yEnd);
1488
1594
  this.context.beginPath();
1489
1595
  this.context.moveTo(position.xStart, position.yStart);
1490
1596
  this.context.quadraticCurveTo(position.xControl, position.yControl, position.xEnd, position.yEnd);
@@ -1495,69 +1601,46 @@ function getDrawLink() {
1495
1601
  this.context.stroke();
1496
1602
  }
1497
1603
  /** 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;
1604
+ if (this.linkSettings.particles && (currentLinkHighlighted || currentNodeHighlighted)) {
1605
+ const speed = (1 / this.linkSettings.particleSpeedByDistance) * (1 + 0.5);
1606
+ if (!this.particles[index]) {
1607
+ const particleCount = Math.max(1, Math.floor(linkDistance / this.linkSettings.particleCountByDistance));
1512
1608
  const particles = [];
1513
- let prevParticle;
1514
1609
  for (let i = 0; i < particleCount; i++) {
1515
1610
  const particle = {
1516
- step: 0,
1517
- sourceId,
1518
- targetId,
1519
- prev: prevParticle,
1520
- next: undefined,
1521
1611
  index: i,
1522
1612
  };
1523
- if (prevParticle)
1524
- prevParticle.next = particle;
1525
1613
  particles.push(particle);
1526
- prevParticle = particle;
1527
- }
1528
- if (particles.length >= 2) {
1529
- particles[0].prev = particles[particles.length - 1];
1530
- particles[particles.length - 1].next = particles[0];
1531
1614
  }
1532
- this.particles[id] = particles;
1615
+ this.particles[index] = particles;
1533
1616
  }
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
- }
1617
+ this.particles[index].forEach((particle, _, particles) => {
1618
+ if (!this.context)
1619
+ return;
1620
+ getParticlePosition({
1621
+ distance: linkDistance,
1622
+ particle,
1623
+ totalCount: particles.length,
1624
+ xEnd,
1625
+ xStart,
1626
+ yEnd,
1627
+ yStart,
1628
+ xControl,
1629
+ yControl,
1630
+ speed,
1559
1631
  });
1560
- }
1632
+ if (particle.x != undefined && particle.y != undefined) {
1633
+ this.context.beginPath();
1634
+ this.context.strokeStyle = linkOptions.particleBorderColor;
1635
+ this.context.lineWidth = linkOptions.particleBorderWidth;
1636
+ this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1637
+ this.context.fillStyle = linkOptions.particleColor;
1638
+ this.context.fill();
1639
+ if (linkOptions.particleBorderWidth > 0) {
1640
+ this.context.stroke();
1641
+ }
1642
+ }
1643
+ });
1561
1644
  }
1562
1645
  /** Arrow */
1563
1646
  if (this.linkSettings.arrow && arrowAlpha > 0) {
@@ -1641,7 +1724,7 @@ function getTextLines({ context, textAlign, textColor, textFont, textStyle, text
1641
1724
  }
1642
1725
 
1643
1726
  function getDrawNode(nodeRenders, textRenders) {
1644
- return function drawNode(node) {
1727
+ return function drawNode(node, index) {
1645
1728
  if (!this.context || !node.x || !node.y)
1646
1729
  return;
1647
1730
  if (node.visible != undefined && !node.visible) {
@@ -1650,7 +1733,7 @@ function getDrawNode(nodeRenders, textRenders) {
1650
1733
  node._height = 0;
1651
1734
  return;
1652
1735
  }
1653
- const nodeOptions = this.nodeOptionsCache[node.id];
1736
+ const nodeOptions = this.nodeOptionsCache[index];
1654
1737
  if (!nodeOptions)
1655
1738
  return;
1656
1739
  if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
@@ -1809,7 +1892,7 @@ function getDrawNode(nodeRenders, textRenders) {
1809
1892
  this.context.lineWidth = borderWidth;
1810
1893
  this.context.strokeStyle = borderColor;
1811
1894
  this.context.fillStyle = color;
1812
- const labelLines = this.cachedNodeLabel[node.id];
1895
+ const labelLines = this.cachedNodeLabel[index];
1813
1896
  switch (nodeOptions.shape) {
1814
1897
  case "circle": {
1815
1898
  if (!node.image) {
@@ -1935,7 +2018,7 @@ function getDrawNode(nodeRenders, textRenders) {
1935
2018
  });
1936
2019
  }
1937
2020
  /** Text draw */
1938
- const textLines = this.cachedNodeText[node.id];
2021
+ const textLines = this.cachedNodeText[index];
1939
2022
  if (nodeOptions.textVisible && nodeOptions.text && textLines) {
1940
2023
  textRenders.push(() => {
1941
2024
  if (nodeOptions.textDraw) {
@@ -1995,57 +2078,7 @@ function getDrawNode(nodeRenders, textRenders) {
1995
2078
  }
1996
2079
 
1997
2080
  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) {
2081
+ function draw() {
2049
2082
  if (!this.context)
2050
2083
  return;
2051
2084
  if (this.listeners.onDraw) {
@@ -2066,7 +2099,6 @@ function initDraw() {
2066
2099
  textRenders.forEach((render) => render());
2067
2100
  this.context.restore();
2068
2101
  this.listeners.onDrawFinished?.call?.(this);
2069
- calculateHighlight.bind(this)(recursive);
2070
2102
  }
2071
2103
  if (this.graphSettings.showDrawTime) {
2072
2104
  return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
@@ -2120,38 +2152,7 @@ function initPointer() {
2120
2152
  else {
2121
2153
  this.area.style.cursor = "default";
2122
2154
  }
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
- }
2155
+ this.animateHighlight(highlightNode ? currentNode : undefined, highlightLink ? currentLink : undefined);
2155
2156
  if (!this.listeners.onMove)
2156
2157
  return;
2157
2158
  if (!currentNode && !checkHighlightNode)
@@ -2321,132 +2322,42 @@ function initResize() {
2321
2322
  observer.observe(this.area);
2322
2323
  }
2323
2324
 
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
- }
2340
- });
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);
2372
- }
2373
- if (this.forceSettings.forces && this.forceSettings.xForce) {
2374
- this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2375
- }
2376
- if (this.forceSettings.forces && this.forceSettings.yForce) {
2377
- this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2378
- }
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));
2384
- }
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));
2387
- }
2388
- initCollideForce.call(this, true);
2389
- }
2390
- function initCollideForce(forceUpdate) {
2391
- if (!this.simulation)
2325
+ function updateLinkCache() {
2326
+ if (!this.context)
2392
2327
  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);
2328
+ const current = this.areaTransform.k;
2329
+ if (this.linkSettings.smartCache &&
2330
+ this._lastLinkZoomK !== undefined &&
2331
+ this.linkOptionsCache.length > 0) {
2332
+ const corner = this.linkSettings.linkScaleSwitch;
2333
+ const prev = this._lastLinkZoomK;
2334
+ this._lastLinkZoomK = current;
2335
+ if (!((prev <= corner && current >= corner) || (prev >= corner && current <= corner))) {
2336
+ return;
2403
2337
  }
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));
2338
+ for (let i = 0; i < this.links.length; i++) {
2339
+ const link = this.links[i];
2340
+ const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2341
+ const cache = this.linkOptionsCache[i];
2342
+ if (cache) {
2343
+ cache.color = linkOptions.color;
2344
+ cache.arrowColor = linkOptions.arrowColor;
2345
+ cache.width = linkOptions.width;
2346
+ }
2435
2347
  }
2348
+ return;
2436
2349
  }
2437
- }
2438
-
2439
- function updateLinkCache() {
2440
- this.linkOptionsCache = {};
2350
+ this._lastLinkZoomK = current;
2351
+ this.linkOptionsCache.length = 0;
2441
2352
  const groupMap = {};
2442
2353
  const groupSelfMap = {};
2443
2354
  for (let i = 0; i < this.links.length; i++) {
2444
2355
  const link = this.links[i];
2445
2356
  const { sourceId, targetId } = extractLinkPointIds(link);
2446
2357
  const linkOptions = linkIterationExtractor(link, i, this.links, this, this.linkSettings.options ?? {}, linkOptionsGetter);
2447
- const id = `${targetId}${sourceId}`;
2448
- this.linkOptionsCache[id] = linkOptions;
2358
+ this.linkOptionsCache[i] = linkOptions;
2449
2359
  if (this.linkSettings.curve) {
2360
+ const id = `${targetId}${sourceId}`;
2450
2361
  if (sourceId === targetId) {
2451
2362
  groupSelfMap[id] ??= 0;
2452
2363
  link._groupIndex = ++groupSelfMap[id];
@@ -2477,13 +2388,34 @@ function indexToPosition(n) {
2477
2388
  // return n & 1 ? -(n >> 1) : n >> 1;
2478
2389
  }
2479
2390
 
2480
- function updateNodeCache(types) {
2481
- this.nodeOptionsCache = {};
2391
+ function updateNodeCache() {
2482
2392
  if (!this.context)
2483
2393
  return;
2394
+ const currentZoom = this.areaTransform.k;
2395
+ const options = this.nodeOptionsCache.length === 0;
2396
+ const label = this.cachedNodeLabel.length === 0;
2397
+ let text = this.cachedNodeText.length === 0;
2398
+ if (this.nodeSettings.smartCache &&
2399
+ this._lastNodeZoomK !== undefined &&
2400
+ !options &&
2401
+ !text &&
2402
+ !label) {
2403
+ const thresholds = this.nodeSettings.textScaleSteps;
2404
+ if (thresholds && thresholds.length > 0) {
2405
+ const min = Math.min(this._lastNodeZoomK, currentZoom);
2406
+ const max = Math.max(this._lastNodeZoomK, currentZoom);
2407
+ const crossed = thresholds.some((t) => t >= min && t <= max);
2408
+ this._lastNodeZoomK = currentZoom;
2409
+ if (!crossed)
2410
+ return;
2411
+ }
2412
+ this.cachedNodeText.length = 0;
2413
+ text = true;
2414
+ }
2415
+ this._lastNodeZoomK = currentZoom;
2484
2416
  for (let i = 0; i < this.nodes.length; i++) {
2485
2417
  const node = this.nodes[i];
2486
- if (types === true || (jsHelpers.isObject(types) && types.options)) {
2418
+ if (options) {
2487
2419
  const nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2488
2420
  const radius = nodeOptions.shape === "circle"
2489
2421
  ? nodeRadiusGetter({
@@ -2534,11 +2466,19 @@ function updateNodeCache(types) {
2534
2466
  nodeOptions.height = node.visible === false ? 0 : height;
2535
2467
  nodeOptions.radius = node.visible === false ? 0 : radius;
2536
2468
  nodeOptions.labelSize = labelSize;
2537
- this.nodeOptionsCache[node.id] = nodeOptions;
2469
+ this.nodeOptionsCache[i] = nodeOptions;
2538
2470
  }
2539
- const nodeOptions = this.nodeOptionsCache[node.id];
2471
+ let nodeOptions = this.nodeOptionsCache[i];
2472
+ if (!nodeOptions)
2473
+ continue;
2540
2474
  /** label */
2541
- if (types === true || (jsHelpers.isObject(types) && types.label)) {
2475
+ if (label) {
2476
+ if (!options) {
2477
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2478
+ const cache = this.nodeOptionsCache[i];
2479
+ if (cache)
2480
+ cache.label = nodeOptions.label;
2481
+ }
2542
2482
  /** label in not text shape */
2543
2483
  if (nodeOptions.shape !== "text" && nodeOptions.label) {
2544
2484
  this.context.font = `${nodeOptions.labelStyle} normal ${nodeOptions.labelWeight} ${nodeOptions.labelSize}px ${nodeOptions.labelFont}`;
@@ -2546,7 +2486,7 @@ function updateNodeCache(types) {
2546
2486
  this.context.textAlign = nodeOptions.labelAlign;
2547
2487
  if (nodeOptions.labelWidth == undefined ||
2548
2488
  this.context.measureText(nodeOptions.label).width <= nodeOptions.labelWidth) {
2549
- this.cachedNodeLabel[node.id] = [nodeOptions.label];
2489
+ this.cachedNodeLabel[i] = [nodeOptions.label];
2550
2490
  }
2551
2491
  const { lines } = getTextLines({
2552
2492
  context: this.context,
@@ -2559,7 +2499,7 @@ function updateNodeCache(types) {
2559
2499
  textStyle: nodeOptions.labelStyle,
2560
2500
  textWeight: nodeOptions.labelWeight,
2561
2501
  });
2562
- this.cachedNodeLabel[node.id] = lines;
2502
+ this.cachedNodeLabel[i] = lines;
2563
2503
  /** label in text shape */
2564
2504
  }
2565
2505
  else if (nodeOptions.shape === "text" && nodeOptions.label) {
@@ -2583,18 +2523,29 @@ function updateNodeCache(types) {
2583
2523
  (lines.length - 1) * nodeOptions.labelGap +
2584
2524
  nodeOptions.labelYPadding;
2585
2525
  nodeOptions.width = maxSize + nodeOptions.labelXPadding;
2586
- this.cachedNodeLabel[node.id] = lines;
2526
+ this.cachedNodeLabel[i] = lines;
2527
+ }
2528
+ else {
2529
+ this.cachedNodeLabel[i] = [];
2587
2530
  }
2588
2531
  }
2589
2532
  /** text */
2590
- if (nodeOptions.text && (types === true || (jsHelpers.isObject(types) && types.text))) {
2533
+ if (text) {
2534
+ if (!options) {
2535
+ nodeOptions = nodeIterationExtractor(node, i, this.nodes, this, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2536
+ const cache = this.nodeOptionsCache[i];
2537
+ if (cache) {
2538
+ cache.text = nodeOptions.text;
2539
+ cache.textVisible = nodeOptions.textVisible;
2540
+ }
2541
+ }
2542
+ if (!nodeOptions.text) {
2543
+ this.cachedNodeText[i] = [];
2544
+ return;
2545
+ }
2591
2546
  this.context.font = `${nodeOptions.textStyle} normal ${nodeOptions.textWeight} ${nodeOptions.textSize}px ${nodeOptions.textFont}`;
2592
2547
  this.context.fillStyle = nodeOptions.textColor;
2593
2548
  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
2549
  const { lines } = getTextLines({
2599
2550
  context: this.context,
2600
2551
  maxWidth: nodeOptions.textWidth,
@@ -2606,49 +2557,230 @@ function updateNodeCache(types) {
2606
2557
  textStyle: nodeOptions.textStyle,
2607
2558
  textWeight: nodeOptions.textWeight,
2608
2559
  });
2609
- this.cachedNodeText[node.id] = lines;
2560
+ this.cachedNodeText[i] = lines;
2610
2561
  }
2611
2562
  }
2612
2563
  }
2613
2564
 
2565
+ const DEFAULT_ZOOM_EXTENT = [0.3, 10];
2566
+ const DEFAULT_TRANSLATE_EXTENT_COEFFICIENT = 8;
2567
+ const DEFAULT_MARGIN = 0.25;
2614
2568
  function initZoom(currentZoom) {
2615
2569
  if (!this.area)
2616
2570
  throw new Error("bad init data");
2571
+ const bounds = computeGraphBounds(this.nodes);
2572
+ const zoomExtent = resolveZoomExtent(this.graphSettings.zoomExtent, this.width, this.height, this.graphSettings.zoomExtentMargin ?? DEFAULT_MARGIN, bounds);
2573
+ const translateExtent = resolveTranslateExtent(this.graphSettings.translateExtent, this.graphSettings.translateExtentCoefficient, this.graphSettings.translateExtentOverlap, this.width, this.height, zoomExtent, bounds);
2574
+ this._translateExtent = translateExtent;
2617
2575
  const zoomInstance = d3Zoom.zoom()
2618
- .scaleExtent(this.graphSettings.zoomExtent)
2576
+ .scaleExtent(zoomExtent)
2619
2577
  .on("zoom", (event) => {
2578
+ if (this._zoomAnimating)
2579
+ return;
2620
2580
  this.listeners.onZoom?.call?.(this, event);
2621
2581
  const oldTransform = this.areaTransform;
2622
2582
  this.areaTransform = event.transform;
2623
2583
  if (this.areaTransform.k !== oldTransform.k) {
2624
2584
  updateLinkCache.call(this);
2625
- updateNodeCache.call(this, true);
2585
+ updateNodeCache.call(this);
2626
2586
  }
2627
- if (!this.simulationWorking && !this.highlightWorking)
2628
- requestAnimationFrame(() => this.draw());
2587
+ this.tick();
2629
2588
  });
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
- }
2589
+ zoomInstance.translateExtent(translateExtent);
2641
2590
  d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2642
2591
  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);
2592
+ this.areaTransform = new d3Zoom.ZoomTransform(Math.max(zoomInitial?.k ?? 1, zoomExtent[0]), zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2644
2593
  d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
2645
2594
  }
2595
+ function resolveZoomExtent(userExtent, viewWidth, viewHeight, margin, bounds) {
2596
+ if (userExtent &&
2597
+ userExtent.length === 2 &&
2598
+ userExtent[0] != undefined &&
2599
+ userExtent[1] != undefined) {
2600
+ return userExtent;
2601
+ }
2602
+ if (!bounds)
2603
+ return DEFAULT_ZOOM_EXTENT;
2604
+ const graphWidth = bounds.maxX - bounds.minX;
2605
+ const graphHeight = bounds.maxY - bounds.minY;
2606
+ if (graphWidth === 0 || graphHeight === 0)
2607
+ return DEFAULT_ZOOM_EXTENT;
2608
+ const scaleToFit = (1 - margin) / Math.max(graphWidth / viewWidth, graphHeight / viewHeight);
2609
+ return [scaleToFit, DEFAULT_ZOOM_EXTENT[1]];
2610
+ }
2611
+ function resolveTranslateExtent(userExtent, userCoefficient, overlap, viewWidth, viewHeight, zoomExtent, bounds) {
2612
+ if (userExtent && userExtent.length >= 2 && jsHelpers.isArray(userExtent[0]) && jsHelpers.isArray(userExtent[1])) {
2613
+ return [
2614
+ [
2615
+ userExtent[0][0] ?? -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2616
+ userExtent[0][1] ?? -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2617
+ ],
2618
+ [
2619
+ userExtent[1][0] ?? viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2620
+ userExtent[1][1] ?? viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2621
+ ],
2622
+ ];
2623
+ }
2624
+ if (userCoefficient) {
2625
+ const [cx, cy] = jsHelpers.isArray(userCoefficient)
2626
+ ? userCoefficient
2627
+ : [userCoefficient, userCoefficient];
2628
+ return [
2629
+ [-viewWidth * cx, -viewHeight * cy],
2630
+ [viewWidth * cx, viewHeight * cy],
2631
+ ];
2632
+ }
2633
+ if (!bounds) {
2634
+ return [
2635
+ [
2636
+ -viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2637
+ -viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2638
+ ],
2639
+ [
2640
+ viewWidth * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2641
+ viewHeight * DEFAULT_TRANSLATE_EXTENT_COEFFICIENT,
2642
+ ],
2643
+ ];
2644
+ }
2645
+ const minZoom = zoomExtent[0];
2646
+ const zoomViewWidth = viewWidth / minZoom;
2647
+ const zoomViewHeight = viewHeight / minZoom;
2648
+ const graphWidth = bounds.maxX - bounds.minX;
2649
+ const graphHeight = bounds.maxY - bounds.minY;
2650
+ const minX = bounds.minX + overlap * graphWidth - zoomViewWidth;
2651
+ const maxX = bounds.maxX - overlap * graphWidth + zoomViewWidth;
2652
+ const minY = bounds.minY + overlap * graphHeight - zoomViewHeight;
2653
+ const maxY = bounds.maxY - overlap * graphHeight + zoomViewHeight;
2654
+ return [
2655
+ [minX, minY],
2656
+ [maxX, maxY],
2657
+ ];
2658
+ }
2659
+
2660
+ function initSimulation() {
2661
+ if (!this.simulation) {
2662
+ this.simulation = d3Force.forceSimulation()
2663
+ .nodes(this.nodes)
2664
+ .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter.bind(this)))
2665
+ .on("tick", () => {
2666
+ this.draw();
2667
+ })
2668
+ .on("end", () => {
2669
+ this.listeners.onSimulationEnd?.call?.(this);
2670
+ if (this.area) {
2671
+ initZoom.call(this, this.areaTransform);
2672
+ }
2673
+ if (this.graphSettings.showDrawTime) {
2674
+ getDrawTime();
2675
+ // eslint-disable-next-line no-console
2676
+ console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2677
+ resetDrawTime();
2678
+ }
2679
+ })
2680
+ .stop();
2681
+ initSimulationForces.call(this);
2682
+ }
2683
+ }
2684
+ function initSimulationForces() {
2685
+ if (!this.simulation)
2686
+ return;
2687
+ const linkForce = this.simulation.force("link");
2688
+ if (!linkForce)
2689
+ return;
2690
+ if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2691
+ this.simulation.force("x", null);
2692
+ }
2693
+ if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2694
+ this.simulation.force("y", null);
2695
+ }
2696
+ if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2697
+ this.simulation.force("charge")) {
2698
+ this.simulation.force("charge", null);
2699
+ }
2700
+ if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2701
+ this.simulation.force("center")) {
2702
+ this.simulation.force("center", null);
2703
+ }
2704
+ if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2705
+ linkForce.distance(0).strength(0).iterations(0);
2706
+ }
2707
+ if (this.forceSettings.forces && this.forceSettings.linkForce) {
2708
+ linkForce
2709
+ .distance(this.forceSettings.linkDistance)
2710
+ .strength(this.forceSettings.linkStrength)
2711
+ .iterations(this.forceSettings.linkIterations);
2712
+ }
2713
+ if (this.forceSettings.forces && this.forceSettings.xForce) {
2714
+ this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2715
+ }
2716
+ if (this.forceSettings.forces && this.forceSettings.yForce) {
2717
+ this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2718
+ }
2719
+ if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2720
+ this.simulation.force("charge", d3Force.forceManyBody()
2721
+ .strength(this.forceSettings.chargeStrength)
2722
+ .distanceMax(Infinity)
2723
+ .distanceMin(this.forceSettings.chargeDistanceMin));
2724
+ }
2725
+ if (this.forceSettings.forces && this.forceSettings.centerForce) {
2726
+ this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2727
+ }
2728
+ initCollideForce.call(this, true);
2729
+ }
2730
+ function initCollideForce(forceUpdate) {
2731
+ if (!this.simulation)
2732
+ return;
2733
+ if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2734
+ this.simulation.force("collide")) {
2735
+ this.simulation.force("collide", null);
2736
+ }
2737
+ if (this.forceSettings.forces && this.forceSettings.collideForce) {
2738
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2739
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2740
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2741
+ if (isMaxCollideNodes && isMaxCollideLinks) {
2742
+ this.simulation.force("collide", null);
2743
+ }
2744
+ else if (!this.simulation.force("collide") || forceUpdate) {
2745
+ this.simulation.force("collide", d3Force.forceCollide()
2746
+ .radius((node, index) => {
2747
+ const nodeOptions = this.nodeOptionsCache[index];
2748
+ if (!nodeOptions)
2749
+ return 0;
2750
+ switch (nodeOptions.shape) {
2751
+ case "circle": {
2752
+ if (this.forceSettings.collideRadius) {
2753
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2754
+ }
2755
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2756
+ }
2757
+ case "square": {
2758
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2759
+ this.forceSettings.collideAdditionalRadius);
2760
+ }
2761
+ case "text": {
2762
+ return (Math.sqrt(nodeOptions.width ** 2 + nodeOptions.height ** 2) / 2 +
2763
+ this.forceSettings.collideAdditionalRadius);
2764
+ }
2765
+ default: {
2766
+ if (this.forceSettings.collideRadius) {
2767
+ return nodeIterationExtractor(node, index, this.nodes, this, this.forceSettings.collideRadius, undefined);
2768
+ }
2769
+ return nodeOptions.radius + this.forceSettings.collideAdditionalRadius;
2770
+ }
2771
+ }
2772
+ })
2773
+ .strength(this.forceSettings.collideStrength)
2774
+ .iterations(this.forceSettings.collideIterations));
2775
+ }
2776
+ }
2777
+ }
2646
2778
 
2647
2779
  class GraphCanvas {
2648
2780
  /** initial data */
2649
2781
  nodes;
2650
2782
  links;
2651
- particles = {};
2783
+ particles = [];
2652
2784
  width;
2653
2785
  height;
2654
2786
  root;
@@ -2665,19 +2797,25 @@ class GraphCanvas {
2665
2797
  context;
2666
2798
  simulation;
2667
2799
  areaTransform = d3Zoom.zoomIdentity;
2800
+ _translateExtent;
2668
2801
  areaRect;
2669
2802
  draw;
2670
2803
  eventAbortController;
2671
- cachedNodeText = {};
2672
- cachedNodeLabel = {};
2673
- linkOptionsCache = {};
2674
- nodeOptionsCache = {};
2804
+ cachedNodeText = [];
2805
+ cachedNodeLabel = [];
2806
+ linkOptionsCache = [];
2807
+ nodeOptionsCache = [];
2675
2808
  isDragging = false;
2676
2809
  highlightedNode = null;
2677
2810
  highlightedLink = null;
2678
2811
  highlightedNeighbors = null;
2679
2812
  highlightProgress = 1;
2680
- highlightWorking = false;
2813
+ highlightStart = null;
2814
+ highlightPositive = false;
2815
+ highlightController;
2816
+ _lastNodeZoomK;
2817
+ _lastLinkZoomK;
2818
+ _zoomAnimating = false;
2681
2819
  get simulationWorking() {
2682
2820
  const simulationAlpha = this.simulation?.alpha?.() ?? 0;
2683
2821
  const simulationAlphaMin = this.simulation?.alphaMin?.() ?? 0;
@@ -2712,13 +2850,148 @@ class GraphCanvas {
2712
2850
  nodes: this.nodes,
2713
2851
  };
2714
2852
  }
2853
+ fitToView(margin = this.graphSettings.zoomToFitMargin, duration = this.graphSettings.zoomAnimationDuration) {
2854
+ const area = this.area;
2855
+ if (!area)
2856
+ return;
2857
+ const bounds = computeGraphBounds(this.nodes);
2858
+ if (!bounds)
2859
+ return;
2860
+ const graphWidth = bounds.maxX - bounds.minX;
2861
+ const graphHeight = bounds.maxY - bounds.minY;
2862
+ if (graphWidth === 0 || graphHeight === 0)
2863
+ return;
2864
+ const graphCenterX = bounds.minX + graphWidth / 2;
2865
+ const graphCenterY = bounds.minY + graphHeight / 2;
2866
+ const scale = (1 - margin) / Math.max(graphWidth / this.width, graphHeight / this.height);
2867
+ const clampedScale = Math.min(scale, this.graphSettings.zoomExtent?.[1] ?? scale);
2868
+ const target = d3Zoom.zoomIdentity
2869
+ .translate(this.width / 2, this.height / 2)
2870
+ .scale(clampedScale)
2871
+ .translate(-graphCenterX, -graphCenterY);
2872
+ if (!this.graphSettings.zoomAnimation) {
2873
+ this.areaTransform = target;
2874
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
2875
+ this.clearCache(true);
2876
+ this.tick();
2877
+ return;
2878
+ }
2879
+ this.animateZoom(area, target, this.areaTransform, duration);
2880
+ }
2881
+ focusOnNode(nodeId, scale = this.graphSettings.zoomToNodeScale, duration = this.graphSettings.zoomAnimationDuration) {
2882
+ const area = this.area;
2883
+ if (!area)
2884
+ return;
2885
+ const node = this.nodes.find((n) => n.id === nodeId);
2886
+ if (!node)
2887
+ return;
2888
+ if (node.x == undefined || node.y == undefined)
2889
+ return;
2890
+ const target = d3Zoom.zoomIdentity
2891
+ .translate(this.width / 2, this.height / 2)
2892
+ .scale(scale)
2893
+ .translate(-node.x, -node.y);
2894
+ if (!this.graphSettings.zoomAnimation) {
2895
+ this.areaTransform = target;
2896
+ d3Zoom.zoom().transform(d3Selection.select(area), target);
2897
+ this.clearCache(true);
2898
+ this.tick();
2899
+ return;
2900
+ }
2901
+ this.animateZoom(area, target, this.areaTransform, duration);
2902
+ }
2903
+ animateHighlight(node, link, baseDuration = this.highlightSettings.highlightDuration) {
2904
+ let positive = true;
2905
+ if (node && (this.highlightedNode !== node || !this.highlightPositive)) {
2906
+ this.highlightedNode = node;
2907
+ this.highlightedNeighbors = new Set(node.neighbors);
2908
+ this.particles = [];
2909
+ this.highlightedLink = null;
2910
+ this.highlightStart = performance.now();
2911
+ }
2912
+ else if (link && (this.highlightedLink !== link || !this.highlightPositive)) {
2913
+ const { sourceId, targetId } = extractLinkPointIds(link);
2914
+ this.highlightProgress = 0;
2915
+ this.highlightedLink = link;
2916
+ this.highlightedNeighbors = new Set([sourceId, targetId]);
2917
+ this.particles = [];
2918
+ this.highlightedNode = null;
2919
+ this.highlightStart = performance.now();
2920
+ }
2921
+ else if (!node && !link && this.highlightPositive) {
2922
+ positive = false;
2923
+ }
2924
+ else {
2925
+ return;
2926
+ }
2927
+ if (this.highlightController) {
2928
+ this.highlightController.abort();
2929
+ }
2930
+ const controller = new AbortController();
2931
+ this.highlightPositive = positive;
2932
+ this.highlightController = controller;
2933
+ const startTime = performance.now();
2934
+ const startProgress = this.highlightProgress;
2935
+ const targetProgress = positive ? 1 : 0;
2936
+ const delta = targetProgress - startProgress;
2937
+ const duration = baseDuration * Math.abs(delta);
2938
+ const animate = () => {
2939
+ if (controller.signal.aborted)
2940
+ return;
2941
+ const elapsed = performance.now() - startTime;
2942
+ const t = Math.min(elapsed / duration, 1);
2943
+ const current = startProgress + delta * t;
2944
+ const eased = current < 0.5 ? 4 * current * current * current : 1 - (-2 * current + 2) ** 3 / 2;
2945
+ this.highlightProgress = eased;
2946
+ if (t < 1 || positive) {
2947
+ requestAnimationFrame(animate);
2948
+ this.draw();
2949
+ }
2950
+ else {
2951
+ this.highlightedNode = null;
2952
+ this.highlightedLink = null;
2953
+ this.highlightedNeighbors = null;
2954
+ this.highlightStart = null;
2955
+ this.particles = [];
2956
+ this.tick();
2957
+ }
2958
+ };
2959
+ requestAnimationFrame(animate);
2960
+ }
2961
+ animateZoom(area, target, start, duration) {
2962
+ this._zoomAnimating = true;
2963
+ const startTime = performance.now();
2964
+ const animate = () => {
2965
+ const elapsed = performance.now() - startTime;
2966
+ const t = Math.min(elapsed / duration, 1);
2967
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
2968
+ const x = start.x + (target.x - start.x) * eased;
2969
+ const y = start.y + (target.y - start.y) * eased;
2970
+ const k = start.k + (target.k - start.k) * eased;
2971
+ this.areaTransform = new d3Zoom.ZoomTransform(k, x, y);
2972
+ d3Zoom.zoom().transform(d3Selection.select(area), this.areaTransform);
2973
+ updateLinkCache.call(this);
2974
+ updateNodeCache.call(this);
2975
+ this.tick();
2976
+ if (t < 1) {
2977
+ requestAnimationFrame(animate);
2978
+ }
2979
+ else {
2980
+ this._zoomAnimating = false;
2981
+ this.clearCache(true);
2982
+ this.tick();
2983
+ }
2984
+ };
2985
+ requestAnimationFrame(animate);
2986
+ }
2715
2987
  changeData(options, alpha = 0.5, clearCache = true) {
2716
2988
  if (options.links != undefined)
2717
2989
  this.links = options.links;
2718
2990
  if (options.nodes != undefined)
2719
2991
  this.nodes = options.nodes;
2720
- if (options.nodes != undefined || options.links != undefined)
2992
+ if (options.nodes != undefined || options.links != undefined) {
2721
2993
  this.updateData(alpha, clearCache);
2994
+ }
2722
2995
  }
2723
2996
  changeSettings(options, clearCache = true) {
2724
2997
  if (options.graphSettings) {
@@ -2793,31 +3066,76 @@ class GraphCanvas {
2793
3066
  }
2794
3067
  clearCache(keys) {
2795
3068
  if (keys === true) {
2796
- updateNodeCache.call(this, true);
2797
- updateLinkCache.call(this);
3069
+ this.nodeOptionsCache.length = 0;
3070
+ this.linkOptionsCache.length = 0;
3071
+ this.cachedNodeLabel.length = 0;
3072
+ this.cachedNodeText.length = 0;
2798
3073
  }
2799
3074
  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);
3075
+ for (const key of keys) {
3076
+ switch (key) {
3077
+ case GRAPH_CACHE_TYPE.NodeText: {
3078
+ this.cachedNodeText.length = 0;
3079
+ break;
3080
+ }
3081
+ case GRAPH_CACHE_TYPE.NodeLabel: {
3082
+ this.cachedNodeLabel.length = 0;
3083
+ break;
3084
+ }
3085
+ case GRAPH_CACHE_TYPE.NodeOptions: {
3086
+ this.nodeOptionsCache.length = 0;
3087
+ break;
3088
+ }
3089
+ case GRAPH_CACHE_TYPE.LinkOptions: {
3090
+ this.linkOptionsCache.length = 0;
3091
+ break;
3092
+ }
3093
+ }
2811
3094
  }
2812
3095
  }
3096
+ updateNodeCache.call(this);
3097
+ updateLinkCache.call(this);
2813
3098
  }
2814
3099
  tick() {
2815
- if (!this.simulationWorking && !this.highlightWorking)
3100
+ if (!this.simulationWorking)
2816
3101
  this.draw();
2817
3102
  }
2818
- restart(alpha) {
2819
- if (this.simulation)
3103
+ restart(alpha, options) {
3104
+ if (!this.simulation)
3105
+ return;
3106
+ const settings = {
3107
+ precompute: options?.precompute ?? this.forceSettings.precompute,
3108
+ precomputeMaxTimeMs: options?.precomputeMaxTimeMs ?? this.forceSettings?.precomputeMaxTimeMs,
3109
+ precomputeMaxTicks: options?.precomputeMaxTicks ?? this.forceSettings?.precomputeMaxTicks ?? 300,
3110
+ precomputeDisableForcesAfter: options?.precomputeDisableForcesAfter ??
3111
+ this.forceSettings?.precomputeDisableForcesAfter ??
3112
+ false,
3113
+ };
3114
+ if (!settings.precompute) {
2820
3115
  this.simulation.alpha(alpha ?? 1).restart();
3116
+ return;
3117
+ }
3118
+ if (settings.precomputeDisableForcesAfter) {
3119
+ this.forceSettings = forceSettingsGetter({ forces: true }, this.forceSettings);
3120
+ }
3121
+ initSimulationForces.call(this);
3122
+ this.simulation.stop();
3123
+ this.simulation.alpha(alpha ?? 1);
3124
+ const startTime = performance.now();
3125
+ let ticks = 0;
3126
+ while (performance.now() - startTime < settings.precomputeMaxTimeMs &&
3127
+ ticks < settings.precomputeMaxTicks) {
3128
+ this.simulation.tick(1);
3129
+ ticks++;
3130
+ if (this.simulation.alpha() <= this.simulation.alphaMin())
3131
+ break;
3132
+ }
3133
+ if (settings.precomputeDisableForcesAfter) {
3134
+ this.forceSettings = forceSettingsGetter({ forces: false }, this.forceSettings);
3135
+ }
3136
+ initSimulationForces.call(this);
3137
+ this.simulation.restart();
3138
+ this.tick();
2821
3139
  }
2822
3140
  start() {
2823
3141
  if (this.simulation)
@@ -2854,8 +3172,7 @@ class GraphCanvas {
2854
3172
  updateSimulation() {
2855
3173
  if (this.simulation) {
2856
3174
  initSimulationForces.call(this);
2857
- this.simulation.alpha(1);
2858
- this.simulation.restart();
3175
+ this.restart(1);
2859
3176
  }
2860
3177
  }
2861
3178
  clearState() {
@@ -2864,7 +3181,8 @@ class GraphCanvas {
2864
3181
  this.highlightedLink = null;
2865
3182
  this.highlightedNeighbors = null;
2866
3183
  this.highlightProgress = 0;
2867
- this.highlightWorking = false;
3184
+ this.highlightStart = null;
3185
+ this.highlightPositive = false;
2868
3186
  }
2869
3187
  updateData(alpha = 0.5, clearCache = true) {
2870
3188
  if (clearCache) {
@@ -2872,9 +3190,7 @@ class GraphCanvas {
2872
3190
  }
2873
3191
  if (this.simulation) {
2874
3192
  initCollideForce.call(this, false);
2875
- this.simulation
2876
- .nodes(this.nodes)
2877
- .force("link", d3Force.forceLink(this.links)
3193
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2878
3194
  .id(this.nodeSettings.idGetter.bind(this))
2879
3195
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2880
3196
  ? this.forceSettings.linkDistance
@@ -2884,16 +3200,17 @@ class GraphCanvas {
2884
3200
  : 0)
2885
3201
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2886
3202
  ? this.forceSettings.linkIterations
2887
- : 0))
2888
- .alpha(alpha)
2889
- .restart();
3203
+ : 0));
3204
+ this.restart(alpha);
3205
+ initZoom.call(this, this.areaTransform);
2890
3206
  }
2891
3207
  }
2892
3208
  init() {
2893
3209
  initArea.call(this);
2894
- updateNodeCache.call(this, true);
3210
+ updateNodeCache.call(this);
2895
3211
  updateLinkCache.call(this);
2896
3212
  initSimulation.call(this);
3213
+ this.restart(1);
2897
3214
  initDnd.call(this);
2898
3215
  initZoom.call(this);
2899
3216
  initResize.call(this);
@@ -3059,22 +3376,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
3059
3376
  label: "Расширение границы связи для курсора",
3060
3377
  },
3061
3378
  {
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,
3379
+ id: "highlightDuration",
3380
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3381
+ max: 1000,
3382
+ min: 10,
3075
3383
  step: 1,
3076
3384
  type: "range",
3077
- label: "Скорость применения анимации в кадрах",
3385
+ label: "Скорость анимации",
3078
3386
  },
3079
3387
  ];
3080
3388
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3547,19 +3855,22 @@ const LINK_SETTINGS_CONTROLS = [
3547
3855
  initialValue: LINK_SETTINGS.particles,
3548
3856
  },
3549
3857
  {
3550
- id: "particleFlexSpeed",
3551
- type: "checkbox",
3552
- label: "Гибкий расчет скорости частицы",
3553
- initialValue: LINK_SETTINGS.particleFlexSpeed,
3858
+ id: "particleSpeedByDistance",
3859
+ initialValue: LINK_SETTINGS.particleSpeedByDistance,
3860
+ max: 500,
3861
+ min: 1,
3862
+ step: 1,
3863
+ type: "range",
3864
+ label: "Скорость частицы на один пиксель",
3554
3865
  },
3555
3866
  {
3556
- id: "particleFlexSpeedCoefficient",
3557
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3558
- max: 10,
3559
- min: 0.1,
3560
- step: 0.1,
3867
+ id: "particleCountByDistance",
3868
+ initialValue: LINK_SETTINGS.particleCountByDistance,
3869
+ max: 250,
3870
+ min: 1,
3871
+ step: 1,
3561
3872
  type: "range",
3562
- label: "Коэффициент скорости частицы относительно расстояния",
3873
+ label: "Количество пикселей для появления частицы",
3563
3874
  },
3564
3875
  ];
3565
3876
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3654,24 +3965,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3654
3965
  label: "Радиус",
3655
3966
  initialValue: LINK_OPTIONS.particleRadius,
3656
3967
  },
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
3968
  {
3676
3969
  id: "particleBorderColor",
3677
3970
  type: "color",
@@ -4105,10 +4398,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
4105
4398
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
4106
4399
  exports.NODE_SETTINGS = NODE_SETTINGS;
4107
4400
  exports.NODE_SETTINGS_CONTROLS = NODE_SETTINGS_CONTROLS;
4401
+ exports.PERFOMANCE_NODE_OPTIONS = PERFOMANCE_NODE_OPTIONS;
4402
+ exports.PERFORMANCE_FORCE_SETTINGS = PERFORMANCE_FORCE_SETTINGS;
4403
+ exports.PERFORMANCE_GRAPH_SETTINGS = PERFORMANCE_GRAPH_SETTINGS;
4404
+ exports.PERFORMANCE_HIGHLIGHT_SETTINGS = PERFORMANCE_HIGHLIGHT_SETTINGS;
4405
+ exports.PERFORMANCE_LINK_OPTIONS = PERFORMANCE_LINK_OPTIONS;
4406
+ exports.PERFORMANCE_LINK_SETTINGS = PERFORMANCE_LINK_SETTINGS;
4407
+ exports.PERFORMANCE_NODE_SETTINGS = PERFORMANCE_NODE_SETTINGS;
4108
4408
  exports.animationByProgress = animationByProgress;
4409
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
4109
4410
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
4110
4411
  exports.colorGetter = colorGetter;
4111
4412
  exports.colorToRgb = colorToRgb;
4413
+ exports.computeGraphBounds = computeGraphBounds;
4112
4414
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
4113
4415
  exports.drawText = drawText;
4114
4416
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -4120,6 +4422,7 @@ exports.getDrawNode = getDrawNode;
4120
4422
  exports.getParticlePosition = getParticlePosition;
4121
4423
  exports.graphSettingsGetter = graphSettingsGetter;
4122
4424
  exports.highlightSettingsGetter = highlightSettingsGetter;
4425
+ exports.isEmptyObject = isEmptyObject;
4123
4426
  exports.isNodeVisible = isNodeVisible;
4124
4427
  exports.isOverlapsNode = isOverlapsNode;
4125
4428
  exports.linkByPointerGetter = linkByPointerGetter;