@krainovsd/graph 0.13.0-beta3 → 0.14.0-beta.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 (68) hide show
  1. package/lib/cjs/index.cjs +772 -466
  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 +265 -74
  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 +85 -46
  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
1614
  }
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;
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;
@@ -2706,21 +2844,72 @@ class GraphCanvas {
2706
2844
  get dpi() {
2707
2845
  return devicePixelRatio;
2708
2846
  }
2709
- getData() {
2847
+ getData = () => {
2710
2848
  return {
2711
2849
  links: this.links,
2712
2850
  nodes: this.nodes,
2713
2851
  };
2714
- }
2715
- changeData(options, alpha = 0.5, clearCache = true) {
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
+ changeData = (options, alpha = 0.5, clearCache = true) => {
2716
2904
  if (options.links != undefined)
2717
2905
  this.links = options.links;
2718
2906
  if (options.nodes != undefined)
2719
2907
  this.nodes = options.nodes;
2720
- if (options.nodes != undefined || options.links != undefined)
2908
+ if (options.nodes != undefined || options.links != undefined) {
2721
2909
  this.updateData(alpha, clearCache);
2722
- }
2723
- changeSettings(options, clearCache = true) {
2910
+ }
2911
+ };
2912
+ changeSettings = (options, clearCache = true) => {
2724
2913
  if (options.graphSettings) {
2725
2914
  this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
2726
2915
  this.draw = initDraw.call(this);
@@ -2770,13 +2959,13 @@ class GraphCanvas {
2770
2959
  return void this.updateSimulation();
2771
2960
  }
2772
2961
  this.tick();
2773
- }
2774
- updateRect() {
2962
+ };
2963
+ updateRect = () => {
2775
2964
  if (!this.area)
2776
2965
  return;
2777
2966
  this.areaRect = this.area.getBoundingClientRect();
2778
- }
2779
- updateSize() {
2967
+ };
2968
+ updateSize = () => {
2780
2969
  if (!this.area)
2781
2970
  return;
2782
2971
  const { width, height } = this.root.getBoundingClientRect();
@@ -2790,51 +2979,96 @@ class GraphCanvas {
2790
2979
  throw new Error("couldn't create canvas context");
2791
2980
  this.context.scale(this.dpi, this.dpi);
2792
2981
  this.draw();
2793
- }
2794
- clearCache(keys) {
2982
+ };
2983
+ clearCache = (keys) => {
2795
2984
  if (keys === true) {
2796
- updateNodeCache.call(this, true);
2797
- updateLinkCache.call(this);
2985
+ this.nodeOptionsCache.length = 0;
2986
+ this.linkOptionsCache.length = 0;
2987
+ this.cachedNodeLabel.length = 0;
2988
+ this.cachedNodeText.length = 0;
2798
2989
  }
2799
2990
  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);
2991
+ for (const key of keys) {
2992
+ switch (key) {
2993
+ case GRAPH_CACHE_TYPE.NodeText: {
2994
+ this.cachedNodeText.length = 0;
2995
+ break;
2996
+ }
2997
+ case GRAPH_CACHE_TYPE.NodeLabel: {
2998
+ this.cachedNodeLabel.length = 0;
2999
+ break;
3000
+ }
3001
+ case GRAPH_CACHE_TYPE.NodeOptions: {
3002
+ this.nodeOptionsCache.length = 0;
3003
+ break;
3004
+ }
3005
+ case GRAPH_CACHE_TYPE.LinkOptions: {
3006
+ this.linkOptionsCache.length = 0;
3007
+ break;
3008
+ }
3009
+ }
2811
3010
  }
2812
3011
  }
2813
- }
2814
- tick() {
2815
- if (!this.simulationWorking && !this.highlightWorking)
3012
+ updateNodeCache.call(this);
3013
+ updateLinkCache.call(this);
3014
+ };
3015
+ tick = () => {
3016
+ if (!this.simulationWorking)
2816
3017
  this.draw();
2817
- }
2818
- restart(alpha) {
2819
- if (this.simulation)
3018
+ };
3019
+ restart = (alpha, options) => {
3020
+ if (!this.simulation)
3021
+ return;
3022
+ const settings = {
3023
+ precompute: options?.precompute ?? this.forceSettings.precompute,
3024
+ precomputeMaxTimeMs: options?.precomputeMaxTimeMs ?? this.forceSettings?.precomputeMaxTimeMs,
3025
+ precomputeMaxTicks: options?.precomputeMaxTicks ?? this.forceSettings?.precomputeMaxTicks ?? 300,
3026
+ precomputeDisableForcesAfter: options?.precomputeDisableForcesAfter ??
3027
+ this.forceSettings?.precomputeDisableForcesAfter ??
3028
+ false,
3029
+ };
3030
+ if (!settings.precompute) {
2820
3031
  this.simulation.alpha(alpha ?? 1).restart();
2821
- }
2822
- start() {
3032
+ return;
3033
+ }
3034
+ if (settings.precomputeDisableForcesAfter) {
3035
+ this.forceSettings = forceSettingsGetter({ forces: true }, this.forceSettings);
3036
+ }
3037
+ initSimulationForces.call(this);
3038
+ this.simulation.stop();
3039
+ this.simulation.alpha(alpha ?? 1);
3040
+ const startTime = performance.now();
3041
+ let ticks = 0;
3042
+ while (performance.now() - startTime < settings.precomputeMaxTimeMs &&
3043
+ ticks < settings.precomputeMaxTicks) {
3044
+ this.simulation.tick(1);
3045
+ ticks++;
3046
+ if (this.simulation.alpha() <= this.simulation.alphaMin())
3047
+ break;
3048
+ }
3049
+ if (settings.precomputeDisableForcesAfter) {
3050
+ this.forceSettings = forceSettingsGetter({ forces: false }, this.forceSettings);
3051
+ }
3052
+ initSimulationForces.call(this);
3053
+ this.simulation.restart();
3054
+ this.tick();
3055
+ };
3056
+ start = () => {
2823
3057
  if (this.simulation)
2824
3058
  this.simulation.alpha(1).restart();
2825
3059
  if (this.container)
2826
3060
  this.container.style.display = "block";
2827
- }
2828
- stop() {
3061
+ };
3062
+ stop = () => {
2829
3063
  if (this.simulation)
2830
3064
  this.simulation.stop();
2831
3065
  if (this.container)
2832
3066
  this.container.style.display = "none";
2833
- }
2834
- create() {
3067
+ };
3068
+ create = () => {
2835
3069
  this.init();
2836
- }
2837
- destroy() {
3070
+ };
3071
+ destroy = () => {
2838
3072
  if (this.simulation) {
2839
3073
  this.simulation.stop();
2840
3074
  this.simulation = undefined;
@@ -2842,39 +3076,51 @@ class GraphCanvas {
2842
3076
  this.clearHTMLElements();
2843
3077
  this.clearState();
2844
3078
  this.clearCache(true);
2845
- }
2846
- clearHTMLElements() {
3079
+ };
3080
+ clearHTMLElements = () => {
2847
3081
  this.root.replaceChildren();
2848
3082
  this.area = undefined;
2849
3083
  this.context = undefined;
2850
3084
  this.container = undefined;
2851
3085
  this.eventAbortController.abort();
2852
3086
  this.eventAbortController = new AbortController();
2853
- }
2854
- updateSimulation() {
3087
+ };
3088
+ updateSimulation = () => {
2855
3089
  if (this.simulation) {
2856
3090
  initSimulationForces.call(this);
2857
- this.simulation.alpha(1);
2858
- this.simulation.restart();
3091
+ this.restart(1);
2859
3092
  }
2860
- }
2861
- clearState() {
3093
+ };
3094
+ clearState = () => {
2862
3095
  this.isDragging = false;
2863
3096
  this.highlightedNode = null;
2864
3097
  this.highlightedLink = null;
2865
3098
  this.highlightedNeighbors = null;
2866
3099
  this.highlightProgress = 0;
2867
- this.highlightWorking = false;
2868
- }
2869
- updateData(alpha = 0.5, clearCache = true) {
3100
+ this.highlightStart = null;
3101
+ this.highlightPositive = false;
3102
+ };
3103
+ init = () => {
3104
+ initArea.call(this);
3105
+ updateNodeCache.call(this);
3106
+ updateLinkCache.call(this);
3107
+ initSimulation.call(this);
3108
+ this.restart(1);
3109
+ initDnd.call(this);
3110
+ initZoom.call(this);
3111
+ initResize.call(this);
3112
+ initPointer.call(this);
3113
+ updateNodeCache.call(this);
3114
+ updateLinkCache.call(this);
3115
+ this.tick();
3116
+ };
3117
+ updateData = (alpha = 0.5, clearCache = true) => {
2870
3118
  if (clearCache) {
2871
3119
  this.clearCache(clearCache);
2872
3120
  }
2873
3121
  if (this.simulation) {
2874
3122
  initCollideForce.call(this, false);
2875
- this.simulation
2876
- .nodes(this.nodes)
2877
- .force("link", d3Force.forceLink(this.links)
3123
+ this.simulation.nodes(this.nodes).force("link", d3Force.forceLink(this.links)
2878
3124
  .id(this.nodeSettings.idGetter.bind(this))
2879
3125
  .distance(this.forceSettings.forces && this.forceSettings.linkForce
2880
3126
  ? this.forceSettings.linkDistance
@@ -2884,21 +3130,95 @@ class GraphCanvas {
2884
3130
  : 0)
2885
3131
  .iterations(this.forceSettings.forces && this.forceSettings.linkForce
2886
3132
  ? this.forceSettings.linkIterations
2887
- : 0))
2888
- .alpha(alpha)
2889
- .restart();
3133
+ : 0));
3134
+ this.restart(alpha);
3135
+ initZoom.call(this, this.areaTransform);
2890
3136
  }
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
- }
3137
+ };
3138
+ animateHighlight = (node, link, baseDuration = this.highlightSettings.highlightDuration) => {
3139
+ let positive = true;
3140
+ if (node && (this.highlightedNode !== node || !this.highlightPositive)) {
3141
+ this.highlightedNode = node;
3142
+ this.highlightedNeighbors = new Set(node.neighbors);
3143
+ this.particles = [];
3144
+ this.highlightedLink = null;
3145
+ this.highlightStart = performance.now();
3146
+ }
3147
+ else if (link && (this.highlightedLink !== link || !this.highlightPositive)) {
3148
+ const { sourceId, targetId } = extractLinkPointIds(link);
3149
+ this.highlightProgress = 0;
3150
+ this.highlightedLink = link;
3151
+ this.highlightedNeighbors = new Set([sourceId, targetId]);
3152
+ this.particles = [];
3153
+ this.highlightedNode = null;
3154
+ this.highlightStart = performance.now();
3155
+ }
3156
+ else if (!node && !link && this.highlightPositive) {
3157
+ positive = false;
3158
+ }
3159
+ else {
3160
+ return;
3161
+ }
3162
+ if (this.highlightController) {
3163
+ this.highlightController.abort();
3164
+ }
3165
+ const controller = new AbortController();
3166
+ this.highlightPositive = positive;
3167
+ this.highlightController = controller;
3168
+ const startTime = performance.now();
3169
+ const startProgress = this.highlightProgress;
3170
+ const targetProgress = positive ? 1 : 0;
3171
+ const delta = targetProgress - startProgress;
3172
+ const duration = baseDuration * Math.abs(delta);
3173
+ const animate = () => {
3174
+ if (controller.signal.aborted)
3175
+ return;
3176
+ const elapsed = performance.now() - startTime;
3177
+ const t = Math.min(elapsed / duration, 1);
3178
+ const current = startProgress + delta * t;
3179
+ const eased = current < 0.5 ? 4 * current * current * current : 1 - (-2 * current + 2) ** 3 / 2;
3180
+ this.highlightProgress = eased;
3181
+ if (t < 1 || positive) {
3182
+ requestAnimationFrame(animate);
3183
+ this.draw();
3184
+ }
3185
+ else {
3186
+ this.highlightedNode = null;
3187
+ this.highlightedLink = null;
3188
+ this.highlightedNeighbors = null;
3189
+ this.highlightStart = null;
3190
+ this.particles = [];
3191
+ this.tick();
3192
+ }
3193
+ };
3194
+ requestAnimationFrame(animate);
3195
+ };
3196
+ animateZoom = (area, target, start, duration) => {
3197
+ this._zoomAnimating = true;
3198
+ const startTime = performance.now();
3199
+ const animate = () => {
3200
+ const elapsed = performance.now() - startTime;
3201
+ const t = Math.min(elapsed / duration, 1);
3202
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
3203
+ const x = start.x + (target.x - start.x) * eased;
3204
+ const y = start.y + (target.y - start.y) * eased;
3205
+ const k = start.k + (target.k - start.k) * eased;
3206
+ this.areaTransform = new d3Zoom.ZoomTransform(k, x, y);
3207
+ d3Zoom.zoom().transform(d3Selection.select(area), this.areaTransform);
3208
+ updateLinkCache.call(this);
3209
+ updateNodeCache.call(this);
3210
+ this.tick();
3211
+ if (t < 1) {
3212
+ requestAnimationFrame(animate);
3213
+ }
3214
+ else {
3215
+ this._zoomAnimating = false;
3216
+ this.clearCache(true);
3217
+ this.tick();
3218
+ }
3219
+ };
3220
+ requestAnimationFrame(animate);
3221
+ };
2902
3222
  }
2903
3223
 
2904
3224
  const FORCE_CONTROLS = [
@@ -3059,22 +3379,13 @@ const HIGHLIGHT_COMMON_CONTROLS = [
3059
3379
  label: "Расширение границы связи для курсора",
3060
3380
  },
3061
3381
  {
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,
3382
+ id: "highlightDuration",
3383
+ initialValue: HIGHLIGHT_SETTINGS.highlightDuration,
3384
+ max: 1000,
3385
+ min: 10,
3075
3386
  step: 1,
3076
3387
  type: "range",
3077
- label: "Скорость применения анимации в кадрах",
3388
+ label: "Скорость анимации",
3078
3389
  },
3079
3390
  ];
3080
3391
  const HIGHLIGHT_BY_NODE_FOR_NODE_CONTROLS = [
@@ -3547,19 +3858,22 @@ const LINK_SETTINGS_CONTROLS = [
3547
3858
  initialValue: LINK_SETTINGS.particles,
3548
3859
  },
3549
3860
  {
3550
- id: "particleFlexSpeed",
3551
- type: "checkbox",
3552
- label: "Гибкий расчет скорости частицы",
3553
- initialValue: LINK_SETTINGS.particleFlexSpeed,
3861
+ id: "particleSpeedByDistance",
3862
+ initialValue: LINK_SETTINGS.particleSpeedByDistance,
3863
+ max: 500,
3864
+ min: 1,
3865
+ step: 1,
3866
+ type: "range",
3867
+ label: "Скорость частицы на один пиксель",
3554
3868
  },
3555
3869
  {
3556
- id: "particleFlexSpeedCoefficient",
3557
- initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
3558
- max: 10,
3559
- min: 0.1,
3560
- step: 0.1,
3870
+ id: "particleCountByDistance",
3871
+ initialValue: LINK_SETTINGS.particleCountByDistance,
3872
+ max: 250,
3873
+ min: 1,
3874
+ step: 1,
3561
3875
  type: "range",
3562
- label: "Коэффициент скорости частицы относительно расстояния",
3876
+ label: "Количество пикселей для появления частицы",
3563
3877
  },
3564
3878
  ];
3565
3879
  const LINK_OPTIONS_LINK_CONTROLS = [
@@ -3654,24 +3968,6 @@ const LINK_OPTIONS_PARTICLE_CONTROLS = [
3654
3968
  label: "Радиус",
3655
3969
  initialValue: LINK_OPTIONS.particleRadius,
3656
3970
  },
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
3971
  {
3676
3972
  id: "particleBorderColor",
3677
3973
  type: "color",
@@ -4105,10 +4401,19 @@ exports.NODE_OPTIONS_NODE_CONTROLS = NODE_OPTIONS_NODE_CONTROLS;
4105
4401
  exports.NODE_OPTIONS_TEXT_CONTROLS = NODE_OPTIONS_TEXT_CONTROLS;
4106
4402
  exports.NODE_SETTINGS = NODE_SETTINGS;
4107
4403
  exports.NODE_SETTINGS_CONTROLS = NODE_SETTINGS_CONTROLS;
4404
+ exports.PERFOMANCE_NODE_OPTIONS = PERFOMANCE_NODE_OPTIONS;
4405
+ exports.PERFORMANCE_FORCE_SETTINGS = PERFORMANCE_FORCE_SETTINGS;
4406
+ exports.PERFORMANCE_GRAPH_SETTINGS = PERFORMANCE_GRAPH_SETTINGS;
4407
+ exports.PERFORMANCE_HIGHLIGHT_SETTINGS = PERFORMANCE_HIGHLIGHT_SETTINGS;
4408
+ exports.PERFORMANCE_LINK_OPTIONS = PERFORMANCE_LINK_OPTIONS;
4409
+ exports.PERFORMANCE_LINK_SETTINGS = PERFORMANCE_LINK_SETTINGS;
4410
+ exports.PERFORMANCE_NODE_SETTINGS = PERFORMANCE_NODE_SETTINGS;
4108
4411
  exports.animationByProgress = animationByProgress;
4412
+ exports.approximateQuadraticBezierLength = approximateQuadraticBezierLength;
4109
4413
  exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
4110
4414
  exports.colorGetter = colorGetter;
4111
4415
  exports.colorToRgb = colorToRgb;
4416
+ exports.computeGraphBounds = computeGraphBounds;
4112
4417
  exports.dragPlaceCoefficientGetter = dragPlaceCoefficientGetter;
4113
4418
  exports.drawText = drawText;
4114
4419
  exports.extractLinkPointIds = extractLinkPointIds;
@@ -4120,6 +4425,7 @@ exports.getDrawNode = getDrawNode;
4120
4425
  exports.getParticlePosition = getParticlePosition;
4121
4426
  exports.graphSettingsGetter = graphSettingsGetter;
4122
4427
  exports.highlightSettingsGetter = highlightSettingsGetter;
4428
+ exports.isEmptyObject = isEmptyObject;
4123
4429
  exports.isNodeVisible = isNodeVisible;
4124
4430
  exports.isOverlapsNode = isOverlapsNode;
4125
4431
  exports.linkByPointerGetter = linkByPointerGetter;