@krainovsd/graph 0.8.4 → 0.10.0-rc1

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 (55) hide show
  1. package/lib/cjs/index.cjs +2819 -1574
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/esm/index.js +14 -4
  4. package/lib/esm/index.js.map +1 -1
  5. package/lib/esm/lib/draw-time.js +5 -1
  6. package/lib/esm/lib/draw-time.js.map +1 -1
  7. package/lib/esm/lib/get-controls-info.js +525 -151
  8. package/lib/esm/lib/get-controls-info.js.map +1 -1
  9. package/lib/esm/module/GraphCanvas/GraphCanvas.js +42 -746
  10. package/lib/esm/module/GraphCanvas/GraphCanvas.js.map +1 -1
  11. package/lib/esm/module/GraphCanvas/constants/settings.js +83 -39
  12. package/lib/esm/module/GraphCanvas/constants/settings.js.map +1 -1
  13. package/lib/esm/module/GraphCanvas/lib/settings/node-settings-getter.js +10 -1
  14. package/lib/esm/module/GraphCanvas/lib/settings/node-settings-getter.js.map +1 -1
  15. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-node.js +270 -0
  16. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-node.js.map +1 -0
  17. package/lib/esm/module/GraphCanvas/lib/utils/drag-place-coefficient-getter.js +2 -2
  18. package/lib/esm/module/GraphCanvas/lib/utils/drag-place-coefficient-getter.js.map +1 -1
  19. package/lib/esm/module/GraphCanvas/lib/utils/get-particle-position.js +19 -4
  20. package/lib/esm/module/GraphCanvas/lib/utils/get-particle-position.js.map +1 -1
  21. package/lib/esm/module/GraphCanvas/lib/utils/is-node-visible.js +29 -4
  22. package/lib/esm/module/GraphCanvas/lib/utils/is-node-visible.js.map +1 -1
  23. package/lib/esm/module/GraphCanvas/lib/utils/is-overlaps-node.js +53 -4
  24. package/lib/esm/module/GraphCanvas/lib/utils/is-overlaps-node.js.map +1 -1
  25. package/lib/esm/module/GraphCanvas/lib/utils/link-by-pointer-getter.js +55 -0
  26. package/lib/esm/module/GraphCanvas/lib/utils/link-by-pointer-getter.js.map +1 -0
  27. package/lib/esm/module/GraphCanvas/lib/utils/node-by-pointer-getter.js +3 -13
  28. package/lib/esm/module/GraphCanvas/lib/utils/node-by-pointer-getter.js.map +1 -1
  29. package/lib/esm/module/GraphCanvas/lib/utils/pointer-getter.js +8 -2
  30. package/lib/esm/module/GraphCanvas/lib/utils/pointer-getter.js.map +1 -1
  31. package/lib/esm/module/GraphCanvas/slices/draw-links.js +207 -0
  32. package/lib/esm/module/GraphCanvas/slices/draw-links.js.map +1 -0
  33. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js +342 -0
  34. package/lib/esm/module/GraphCanvas/slices/draw-nodes.js.map +1 -0
  35. package/lib/esm/module/GraphCanvas/{lib/utils → slices}/draw-text.js +23 -6
  36. package/lib/esm/module/GraphCanvas/slices/draw-text.js.map +1 -0
  37. package/lib/esm/module/GraphCanvas/slices/init-area.js +31 -0
  38. package/lib/esm/module/GraphCanvas/slices/init-area.js.map +1 -0
  39. package/lib/esm/module/GraphCanvas/slices/init-dnd.js +62 -0
  40. package/lib/esm/module/GraphCanvas/slices/init-dnd.js.map +1 -0
  41. package/lib/esm/module/GraphCanvas/slices/init-draw.js +84 -0
  42. package/lib/esm/module/GraphCanvas/slices/init-draw.js.map +1 -0
  43. package/lib/esm/module/GraphCanvas/slices/init-pointer.js +211 -0
  44. package/lib/esm/module/GraphCanvas/slices/init-pointer.js.map +1 -0
  45. package/lib/esm/module/GraphCanvas/slices/init-resize.js +28 -0
  46. package/lib/esm/module/GraphCanvas/slices/init-resize.js.map +1 -0
  47. package/lib/esm/module/GraphCanvas/slices/init-simulation.js +172 -0
  48. package/lib/esm/module/GraphCanvas/slices/init-simulation.js.map +1 -0
  49. package/lib/esm/module/GraphCanvas/slices/init-zoom.js +36 -0
  50. package/lib/esm/module/GraphCanvas/slices/init-zoom.js.map +1 -0
  51. package/lib/index.d.ts +192 -114
  52. package/package.json +1 -1
  53. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-radius.js +0 -20
  54. package/lib/esm/module/GraphCanvas/lib/utils/calculate-link-position-by-radius.js.map +0 -1
  55. package/lib/esm/module/GraphCanvas/lib/utils/draw-text.js.map +0 -1
package/lib/cjs/index.cjs CHANGED
@@ -2,12 +2,203 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
+ const d3Force = require('d3-force');
6
+ const d3Zoom = require('d3-zoom');
5
7
  const jsHelpers = require('@krainovsd/js-helpers');
6
8
  const d3Array = require('d3-array');
7
- const d3Drag = require('d3-drag');
8
- const d3Force = require('d3-force');
9
9
  const d3Selection = require('d3-selection');
10
- const d3Zoom = require('d3-zoom');
10
+ const d3Drag = require('d3-drag');
11
+ const lodash = require('lodash');
12
+
13
+ const FORCE_SETTINGS = {
14
+ forces: true,
15
+ centerForce: true,
16
+ chargeForce: true,
17
+ linkForce: true,
18
+ xForce: true,
19
+ yForce: true,
20
+ collideForce: true,
21
+ centerPosition: {},
22
+ centerStrength: 1,
23
+ collideStrength: 0.1,
24
+ collideAdditionalRadius: 4,
25
+ collideIterations: 2,
26
+ collideOffMax: { links: 0, nodes: 0 },
27
+ chargeStrength: -40,
28
+ chargeDistanceMax: Infinity,
29
+ chargeDistanceMin: 1,
30
+ xPosition: 0,
31
+ xStrength: 0.1,
32
+ yPosition: 0,
33
+ yStrength: 0.1,
34
+ linkDistance: 30,
35
+ linkIterations: 1,
36
+ linkStrength: 1,
37
+ collideRadius: null,
38
+ };
39
+ const GRAPH_SETTINGS = {
40
+ zoomExtent: [0.3, 10],
41
+ translateExtent: [[], []],
42
+ translateExtentEnable: true,
43
+ translateExtentCoefficient: [3, 3],
44
+ highlightUpFrames: 5,
45
+ highlightDownFrames: 5,
46
+ dragPlaceCoefficient: dragPlaceCoefficientGetter,
47
+ zoomInitial: null,
48
+ showDrawTime: true,
49
+ showDrawTimeEveryTick: false,
50
+ };
51
+ const NODE_SETTINGS = {
52
+ cache: true,
53
+ highlightByNodeOnlyRoot: true,
54
+ highlightByHoverNode: true,
55
+ nodeRadiusFlexible: true,
56
+ nodeSizeFlexible: true,
57
+ textNodeDebug: false,
58
+ highlightByNodeNodeFading: true,
59
+ highlightByNodeNodeColor: false,
60
+ highlightByNodeNodeSizing: true,
61
+ highlightByNodeTextFading: true,
62
+ highlightByNodeTextSizing: true,
63
+ highlightByLinkNodeFading: false,
64
+ highlightByLinkNodeColor: false,
65
+ highlightByLinkNodeSizing: true,
66
+ highlightByLinkTextFading: false,
67
+ highlightByLinkTextSizing: true,
68
+ nodeRadiusCoefficient: 5,
69
+ nodeRadiusFactor: 1,
70
+ nodeSizeCoefficient: 5,
71
+ nodeSizeFactor: 0.1,
72
+ highlightByNodeNodeSizingAdditional: 0.5,
73
+ highlightByLinkNodeSizingAdditionalCoefficient: 0.35,
74
+ highlightByNodeNodeColorFadingMin: 0.15,
75
+ highlightByNodeTextShiftXAdditional: 0,
76
+ highlightByNodeTextShiftYAdditional: 2,
77
+ highlightByNodeTextSizingAdditional: 1,
78
+ highlightByNodeTextWeightAdditional: 0,
79
+ highlightByNodeTextWidthAdditional: 10,
80
+ highlightByNodeNodeFadingMin: 0.21,
81
+ highlightByNodeTextFadingMin: 0.21,
82
+ highlightByLinkNodeSizingAdditional: 0.5,
83
+ highlightByNodeNodeSizingAdditionalCoefficient: 0.35,
84
+ highlightByLinkNodeColorFadingMin: 0.15,
85
+ highlightByLinkTextShiftXAdditional: 0,
86
+ highlightByLinkTextShiftYAdditional: 2,
87
+ highlightByLinkTextSizingAdditional: 1,
88
+ highlightByLinkTextWeightAdditional: 0,
89
+ highlightByLinkTextWidthAdditional: 10,
90
+ highlightByLinkNodeFadingMin: 0.21,
91
+ highlightByLinkTextFadingMin: 0.21,
92
+ };
93
+ const NODE_OPTIONS = {
94
+ shape: "text",
95
+ height: 10,
96
+ width: 15,
97
+ borderRadius: 0,
98
+ radius: 4,
99
+ alpha: 1,
100
+ textNodeXPadding: 5,
101
+ textNodeYPadding: 3,
102
+ borderWidth: 0.1,
103
+ borderColor: "#000000FF",
104
+ textAlpha: 1,
105
+ textWidth: 20,
106
+ textShiftX: 0,
107
+ textFont: "Arial",
108
+ textAlign: "center",
109
+ textColor: "#333",
110
+ textStyle: "normal",
111
+ textWeight: 500,
112
+ textGap: 0,
113
+ };
114
+ const LINK_SETTINGS = {
115
+ cache: true,
116
+ particles: true,
117
+ particleFlexSpeed: true,
118
+ pretty: true,
119
+ arrow: true,
120
+ highlightByHoverLink: true,
121
+ arrowByHighlight: true,
122
+ highlightByNodeLinkFading: true,
123
+ highlightByNodeArrowFading: true,
124
+ highlightByLinkLinkFading: false,
125
+ highlightByLinkArrowFading: false,
126
+ particleFlexSpeedCoefficient: 4,
127
+ hoverLinkThreshold: 2,
128
+ highlightByNodeLinkFadingMin: 0.21,
129
+ highlightByNodeArrowFadingMin: 0.21,
130
+ highlightByLinkArrowFadingMin: 0.21,
131
+ highlightByLinkLinkFadingMin: 0.21,
132
+ };
133
+ const LINK_OPTIONS = {
134
+ alpha: 1,
135
+ arrowAlpha: 1,
136
+ arrowSize: 2,
137
+ arrowBorderColor: "#000000FF",
138
+ arrowBorderWidth: 0.1,
139
+ particleAlpha: 1,
140
+ particleColor: "#000000FF",
141
+ particleBorderColor: "#000000FF",
142
+ particleBorderWidth: 0.1,
143
+ particleCount: 2,
144
+ particleRadius: 0.5,
145
+ particleSteps: 60,
146
+ };
147
+ const COMMON_SETTINGS = {
148
+ linkColorZoomFar: "#999",
149
+ linkColorZoomNear: "#000000FF",
150
+ linkWidthZoomFar: 1,
151
+ linkWidthZoomNear: 0.1,
152
+ linkWidthZoomBorder: 1,
153
+ linkColorZoomBorder: 1,
154
+ nodeTextScaleMin: 1.5,
155
+ nodeTextScaleMax: 20,
156
+ nodeTextSizeMin: 1.5,
157
+ nodeTextSizeMax: 3.5,
158
+ nodeTextShiftYMin: 2.5,
159
+ nodeTextShiftYMax: 4,
160
+ nodeTextChangeStepCount: 200,
161
+ nodeRadius: 5,
162
+ nodeSize: 5,
163
+ };
164
+
165
+ function forceSettingsGetter(settings, prevSettings) {
166
+ return {
167
+ ...(prevSettings ?? FORCE_SETTINGS),
168
+ ...settings,
169
+ };
170
+ }
171
+
172
+ function graphSettingsGetter(settings, prevSettings) {
173
+ return {
174
+ ...(prevSettings ?? GRAPH_SETTINGS),
175
+ ...settings,
176
+ };
177
+ }
178
+
179
+ function linkSettingsGetter(settings, prevSettings) {
180
+ return { ...(prevSettings ?? LINK_SETTINGS), ...settings };
181
+ }
182
+ function linkOptionsGetter(_, __, ___, state) {
183
+ return {
184
+ ...LINK_OPTIONS,
185
+ drawExtraLink: null,
186
+ drawLink: null,
187
+ color: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkColorZoomBorder
188
+ ? COMMON_SETTINGS.linkColorZoomNear
189
+ : COMMON_SETTINGS.linkColorZoomFar,
190
+ arrowColor: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkColorZoomBorder
191
+ ? COMMON_SETTINGS.linkColorZoomNear
192
+ : COMMON_SETTINGS.linkColorZoomFar,
193
+ width: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkWidthZoomBorder
194
+ ? COMMON_SETTINGS.linkWidthZoomNear
195
+ : COMMON_SETTINGS.linkWidthZoomFar,
196
+ };
197
+ }
198
+
199
+ function listenersGetter(settings) {
200
+ return settings ?? {};
201
+ }
11
202
 
12
203
  function checkType(value, condition) {
13
204
  return condition;
@@ -87,123 +278,1087 @@ function rgbAnimationByProgress(start, end, progress) {
87
278
  };
88
279
  }
89
280
 
90
- function forceSettingsGetter(settings, prevSettings) {
91
- return {
92
- ...(prevSettings ?? FORCE_SETTINGS),
93
- ...settings,
94
- };
95
- }
96
-
97
- function graphSettingsGetter(settings, prevSettings) {
98
- return {
99
- ...(prevSettings ?? GRAPH_SETTINGS),
100
- ...settings,
101
- };
102
- }
103
-
104
- function linkSettingsGetter(settings, prevSettings) {
105
- return { ...(prevSettings ?? LINK_SETTINGS), ...settings };
281
+ function getForceControls(keys) {
282
+ const FORCE_CONTROLS = [
283
+ {
284
+ id: "forces",
285
+ initialValue: FORCE_SETTINGS.forces,
286
+ label: "Физика",
287
+ type: "checkbox",
288
+ },
289
+ {
290
+ id: "collideForce",
291
+ type: "checkbox",
292
+ label: "Сила отталкивания",
293
+ initialValue: FORCE_SETTINGS.collideForce,
294
+ },
295
+ {
296
+ id: "centerForce",
297
+ type: "checkbox",
298
+ label: "Сила центрирования",
299
+ initialValue: FORCE_SETTINGS.centerForce,
300
+ },
301
+ {
302
+ id: "chargeForce",
303
+ type: "checkbox",
304
+ label: "Сила дистанции",
305
+ initialValue: FORCE_SETTINGS.chargeForce,
306
+ },
307
+ {
308
+ id: "linkForce",
309
+ type: "checkbox",
310
+ label: "Сила связей",
311
+ initialValue: FORCE_SETTINGS.linkForce,
312
+ },
313
+ {
314
+ id: "xForce",
315
+ type: "checkbox",
316
+ label: "Сила X",
317
+ initialValue: FORCE_SETTINGS.xForce,
318
+ },
319
+ {
320
+ id: "yForce",
321
+ type: "checkbox",
322
+ label: "Сила Y",
323
+ initialValue: FORCE_SETTINGS.yForce,
324
+ },
325
+ {
326
+ id: "centerStrength",
327
+ initialValue: FORCE_SETTINGS.centerStrength,
328
+ label: "Граница центра",
329
+ max: 1,
330
+ min: 0,
331
+ step: 0.01,
332
+ type: "range",
333
+ },
334
+ {
335
+ id: "collideStrength",
336
+ initialValue: FORCE_SETTINGS.collideStrength,
337
+ label: "Сила отталкивания",
338
+ max: 1,
339
+ min: 0,
340
+ step: 0.01,
341
+ type: "range",
342
+ },
343
+ {
344
+ id: "collideAdditionalRadius",
345
+ initialValue: FORCE_SETTINGS.collideAdditionalRadius,
346
+ label: "Радиус отталкивания",
347
+ max: 300,
348
+ min: 0,
349
+ step: 0.1,
350
+ type: "range",
351
+ },
352
+ {
353
+ id: "collideIterations",
354
+ initialValue: FORCE_SETTINGS.collideIterations,
355
+ label: "Итерации отталкивания",
356
+ max: 10,
357
+ min: 0,
358
+ step: 1,
359
+ type: "range",
360
+ },
361
+ {
362
+ id: "chargeStrength",
363
+ initialValue: FORCE_SETTINGS.chargeStrength ,
364
+ label: "Дистанция",
365
+ max: 0,
366
+ min: -300,
367
+ step: 0.1,
368
+ type: "range",
369
+ },
370
+ {
371
+ type: "range",
372
+ id: "xPosition",
373
+ min: 0,
374
+ max: 2000,
375
+ step: 1,
376
+ label: "Координата притяжения X",
377
+ initialValue: FORCE_SETTINGS.xPosition ,
378
+ },
379
+ {
380
+ type: "range",
381
+ id: "yPosition",
382
+ min: 0,
383
+ max: 2000,
384
+ step: 1,
385
+ label: "Координата притяжения Y",
386
+ initialValue: FORCE_SETTINGS.yPosition ,
387
+ },
388
+ {
389
+ type: "range",
390
+ id: "xStrength",
391
+ min: 0,
392
+ max: 1,
393
+ step: 0.01,
394
+ label: "Гравитация X",
395
+ initialValue: FORCE_SETTINGS.xStrength ,
396
+ },
397
+ {
398
+ type: "range",
399
+ id: "yStrength",
400
+ min: 0,
401
+ max: 1,
402
+ step: 0.01,
403
+ label: "Гравитация Y",
404
+ initialValue: FORCE_SETTINGS.yStrength ,
405
+ },
406
+ {
407
+ type: "range",
408
+ id: "linkStrength",
409
+ min: 0,
410
+ max: 1,
411
+ step: 0.01,
412
+ label: "Натяжение связей",
413
+ initialValue: FORCE_SETTINGS.linkStrength ,
414
+ },
415
+ {
416
+ type: "range",
417
+ id: "linkDistance",
418
+ min: 0,
419
+ max: 300,
420
+ step: 0.1,
421
+ label: "Расстояние связей",
422
+ initialValue: FORCE_SETTINGS.linkDistance ,
423
+ },
424
+ ];
425
+ return keys ? FORCE_CONTROLS.filter((control) => keys.includes(control.id)) : FORCE_CONTROLS;
106
426
  }
107
- function linkOptionsGetter(_, __, ___, state) {
108
- return {
109
- ...LINK_OPTIONS,
110
- drawExtraLink: null,
111
- drawLink: null,
112
- color: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkColorZoomBorder
113
- ? COMMON_SETTINGS.linkColorZoomNear
114
- : COMMON_SETTINGS.linkColorZoomFar,
115
- arrowColor: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkColorZoomBorder
116
- ? COMMON_SETTINGS.linkColorZoomNear
117
- : COMMON_SETTINGS.linkColorZoomFar,
118
- width: state?.areaTransform && state?.areaTransform.k > COMMON_SETTINGS.linkWidthZoomBorder
119
- ? COMMON_SETTINGS.linkWidthZoomNear
120
- : COMMON_SETTINGS.linkWidthZoomFar,
121
- };
122
- }
123
-
124
- function listenersGetter(settings) {
125
- return settings ?? {};
126
- }
127
-
128
- function nodeSettingsGetter(settings, prevNodeSettings) {
129
- return {
130
- ...(prevNodeSettings ?? NODE_SETTINGS),
131
- idGetter: nodeIdGetter,
132
- ...settings,
133
- };
427
+ function getGraphControls(keys) {
428
+ const GRAPH_CONTROLS = [
429
+ {
430
+ id: "highlightDownFrames",
431
+ initialValue: GRAPH_SETTINGS.highlightDownFrames,
432
+ max: 60,
433
+ min: 1,
434
+ step: 1,
435
+ type: "range",
436
+ label: "Скорость отмены анимации в кадрах",
437
+ },
438
+ {
439
+ id: "highlightUpFrames",
440
+ initialValue: GRAPH_SETTINGS.highlightUpFrames,
441
+ max: 60,
442
+ min: 1,
443
+ step: 1,
444
+ type: "range",
445
+ label: "Скорость применения анимации в кадрах",
446
+ },
447
+ ];
448
+ return keys ? GRAPH_CONTROLS.filter((control) => keys.includes(control.id)) : GRAPH_CONTROLS;
134
449
  }
135
- const color = colorGetter();
136
- function nodeOptionsGetter(node, _, __, state) {
137
- const { textShiftY, textSize } = nodeTextSizeGetter(state?.areaTransform);
138
- return {
139
- ...NODE_OPTIONS,
140
- nodeDraw: null,
141
- nodeExtraDraw: null,
142
- textDraw: null,
143
- textExtraDraw: null,
144
- color: color(String(node.group ?? "_DEFAULT")),
145
- textVisible: Boolean(state?.areaTransform && state.areaTransform.k > COMMON_SETTINGS.nodeTextScaleMin),
146
- text: node.name ?? node.id.toString(),
147
- textShiftY,
148
- textSize,
149
- };
450
+ function getNodeSettingControls(keys) {
451
+ const NODE_SETTINGS_CONTROLS = [
452
+ {
453
+ id: "cache",
454
+ type: "checkbox",
455
+ initialValue: NODE_SETTINGS.cache,
456
+ label: "Кеширование вычисляемых настроек",
457
+ },
458
+ {
459
+ id: "highlightByNodeOnlyRoot",
460
+ type: "checkbox",
461
+ initialValue: NODE_SETTINGS.highlightByNodeOnlyRoot,
462
+ label: "Дополнительная анимация только главное цели при анимации ноды",
463
+ },
464
+ {
465
+ id: "highlightByHoverNode",
466
+ type: "checkbox",
467
+ initialValue: NODE_SETTINGS.highlightByHoverNode,
468
+ label: "Анимации при наведении на ноду",
469
+ },
470
+ {
471
+ id: "nodeRadiusFlexible",
472
+ type: "checkbox",
473
+ initialValue: NODE_SETTINGS.nodeRadiusFlexible,
474
+ label: "Гибкий радиус ноды",
475
+ },
476
+ {
477
+ id: "nodeSizeFlexible",
478
+ type: "checkbox",
479
+ initialValue: NODE_SETTINGS.nodeSizeFlexible,
480
+ label: "Гибкий размер ноды",
481
+ },
482
+ {
483
+ id: "textNodeDebug",
484
+ type: "checkbox",
485
+ initialValue: NODE_SETTINGS.textNodeDebug,
486
+ label: "Отладка текстовой ноды",
487
+ },
488
+ {
489
+ id: "highlightByNodeNodeFading",
490
+ type: "checkbox",
491
+ initialValue: NODE_SETTINGS.highlightByNodeNodeFading,
492
+ label: "Затухание при анимации ноды",
493
+ },
494
+ {
495
+ id: "highlightByNodeNodeColor",
496
+ type: "checkbox",
497
+ initialValue: NODE_SETTINGS.highlightByNodeNodeColor,
498
+ label: "Затухание цвета при анимации ноды",
499
+ },
500
+ {
501
+ id: "highlightByNodeNodeSizing",
502
+ type: "checkbox",
503
+ initialValue: NODE_SETTINGS.highlightByNodeNodeSizing,
504
+ label: "Изменение размера при анимации ноды",
505
+ },
506
+ {
507
+ id: "highlightByNodeTextFading",
508
+ type: "checkbox",
509
+ initialValue: NODE_SETTINGS.highlightByNodeTextFading,
510
+ label: "Затухание текста при анимации ноды",
511
+ },
512
+ {
513
+ id: "highlightByNodeTextSizing",
514
+ type: "checkbox",
515
+ initialValue: NODE_SETTINGS.highlightByNodeTextSizing,
516
+ label: "Изменение размера текста при анимации ноды",
517
+ },
518
+ {
519
+ id: "highlightByLinkNodeFading",
520
+ type: "checkbox",
521
+ initialValue: NODE_SETTINGS.highlightByLinkNodeFading,
522
+ label: "Затухание при анимации связи",
523
+ },
524
+ {
525
+ id: "highlightByLinkNodeColor",
526
+ type: "checkbox",
527
+ initialValue: NODE_SETTINGS.highlightByLinkNodeColor,
528
+ label: "Затухание цвета при анимации связи",
529
+ },
530
+ {
531
+ id: "highlightByLinkNodeSizing",
532
+ type: "checkbox",
533
+ initialValue: NODE_SETTINGS.highlightByLinkNodeSizing,
534
+ label: "Изменение размера при анимации связи",
535
+ },
536
+ {
537
+ id: "highlightByLinkTextFading",
538
+ type: "checkbox",
539
+ initialValue: NODE_SETTINGS.highlightByLinkTextFading,
540
+ label: "Затухание текста при анимации связи",
541
+ },
542
+ {
543
+ id: "highlightByLinkTextSizing",
544
+ type: "checkbox",
545
+ initialValue: NODE_SETTINGS.highlightByLinkTextSizing,
546
+ label: "Изменение размера текста при анимации связи",
547
+ },
548
+ {
549
+ id: "nodeRadiusCoefficient",
550
+ initialValue: NODE_SETTINGS.nodeRadiusCoefficient,
551
+ max: 100,
552
+ min: 0.1,
553
+ step: 0.1,
554
+ type: "range",
555
+ label: "Количество связей для увеличения радиуса",
556
+ },
557
+ {
558
+ id: "nodeRadiusFactor",
559
+ initialValue: NODE_SETTINGS.nodeRadiusFactor,
560
+ max: 50,
561
+ min: 0.1,
562
+ step: 0.1,
563
+ type: "range",
564
+ label: "Коэффициент увеличения радиуса",
565
+ },
566
+ {
567
+ id: "nodeSizeCoefficient",
568
+ initialValue: NODE_SETTINGS.nodeSizeCoefficient,
569
+ max: 50,
570
+ min: 0.1,
571
+ step: 0.1,
572
+ type: "range",
573
+ label: "Количество связей для увеличения размера",
574
+ },
575
+ {
576
+ id: "nodeSizeFactor",
577
+ initialValue: NODE_SETTINGS.nodeSizeFactor,
578
+ max: 20,
579
+ min: 0.01,
580
+ step: 0.01,
581
+ type: "range",
582
+ label: "Коэффициент увеличения размера",
583
+ },
584
+ {
585
+ id: "highlightByNodeNodeSizingAdditional",
586
+ initialValue: NODE_SETTINGS.highlightByNodeNodeSizingAdditional,
587
+ max: 10,
588
+ min: 0.1,
589
+ step: 0.01,
590
+ type: "range",
591
+ label: "Дополнительный размер при анимации ноды",
592
+ },
593
+ {
594
+ id: "highlightByNodeNodeSizingAdditionalCoefficient",
595
+ initialValue: NODE_SETTINGS.highlightByNodeNodeSizingAdditionalCoefficient,
596
+ max: 10,
597
+ min: 0.01,
598
+ step: 0.01,
599
+ type: "range",
600
+ label: "Дополнительный размер квадрата при анимации ноды",
601
+ },
602
+ {
603
+ id: "highlightByNodeNodeColorFadingMin",
604
+ initialValue: NODE_SETTINGS.highlightByNodeNodeColorFadingMin,
605
+ max: 1,
606
+ min: 0.01,
607
+ step: 0.01,
608
+ type: "range",
609
+ label: "Граница затухания цвета при анимации ноды",
610
+ },
611
+ {
612
+ id: "highlightByNodeTextShiftXAdditional",
613
+ initialValue: NODE_SETTINGS.highlightByNodeTextShiftXAdditional,
614
+ max: 50,
615
+ min: 0,
616
+ step: 0.1,
617
+ type: "range",
618
+ label: "Смещение текста по X при анимации ноды",
619
+ },
620
+ {
621
+ id: "highlightByNodeTextShiftYAdditional",
622
+ initialValue: NODE_SETTINGS.highlightByNodeTextShiftYAdditional,
623
+ max: 50,
624
+ min: 0,
625
+ step: 0.1,
626
+ type: "range",
627
+ label: "Смещение текста по Y при анимации ноды",
628
+ },
629
+ {
630
+ id: "highlightByNodeTextSizingAdditional",
631
+ initialValue: NODE_SETTINGS.highlightByNodeTextSizingAdditional,
632
+ max: 10,
633
+ min: 0.1,
634
+ step: 0.1,
635
+ type: "range",
636
+ label: "Увеличение текста при анимации ноды",
637
+ },
638
+ {
639
+ id: "highlightByNodeTextWeightAdditional",
640
+ initialValue: NODE_SETTINGS.highlightByNodeTextWeightAdditional,
641
+ max: 1000,
642
+ min: 0,
643
+ step: 100,
644
+ type: "range",
645
+ label: "Увеличение жирности текста при анимации ноды",
646
+ },
647
+ {
648
+ id: "highlightByNodeTextWidthAdditional",
649
+ initialValue: NODE_SETTINGS.highlightByNodeTextWidthAdditional,
650
+ max: 100,
651
+ min: 0,
652
+ step: 0.1,
653
+ type: "range",
654
+ label: "Увеличение ширины текста при анимации ноды",
655
+ },
656
+ {
657
+ id: "highlightByNodeNodeFadingMin",
658
+ initialValue: NODE_SETTINGS.highlightByNodeNodeFadingMin,
659
+ max: 1,
660
+ min: 0,
661
+ step: 0.01,
662
+ type: "range",
663
+ label: "Граница затухания ноды при анимации ноды",
664
+ },
665
+ {
666
+ id: "highlightByNodeTextFadingMin",
667
+ initialValue: NODE_SETTINGS.highlightByNodeTextFadingMin,
668
+ max: 1,
669
+ min: 0,
670
+ step: 0.01,
671
+ type: "range",
672
+ label: "Граница затухания текста при анимации ноды",
673
+ },
674
+ {
675
+ id: "highlightByLinkNodeSizingAdditional",
676
+ initialValue: NODE_SETTINGS.highlightByLinkNodeSizingAdditional,
677
+ max: 10,
678
+ min: 0.1,
679
+ step: 0.01,
680
+ type: "range",
681
+ label: "Дополнительный размер при анимации связи",
682
+ },
683
+ {
684
+ id: "highlightByLinkNodeSizingAdditionalCoefficient",
685
+ initialValue: NODE_SETTINGS.highlightByLinkNodeSizingAdditionalCoefficient,
686
+ max: 10,
687
+ min: 0.01,
688
+ step: 0.01,
689
+ type: "range",
690
+ label: "Дополнительный размер квадрата при анимации связи",
691
+ },
692
+ {
693
+ id: "highlightByLinkNodeColorFadingMin",
694
+ initialValue: NODE_SETTINGS.highlightByLinkNodeColorFadingMin,
695
+ max: 1,
696
+ min: 0.01,
697
+ step: 0.01,
698
+ type: "range",
699
+ label: "Граница затухания цвета при анимации связи",
700
+ },
701
+ {
702
+ id: "highlightByLinkTextShiftXAdditional",
703
+ initialValue: NODE_SETTINGS.highlightByLinkTextShiftXAdditional,
704
+ max: 50,
705
+ min: 0,
706
+ step: 0.1,
707
+ type: "range",
708
+ label: "Смещение текста по X при анимации связи",
709
+ },
710
+ {
711
+ id: "highlightByLinkTextShiftYAdditional",
712
+ initialValue: NODE_SETTINGS.highlightByLinkTextShiftYAdditional,
713
+ max: 50,
714
+ min: 0,
715
+ step: 0.1,
716
+ type: "range",
717
+ label: "Смещение текста по Y при анимации связи",
718
+ },
719
+ {
720
+ id: "highlightByLinkTextSizingAdditional",
721
+ initialValue: NODE_SETTINGS.highlightByLinkTextSizingAdditional,
722
+ max: 10,
723
+ min: 0.1,
724
+ step: 0.1,
725
+ type: "range",
726
+ label: "Увеличение текста при анимации связи",
727
+ },
728
+ {
729
+ id: "highlightByLinkTextWeightAdditional",
730
+ initialValue: NODE_SETTINGS.highlightByLinkTextWeightAdditional,
731
+ max: 1000,
732
+ min: 0,
733
+ step: 100,
734
+ type: "range",
735
+ label: "Увеличение жирности текста при анимации связи",
736
+ },
737
+ {
738
+ id: "highlightByLinkTextWidthAdditional",
739
+ initialValue: NODE_SETTINGS.highlightByLinkTextWidthAdditional,
740
+ max: 100,
741
+ min: 0,
742
+ step: 0.1,
743
+ type: "range",
744
+ label: "Увеличение ширины текста при анимации связи",
745
+ },
746
+ {
747
+ id: "highlightByLinkNodeFadingMin",
748
+ initialValue: NODE_SETTINGS.highlightByLinkNodeFadingMin,
749
+ max: 1,
750
+ min: 0,
751
+ step: 0.01,
752
+ type: "range",
753
+ label: "Граница затухания ноды при анимации связи",
754
+ },
755
+ {
756
+ id: "highlightByLinkTextFadingMin",
757
+ initialValue: NODE_SETTINGS.highlightByLinkTextFadingMin,
758
+ max: 1,
759
+ min: 0,
760
+ step: 0.01,
761
+ type: "range",
762
+ label: "Граница затухания текста при анимации связи",
763
+ },
764
+ ];
765
+ return keys
766
+ ? NODE_SETTINGS_CONTROLS.filter((control) => keys.includes(control.id))
767
+ : NODE_SETTINGS_CONTROLS;
150
768
  }
151
- function nodeTextSizeGetter(transform) {
152
- let textSize = COMMON_SETTINGS.nodeTextSizeMax;
153
- let textShiftY = COMMON_SETTINGS.nodeTextShiftYMax;
154
- if (transform) {
155
- const scaleStepCoefficient = (COMMON_SETTINGS.nodeTextScaleMax - COMMON_SETTINGS.nodeTextScaleMin) /
156
- COMMON_SETTINGS.nodeTextChangeStepCount;
157
- const textStepCoefficient = (COMMON_SETTINGS.nodeTextSizeMax - COMMON_SETTINGS.nodeTextSizeMin) /
158
- COMMON_SETTINGS.nodeTextChangeStepCount;
159
- const shiftStepCoefficient = (COMMON_SETTINGS.nodeTextShiftYMax - COMMON_SETTINGS.nodeTextShiftYMin) /
160
- COMMON_SETTINGS.nodeTextChangeStepCount;
161
- if (transform.k >= COMMON_SETTINGS.nodeTextScaleMax) {
162
- textSize = COMMON_SETTINGS.nodeTextSizeMin;
163
- textShiftY = COMMON_SETTINGS.nodeTextShiftYMin;
164
- }
165
- else if (transform.k > COMMON_SETTINGS.nodeTextScaleMin) {
166
- const transformSteps = (transform.k - COMMON_SETTINGS.nodeTextScaleMin) / scaleStepCoefficient;
167
- textSize -= transformSteps * textStepCoefficient;
168
- textShiftY -= transformSteps * shiftStepCoefficient;
169
- }
170
- }
171
- return { textSize, textShiftY };
172
- }
173
- function nodeRadiusGetter({ radiusFlexible, radiusInitial, linkCount, radiusCoefficient, radiusFactor, }) {
174
- return ((radiusFlexible && linkCount ? linkCount / radiusCoefficient : 0) * radiusFactor + radiusInitial);
175
- }
176
- function nodeIdGetter(node) {
177
- return node.id;
178
- }
179
-
180
- function pointerGetter(mouseEvent, areaRect, areaTransform) {
181
- const px = (mouseEvent.clientX - areaRect.left - areaTransform.x) / areaTransform.k;
182
- const py = (mouseEvent.clientY - areaRect.top - areaTransform.y) / areaTransform.k;
183
- return [px, py];
184
- }
185
-
186
- function isOverlapsNode(node, radius, pointerX, pointerY) {
187
- if (node.x == undefined || node.y == undefined)
188
- return false;
189
- const isOverX = node.x - radius <= pointerX && pointerX <= node.x + radius;
190
- const isOverY = node.y - radius <= pointerY && pointerY <= node.y + radius;
191
- return isOverX && isOverY;
769
+ function getNodeOptionsControls(keys) {
770
+ const NODE_CONTROLS = [
771
+ {
772
+ id: "shape",
773
+ type: "select",
774
+ initialValue: NODE_OPTIONS.shape,
775
+ label: "Форма",
776
+ options: [
777
+ { label: "Картинки", value: "icon" },
778
+ { label: "Круг", value: "circle" },
779
+ { label: "Прямоугольник", value: "square" },
780
+ { label: "Текст", value: "text" },
781
+ ],
782
+ },
783
+ {
784
+ id: "radius",
785
+ type: "range",
786
+ max: 50,
787
+ min: 1,
788
+ step: 1,
789
+ label: "Радиус",
790
+ initialValue: NODE_OPTIONS.radius,
791
+ },
792
+ {
793
+ id: "width",
794
+ type: "range",
795
+ max: 50,
796
+ min: 1,
797
+ step: 1,
798
+ label: "Ширина",
799
+ initialValue: NODE_OPTIONS.width,
800
+ },
801
+ {
802
+ id: "height",
803
+ type: "range",
804
+ max: 50,
805
+ min: 1,
806
+ step: 1,
807
+ label: "Высота",
808
+ initialValue: NODE_OPTIONS.height,
809
+ },
810
+ {
811
+ id: "borderRadius",
812
+ type: "range",
813
+ max: 50,
814
+ min: 0,
815
+ step: 1,
816
+ label: "Радиус границы",
817
+ initialValue: NODE_OPTIONS.borderRadius,
818
+ },
819
+ {
820
+ id: "alpha",
821
+ type: "range",
822
+ max: 1,
823
+ min: 0,
824
+ step: 0.1,
825
+ label: "Прозрачность",
826
+ initialValue: NODE_OPTIONS.alpha,
827
+ },
828
+ {
829
+ id: "borderWidth",
830
+ type: "range",
831
+ max: 5,
832
+ min: 0,
833
+ step: 0.1,
834
+ label: "Толщина границы",
835
+ initialValue: NODE_OPTIONS.borderWidth,
836
+ },
837
+ {
838
+ id: "borderColor",
839
+ type: "color",
840
+ initialValue: "#000000FF",
841
+ label: "Цвет границы",
842
+ },
843
+ {
844
+ id: "color",
845
+ type: "color",
846
+ initialValue: "#000000FF",
847
+ label: "Цвет",
848
+ },
849
+ {
850
+ id: "textNodeXPadding",
851
+ type: "range",
852
+ max: 30,
853
+ min: 0,
854
+ step: 0.1,
855
+ label: "Отступы по оси X у текстовой ноды внутри контейнера",
856
+ initialValue: NODE_OPTIONS.textNodeXPadding,
857
+ },
858
+ {
859
+ id: "textNodeYPadding",
860
+ type: "range",
861
+ max: 30,
862
+ min: 0,
863
+ step: 0.1,
864
+ label: "Отступы по оси Y у текстовой ноды внутри контейнера",
865
+ initialValue: NODE_OPTIONS.textNodeYPadding,
866
+ },
867
+ {
868
+ id: "textSize",
869
+ type: "range",
870
+ max: 30,
871
+ min: 0,
872
+ step: 0.1,
873
+ label: "Размер текста",
874
+ initialValue: 0,
875
+ },
876
+ {
877
+ id: "textAlpha",
878
+ type: "range",
879
+ max: 1,
880
+ min: 0,
881
+ step: 0.1,
882
+ label: "Прозрачность текста",
883
+ initialValue: NODE_OPTIONS.textAlpha,
884
+ },
885
+ {
886
+ id: "textWidth",
887
+ type: "range",
888
+ max: 200,
889
+ min: 5,
890
+ step: 1,
891
+ label: "Ширина текста",
892
+ initialValue: NODE_OPTIONS.textWidth,
893
+ },
894
+ {
895
+ id: "textShiftX",
896
+ type: "range",
897
+ max: 100,
898
+ min: 0,
899
+ step: 0.1,
900
+ label: "Смещение X текста",
901
+ initialValue: NODE_OPTIONS.textShiftX,
902
+ },
903
+ {
904
+ id: "textShiftY",
905
+ type: "range",
906
+ max: 100,
907
+ min: 0,
908
+ step: 0.1,
909
+ label: "Смещение Y текста",
910
+ initialValue: 4,
911
+ },
912
+ {
913
+ id: "textWeight",
914
+ type: "range",
915
+ max: 1000,
916
+ min: 100,
917
+ step: 100,
918
+ label: "Жирность текста",
919
+ initialValue: NODE_OPTIONS.textWeight,
920
+ },
921
+ {
922
+ id: "textGap",
923
+ type: "range",
924
+ max: 30,
925
+ min: 0,
926
+ step: 0.1,
927
+ label: "Расстояние между строками",
928
+ initialValue: NODE_OPTIONS.textGap,
929
+ },
930
+ {
931
+ id: "textColor",
932
+ type: "color",
933
+ initialValue: NODE_OPTIONS.textColor,
934
+ label: "Цвет текста",
935
+ },
936
+ ];
937
+ return keys ? NODE_CONTROLS.filter((control) => keys.includes(control.id)) : NODE_CONTROLS;
192
938
  }
193
-
194
- function dragPlaceCoefficientGetter(node, pointerX, pointerY, radius) {
195
- if (!node.x || !node.y)
196
- return undefined;
197
- if (isOverlapsNode(node, radius, pointerX, pointerY))
198
- return node.index;
939
+ function getLinkSettingsControls(keys) {
940
+ const LINK_SETTINGS_CONTROLS = [
941
+ {
942
+ id: "cache",
943
+ type: "checkbox",
944
+ label: "Кеширование вычисляемых настроек",
945
+ initialValue: LINK_SETTINGS.cache,
946
+ },
947
+ {
948
+ id: "particles",
949
+ type: "checkbox",
950
+ label: "Частицы",
951
+ initialValue: LINK_SETTINGS.particles,
952
+ },
953
+ {
954
+ id: "particleFlexSpeed",
955
+ type: "checkbox",
956
+ label: "Гибкий расчет скорости частицы",
957
+ initialValue: LINK_SETTINGS.particleFlexSpeed,
958
+ },
959
+ {
960
+ id: "pretty",
961
+ type: "checkbox",
962
+ label: "Обсчет красивого соединения",
963
+ initialValue: LINK_SETTINGS.pretty,
964
+ },
965
+ {
966
+ id: "arrow",
967
+ type: "checkbox",
968
+ label: "Стрелка",
969
+ initialValue: LINK_SETTINGS.arrow,
970
+ },
971
+ {
972
+ id: "arrowByHighlight",
973
+ type: "checkbox",
974
+ label: "Появление стрелки при анимации",
975
+ initialValue: LINK_SETTINGS.arrowByHighlight,
976
+ },
977
+ {
978
+ id: "highlightByHoverLink",
979
+ type: "checkbox",
980
+ label: "Анимация при наведении на связь",
981
+ initialValue: LINK_SETTINGS.highlightByHoverLink,
982
+ },
983
+ {
984
+ id: "highlightByNodeLinkFading",
985
+ type: "checkbox",
986
+ label: "Затухание при анимации ноды",
987
+ initialValue: LINK_SETTINGS.highlightByNodeLinkFading,
988
+ },
989
+ {
990
+ id: "highlightByNodeArrowFading",
991
+ type: "checkbox",
992
+ label: "Затухание стрелки при анимации ноды",
993
+ initialValue: LINK_SETTINGS.highlightByNodeArrowFading,
994
+ },
995
+ {
996
+ id: "highlightByLinkLinkFading",
997
+ type: "checkbox",
998
+ label: "Затухание при анимации связи",
999
+ initialValue: LINK_SETTINGS.highlightByLinkLinkFading,
1000
+ },
1001
+ {
1002
+ id: "highlightByLinkArrowFading",
1003
+ type: "checkbox",
1004
+ label: "Затухание стрелки при анимации связи",
1005
+ initialValue: LINK_SETTINGS.highlightByLinkArrowFading,
1006
+ },
1007
+ {
1008
+ id: "particleFlexSpeedCoefficient",
1009
+ initialValue: LINK_SETTINGS.particleFlexSpeedCoefficient,
1010
+ max: 10,
1011
+ min: 0.1,
1012
+ step: 0.1,
1013
+ type: "range",
1014
+ label: "Коэффициент скорости частицы относительно расстояния",
1015
+ },
1016
+ {
1017
+ id: "hoverLinkThreshold",
1018
+ initialValue: LINK_SETTINGS.hoverLinkThreshold,
1019
+ max: 10,
1020
+ min: 0,
1021
+ step: 0.01,
1022
+ type: "range",
1023
+ label: "Расширение границы связи для курсора",
1024
+ },
1025
+ {
1026
+ id: "highlightByNodeLinkFadingMin",
1027
+ initialValue: LINK_SETTINGS.highlightByNodeLinkFadingMin,
1028
+ max: 1,
1029
+ min: 0,
1030
+ step: 0.01,
1031
+ type: "range",
1032
+ label: "Граница затухания связи при анимации ноды",
1033
+ },
1034
+ {
1035
+ id: "highlightByNodeArrowFadingMin",
1036
+ initialValue: LINK_SETTINGS.highlightByNodeArrowFadingMin,
1037
+ max: 1,
1038
+ min: 0,
1039
+ step: 0.01,
1040
+ type: "range",
1041
+ label: "Граница затухания стрелки при анимации ноды",
1042
+ },
1043
+ {
1044
+ id: "highlightByLinkLinkFadingMin",
1045
+ initialValue: LINK_SETTINGS.highlightByLinkLinkFadingMin,
1046
+ max: 1,
1047
+ min: 0,
1048
+ step: 0.01,
1049
+ type: "range",
1050
+ label: "Граница затухания связи при анимации связи",
1051
+ },
1052
+ {
1053
+ id: "highlightByLinkArrowFadingMin",
1054
+ initialValue: LINK_SETTINGS.highlightByLinkArrowFadingMin,
1055
+ max: 1,
1056
+ min: 0,
1057
+ step: 0.01,
1058
+ type: "range",
1059
+ label: "Граница затухания стрелки при анимации связи",
1060
+ },
1061
+ ];
1062
+ return keys
1063
+ ? LINK_SETTINGS_CONTROLS.filter((control) => keys.includes(control.id))
1064
+ : LINK_SETTINGS_CONTROLS;
199
1065
  }
200
-
201
- function linkIterationExtractor(link, i, links, state, option, optionConstantGetter) {
202
- let customOptions;
203
- let constantOptions;
204
- if (typeof option === "function")
205
- customOptions = option(link, i, links, state);
206
- else
1066
+ function getLinkOptionsControls(keys) {
1067
+ const LINK_CONTROLS = [
1068
+ {
1069
+ id: "alpha",
1070
+ type: "range",
1071
+ max: 1,
1072
+ min: 0,
1073
+ step: 0.1,
1074
+ label: "Прозрачность",
1075
+ initialValue: LINK_OPTIONS.alpha,
1076
+ },
1077
+ {
1078
+ id: "width",
1079
+ type: "range",
1080
+ max: 10,
1081
+ min: 0.1,
1082
+ step: 0.1,
1083
+ label: "Толщина",
1084
+ initialValue: 0.1,
1085
+ },
1086
+ {
1087
+ id: "arrowAlpha",
1088
+ type: "range",
1089
+ max: 1,
1090
+ min: 0,
1091
+ step: 0.1,
1092
+ label: "Прозрачность стрелки",
1093
+ initialValue: LINK_OPTIONS.arrowAlpha,
1094
+ },
1095
+ {
1096
+ id: "arrowSize",
1097
+ type: "range",
1098
+ max: 10,
1099
+ min: 0.1,
1100
+ step: 0.1,
1101
+ label: "Размер стрелки",
1102
+ initialValue: LINK_OPTIONS.arrowSize,
1103
+ },
1104
+ {
1105
+ id: "arrowBorderWidth",
1106
+ type: "range",
1107
+ max: 10,
1108
+ min: 0.1,
1109
+ step: 0.1,
1110
+ label: "Размер границы стрелки",
1111
+ initialValue: LINK_OPTIONS.arrowBorderWidth,
1112
+ },
1113
+ {
1114
+ id: "particleBorderWidth",
1115
+ type: "range",
1116
+ max: 10,
1117
+ min: 0.1,
1118
+ step: 0.1,
1119
+ label: "Размер границы частиц",
1120
+ initialValue: LINK_OPTIONS.particleBorderWidth,
1121
+ },
1122
+ {
1123
+ id: "particleAlpha",
1124
+ type: "range",
1125
+ max: 1,
1126
+ min: 0,
1127
+ step: 0.1,
1128
+ label: "Прозрачность частиц",
1129
+ initialValue: LINK_OPTIONS.particleAlpha,
1130
+ },
1131
+ {
1132
+ id: "particleRadius",
1133
+ type: "range",
1134
+ max: 10,
1135
+ min: 0.1,
1136
+ step: 0.1,
1137
+ label: "Радиус частиц",
1138
+ initialValue: LINK_OPTIONS.particleRadius,
1139
+ },
1140
+ {
1141
+ id: "particleCount",
1142
+ type: "range",
1143
+ max: 20,
1144
+ min: 1,
1145
+ step: 1,
1146
+ label: "Количество частиц",
1147
+ initialValue: LINK_OPTIONS.particleCount,
1148
+ },
1149
+ {
1150
+ id: "particleSteps",
1151
+ type: "range",
1152
+ max: 200,
1153
+ min: 1,
1154
+ step: 1,
1155
+ label: "Количество кадров у частиц",
1156
+ initialValue: LINK_OPTIONS.particleSteps,
1157
+ },
1158
+ {
1159
+ id: "color",
1160
+ type: "color",
1161
+ initialValue: "#000000FF",
1162
+ label: "Цвет",
1163
+ },
1164
+ {
1165
+ id: "arrowColor",
1166
+ type: "color",
1167
+ initialValue: "#000000FF",
1168
+ label: "Цвет стрелки",
1169
+ },
1170
+ {
1171
+ id: "arrowBorderColor",
1172
+ type: "color",
1173
+ initialValue: LINK_OPTIONS.arrowBorderColor,
1174
+ label: "Цвет границы стрелки",
1175
+ },
1176
+ {
1177
+ id: "particleColor",
1178
+ type: "color",
1179
+ initialValue: LINK_OPTIONS.particleColor,
1180
+ label: "Цвет частиц",
1181
+ },
1182
+ {
1183
+ id: "particleBorderColor",
1184
+ type: "color",
1185
+ initialValue: LINK_OPTIONS.particleBorderColor,
1186
+ label: "Цвет границы частиц",
1187
+ },
1188
+ ];
1189
+ return keys ? LINK_CONTROLS.filter((control) => keys.includes(control.id)) : LINK_CONTROLS;
1190
+ }
1191
+
1192
+ let countTime = 0;
1193
+ let countOperation = 0;
1194
+ function setDrawTime(cb, showEveryTick) {
1195
+ return () => {
1196
+ const start = performance.now();
1197
+ cb();
1198
+ const end = performance.now();
1199
+ const result = end - start;
1200
+ countTime += result;
1201
+ countOperation++;
1202
+ if (showEveryTick) {
1203
+ const average = countTime / countOperation;
1204
+ // eslint-disable-next-line no-console
1205
+ console.log("average draw: ", average, "current draw: ", result);
1206
+ }
1207
+ };
1208
+ }
1209
+ function getDrawTime() {
1210
+ const average = countTime / countOperation;
1211
+ // eslint-disable-next-line no-console
1212
+ console.log("average draw: ", average);
1213
+ }
1214
+ function resetDrawTime() {
1215
+ countTime = 0;
1216
+ countOperation = 0;
1217
+ }
1218
+
1219
+ function nodeSettingsGetter(settings, prevNodeSettings) {
1220
+ return {
1221
+ ...(prevNodeSettings ?? NODE_SETTINGS),
1222
+ idGetter: nodeIdGetter,
1223
+ ...settings,
1224
+ };
1225
+ }
1226
+ const color = colorGetter();
1227
+ function nodeOptionsGetter(node, _, __, state) {
1228
+ const { textShiftY, textSize } = nodeTextSizeGetter(state?.areaTransform);
1229
+ return {
1230
+ ...NODE_OPTIONS,
1231
+ nodeDraw: null,
1232
+ nodeExtraDraw: null,
1233
+ textDraw: null,
1234
+ textExtraDraw: null,
1235
+ color: color(String(node.group ?? "_DEFAULT")),
1236
+ textVisible: Boolean(state?.areaTransform && state.areaTransform.k > COMMON_SETTINGS.nodeTextScaleMin),
1237
+ text: node.name ?? node.id.toString(),
1238
+ textShiftY,
1239
+ textSize,
1240
+ };
1241
+ }
1242
+ function nodeTextSizeGetter(transform) {
1243
+ let textSize = COMMON_SETTINGS.nodeTextSizeMax;
1244
+ let textShiftY = COMMON_SETTINGS.nodeTextShiftYMax;
1245
+ if (transform) {
1246
+ const scaleStepCoefficient = (COMMON_SETTINGS.nodeTextScaleMax - COMMON_SETTINGS.nodeTextScaleMin) /
1247
+ COMMON_SETTINGS.nodeTextChangeStepCount;
1248
+ const textStepCoefficient = (COMMON_SETTINGS.nodeTextSizeMax - COMMON_SETTINGS.nodeTextSizeMin) /
1249
+ COMMON_SETTINGS.nodeTextChangeStepCount;
1250
+ const shiftStepCoefficient = (COMMON_SETTINGS.nodeTextShiftYMax - COMMON_SETTINGS.nodeTextShiftYMin) /
1251
+ COMMON_SETTINGS.nodeTextChangeStepCount;
1252
+ if (transform.k >= COMMON_SETTINGS.nodeTextScaleMax) {
1253
+ textSize = COMMON_SETTINGS.nodeTextSizeMin;
1254
+ textShiftY = COMMON_SETTINGS.nodeTextShiftYMin;
1255
+ }
1256
+ else if (transform.k > COMMON_SETTINGS.nodeTextScaleMin) {
1257
+ const transformSteps = (transform.k - COMMON_SETTINGS.nodeTextScaleMin) / scaleStepCoefficient;
1258
+ textSize -= transformSteps * textStepCoefficient;
1259
+ textShiftY -= transformSteps * shiftStepCoefficient;
1260
+ }
1261
+ }
1262
+ return { textSize, textShiftY };
1263
+ }
1264
+ function nodeRadiusGetter({ radiusFlexible, radiusInitial, linkCount, radiusCoefficient, radiusFactor, }) {
1265
+ return ((radiusFlexible && linkCount ? linkCount / radiusCoefficient : 0) * radiusFactor + radiusInitial);
1266
+ }
1267
+ function nodeSizeGetter({ heightInitial, linkCount, sizeCoefficient, sizeFactor, sizeFlexible, widthInitial, }) {
1268
+ let widthCoefficient = 1;
1269
+ let heightCoefficient = 1;
1270
+ if (sizeFlexible && linkCount != undefined) {
1271
+ widthCoefficient += (linkCount / sizeCoefficient) * sizeFactor;
1272
+ heightCoefficient += (linkCount / sizeCoefficient) * sizeFactor;
1273
+ }
1274
+ return { width: widthInitial * widthCoefficient, height: heightInitial * heightCoefficient };
1275
+ }
1276
+ function nodeIdGetter(node) {
1277
+ return node.id;
1278
+ }
1279
+
1280
+ function pointerGetter(mouseEvent, areaRect, areaTransform) {
1281
+ const clientX = "clientX" in mouseEvent
1282
+ ? mouseEvent.clientX
1283
+ : (mouseEvent.touches[0]?.clientX ?? mouseEvent.changedTouches[0]?.clientX);
1284
+ const clientY = "clientX" in mouseEvent
1285
+ ? mouseEvent.clientY
1286
+ : (mouseEvent.touches[0]?.clientY ?? mouseEvent.changedTouches[0]?.clientY);
1287
+ const px = (clientX - areaRect.left - areaTransform.x) / areaTransform.k;
1288
+ const py = (clientY - areaRect.top - areaTransform.y) / areaTransform.k;
1289
+ return [px, py];
1290
+ }
1291
+
1292
+ function isOverlapsNode(node, pointerX, pointerY, radius) {
1293
+ if (node.x == undefined || node.y == undefined)
1294
+ return false;
1295
+ switch (node._shape) {
1296
+ case "circle": {
1297
+ const nodeRadius = node._radius ?? radius ?? 5;
1298
+ const isOverX = node.x - nodeRadius <= pointerX && pointerX <= node.x + nodeRadius;
1299
+ const isOverY = node.y - nodeRadius <= pointerY && pointerY <= node.y + nodeRadius;
1300
+ return isOverX && isOverY;
1301
+ }
1302
+ case "square":
1303
+ case "text": {
1304
+ const width = node._width ?? 5;
1305
+ const height = node._height ?? 5;
1306
+ const borderRadius = node._borderRadius ?? 0;
1307
+ const overlaps = Math.abs(pointerX - node.x) <= width / 2 && Math.abs(pointerY - node.y) <= height / 2;
1308
+ if (node._shape !== "text" && borderRadius != undefined && borderRadius > 0 && overlaps) {
1309
+ return isCursorOverRoundedRect(pointerX, pointerY, node.x, node.y, width / 2, height / 2, borderRadius);
1310
+ }
1311
+ return overlaps;
1312
+ }
1313
+ default: {
1314
+ const nodeRadius = node._radius ?? radius ?? 5;
1315
+ const isOverX = node.x - nodeRadius <= pointerX && pointerX <= node.x + nodeRadius;
1316
+ const isOverY = node.y - nodeRadius <= pointerY && pointerY <= node.y + nodeRadius;
1317
+ return isOverX && isOverY;
1318
+ }
1319
+ }
1320
+ }
1321
+ function isCursorOverRoundedRect(pointerX, pointerY, x, y, halfWidth, halfHeight, borderRadius) {
1322
+ const left = x - halfWidth;
1323
+ const right = x + halfWidth;
1324
+ const top = y - halfHeight;
1325
+ const bottom = y + halfHeight;
1326
+ if (pointerX < left + borderRadius && pointerY < top + borderRadius) {
1327
+ const dx = pointerX - (left + borderRadius);
1328
+ const dy = pointerY - (top + borderRadius);
1329
+ return dx * dx + dy * dy <= borderRadius * borderRadius;
1330
+ }
1331
+ else if (pointerX > right - borderRadius && pointerY < top + borderRadius) {
1332
+ const dx = pointerX - (right - borderRadius);
1333
+ const dy = pointerY - (top + borderRadius);
1334
+ return dx * dx + dy * dy <= borderRadius * borderRadius;
1335
+ }
1336
+ else if (pointerX < left + borderRadius && pointerY > bottom - borderRadius) {
1337
+ const dx = pointerX - (left + borderRadius);
1338
+ const dy = pointerY - (bottom - borderRadius);
1339
+ return dx * dx + dy * dy <= borderRadius * borderRadius;
1340
+ }
1341
+ else if (pointerX > right - borderRadius && pointerY > bottom - borderRadius) {
1342
+ const dx = pointerX - (right - borderRadius);
1343
+ const dy = pointerY - (bottom - borderRadius);
1344
+ return dx * dx + dy * dy <= borderRadius * borderRadius;
1345
+ }
1346
+ return true;
1347
+ }
1348
+
1349
+ function dragPlaceCoefficientGetter(node, pointerX, pointerY) {
1350
+ if (!node.x || !node.y)
1351
+ return undefined;
1352
+ if (isOverlapsNode(node, pointerX, pointerY, undefined))
1353
+ return node.index;
1354
+ }
1355
+
1356
+ function linkIterationExtractor(link, i, links, state, option, optionConstantGetter) {
1357
+ let customOptions;
1358
+ let constantOptions;
1359
+ if (typeof option === "function")
1360
+ customOptions = option(link, i, links, state);
1361
+ else
207
1362
  customOptions = option;
208
1363
  if (customOptions && typeof customOptions === "object" && !Array.isArray(customOptions)) {
209
1364
  for (const key in customOptions) {
@@ -230,20 +1385,12 @@ function linkIterationExtractor(link, i, links, state, option, optionConstantGet
230
1385
  return customOptions;
231
1386
  }
232
1387
 
233
- function nodeByPointerGetter({ areaRect, areaTransform, mouseEvent, nodes, graphSettings, }) {
1388
+ function nodeByPointerGetter({ areaRect, areaTransform, mouseEvent, nodes, }) {
234
1389
  if (!areaRect)
235
1390
  return undefined;
236
1391
  const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
237
1392
  return d3Array.greatest(nodes, (node) => {
238
- const radius = node._radius ??
239
- nodeRadiusGetter({
240
- radiusFlexible: graphSettings.nodeRadiusFlexible,
241
- radiusInitial: graphSettings.nodeRadiusInitial,
242
- radiusCoefficient: graphSettings.nodeRadiusCoefficient,
243
- radiusFactor: graphSettings.nodeRadiusFactor,
244
- linkCount: node.linkCount,
245
- });
246
- if (isOverlapsNode(node, radius, pointerX, pointerY))
1393
+ if (isOverlapsNode(node, pointerX, pointerY, undefined))
247
1394
  return node.index;
248
1395
  });
249
1396
  }
@@ -280,68 +1427,271 @@ function nodeIterationExtractor(node, i, nodes, state, option, optionConstantGet
280
1427
  return customOptions;
281
1428
  }
282
1429
 
283
- const SPACE = " ";
284
- function drawText({ context, id, textAlign, textColor, textFont, textStyle, textGap, textWeight, textSize, text, x, y, cachedNodeText, maxWidth, }) {
285
- context.font = `${textStyle} normal ${textWeight} ${textSize}px ${textFont}`;
286
- context.fillStyle = textColor;
287
- context.textAlign = textAlign;
288
- if (cachedNodeText[id] != undefined) {
289
- cachedNodeText[id].forEach((line, index) => {
290
- context.fillText(line, x, y + index * textSize + index * textGap);
291
- });
292
- return;
293
- }
294
- if (maxWidth == undefined || context.measureText(text).width <= maxWidth) {
295
- cachedNodeText[id] = [text];
296
- context.fillText(text, x, y);
297
- return;
1430
+ function calculateLinkPositionByNode(link, arrowSize = 0) {
1431
+ const source = link.source;
1432
+ const target = link.target;
1433
+ if (typeof source != "object" || typeof target != "object")
1434
+ return null;
1435
+ const sourcePoint = getLinkPoint({
1436
+ arrowSize: 0,
1437
+ node: source,
1438
+ oppositeNode: target,
1439
+ });
1440
+ const targetPoint = getLinkPoint({
1441
+ arrowSize: arrowSize > 0 ? arrowSize * 0.85 : 0,
1442
+ node: target,
1443
+ oppositeNode: source,
1444
+ });
1445
+ if (!sourcePoint || !targetPoint)
1446
+ return null;
1447
+ const distance = targetPoint && sourcePoint
1448
+ ? Math.sqrt((targetPoint.x - sourcePoint.x) ** 2 + (targetPoint.y - sourcePoint.y) ** 2)
1449
+ : 0;
1450
+ return {
1451
+ x1: sourcePoint.x,
1452
+ y1: sourcePoint.y,
1453
+ x2: targetPoint.x,
1454
+ y2: targetPoint.y,
1455
+ distance,
1456
+ };
1457
+ }
1458
+ function getLinkPoint(opts) {
1459
+ if (opts.node.x == undefined ||
1460
+ opts.node.y == undefined ||
1461
+ opts.oppositeNode.x == undefined ||
1462
+ opts.oppositeNode.y == undefined)
1463
+ return null;
1464
+ let nodePoint;
1465
+ switch (opts.node._shape) {
1466
+ case "circle": {
1467
+ nodePoint = getCircleIntersection({
1468
+ x: opts.node.x,
1469
+ y: opts.node.y,
1470
+ radius: opts.node._radius ?? COMMON_SETTINGS.nodeRadius,
1471
+ oppositeX: opts.oppositeNode.x,
1472
+ oppositeY: opts.oppositeNode.y,
1473
+ arrowSize: opts.arrowSize,
1474
+ });
1475
+ break;
1476
+ }
1477
+ case "square":
1478
+ case "text": {
1479
+ nodePoint = getRectangleIntersection({
1480
+ arrowSize: opts.arrowSize,
1481
+ x: opts.node.x,
1482
+ y: opts.node.y,
1483
+ height: opts.node._height ?? COMMON_SETTINGS.nodeSize,
1484
+ width: opts.node._width ?? COMMON_SETTINGS.nodeSize,
1485
+ oppositeX: opts.oppositeNode.x,
1486
+ oppositeY: opts.oppositeNode.y,
1487
+ borderRadius: opts.node._shape === "text" ? 0 : (opts.node._borderRadius ?? 0),
1488
+ });
1489
+ break;
1490
+ }
1491
+ default: {
1492
+ nodePoint = getCircleIntersection({
1493
+ x: opts.node.x,
1494
+ y: opts.node.y,
1495
+ radius: opts.node._radius ?? COMMON_SETTINGS.nodeRadius,
1496
+ oppositeX: opts.oppositeNode.x,
1497
+ oppositeY: opts.oppositeNode.y,
1498
+ arrowSize: opts.arrowSize,
1499
+ });
1500
+ }
298
1501
  }
299
- const spaceWidth = context.measureText(" ").width;
300
- const lines = [];
301
- let lineWidth = 0;
302
- let line = "";
303
- for (const word of text.split(" ")) {
304
- const size = context.measureText(word).width;
305
- lineWidth += size;
306
- if (line === "") {
307
- line = word;
308
- continue;
309
- }
310
- if (lineWidth > maxWidth) {
311
- lineWidth = 0;
312
- lines.push(line);
313
- line = word;
1502
+ return nodePoint;
1503
+ }
1504
+ function getCircleIntersection(opts) {
1505
+ const dx = opts.oppositeX - opts.x;
1506
+ const dy = opts.oppositeY - opts.y;
1507
+ const dr = Math.sqrt(dx * dx + dy * dy);
1508
+ const radius = opts.radius + opts.arrowSize;
1509
+ return {
1510
+ x: opts.x + (dx * radius) / dr,
1511
+ y: opts.y + (dy * radius) / dr,
1512
+ };
1513
+ }
1514
+ function getRectangleIntersection(opts) {
1515
+ const halfWidth = opts.width / 2;
1516
+ const halfHeight = opts.height / 2;
1517
+ const dx = opts.oppositeX - opts.x;
1518
+ const dy = opts.oppositeY - opts.y;
1519
+ let relX, relY;
1520
+ if (Math.abs(dx) <= halfWidth && Math.abs(dy) <= halfHeight) {
1521
+ const distToRight = halfWidth - dx;
1522
+ const distToLeft = halfWidth + dx;
1523
+ const distToTop = halfHeight - dy;
1524
+ const distToBottom = halfHeight + dy;
1525
+ const minDist = Math.min(distToRight, distToLeft, distToTop, distToBottom);
1526
+ relX = minDist === distToRight ? halfWidth : minDist === distToLeft ? -halfWidth : dx;
1527
+ relY = minDist === distToTop ? halfHeight : minDist === distToBottom ? -halfHeight : dy;
1528
+ }
1529
+ else {
1530
+ const absDx = Math.abs(dx);
1531
+ const absDy = Math.abs(dy);
1532
+ if (halfWidth * absDy < halfHeight * absDx) {
1533
+ relX = dx > 0 ? halfWidth : -halfWidth;
1534
+ relY = (relX * dy) / dx;
1535
+ if (Math.abs(relY) > halfHeight) {
1536
+ relY = dy > 0 ? halfHeight : -halfHeight;
1537
+ relX = (relY * dx) / dy;
1538
+ }
314
1539
  }
315
1540
  else {
316
- lineWidth += spaceWidth;
317
- line += `${SPACE}${word}`;
1541
+ relY = dy > 0 ? halfHeight : -halfHeight;
1542
+ relX = (relY * dx) / dy;
1543
+ if (Math.abs(relX) > halfWidth) {
1544
+ relX = dx > 0 ? halfWidth : -halfWidth;
1545
+ relY = (relX * dy) / dx;
1546
+ }
318
1547
  }
319
1548
  }
320
- if (line !== "")
321
- lines.push(line);
322
- cachedNodeText[id] = lines;
323
- lines.forEach((line, index) => {
324
- context.fillText(line, x, y + index * textSize + index * textGap);
325
- });
326
- }
327
-
328
- function calculateLinkPositionByRadius(link, arrowSize = 0) {
329
- const source = link.source;
330
- const target = link.target;
331
- if (typeof source != "object" || typeof target != "object")
332
- return null;
333
- const dx = (target.x ?? 0) - (source.x ?? 0);
334
- const dy = (target.y ?? 0) - (source.y ?? 0);
335
- const dr = Math.sqrt(dx * dx + dy * dy);
336
- const sourceRadius = source._radius ?? 0;
337
- const targetRadius = (target._radius ?? 0) + (arrowSize > 0 ? arrowSize * 0.85 : 0);
1549
+ if (opts.borderRadius != undefined) {
1550
+ const { x, y } = squareRadiusFix(relX, relY, halfWidth, halfHeight, opts.borderRadius);
1551
+ if (x != undefined)
1552
+ relX = x;
1553
+ if (y != undefined)
1554
+ relY = y;
1555
+ }
1556
+ if (opts.arrowSize > 0) {
1557
+ const edgeDist = Math.sqrt(relX * relX + relY * relY);
1558
+ const scale = (edgeDist + opts.arrowSize) / edgeDist;
1559
+ relX *= scale;
1560
+ relY *= scale;
1561
+ }
338
1562
  return {
339
- x1: (source.x ?? 0) + (dx * sourceRadius) / dr,
340
- y1: (source.y ?? 0) + (dy * sourceRadius) / dr,
341
- x2: (target.x ?? 0) - (dx * targetRadius) / dr,
342
- y2: (target.y ?? 0) - (dy * targetRadius) / dr,
1563
+ x: opts.x + relX,
1564
+ y: opts.y + relY,
343
1565
  };
344
1566
  }
1567
+ // eslint-disable-next-line no-warning-comments
1568
+ // TODO: Need upgrade
1569
+ function squareRadiusFix(relX, relY, halfWidth, halfHeight, borderRadius) {
1570
+ const epsilon = 1e-6;
1571
+ const absX = Math.abs(relX);
1572
+ const absY = Math.abs(relY);
1573
+ let side = null;
1574
+ if (absX >= halfWidth - epsilon) {
1575
+ side = relX > 0 ? "right" : "left";
1576
+ }
1577
+ else if (absY >= halfHeight - epsilon) {
1578
+ side = relY > 0 ? "top" : "bottom";
1579
+ }
1580
+ if (side) {
1581
+ const topBound = halfHeight - borderRadius;
1582
+ const bottomBound = -topBound;
1583
+ const rightBound = halfWidth - borderRadius;
1584
+ const leftBound = -rightBound;
1585
+ switch (side) {
1586
+ case "right":
1587
+ if (relY > topBound) {
1588
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "right-top");
1589
+ }
1590
+ else if (relY < bottomBound) {
1591
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "right-bottom");
1592
+ }
1593
+ break;
1594
+ case "left":
1595
+ if (relY > topBound) {
1596
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "left-top");
1597
+ }
1598
+ else if (relY < bottomBound) {
1599
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "left-bottom");
1600
+ }
1601
+ break;
1602
+ case "top":
1603
+ if (relX > rightBound) {
1604
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "top-right");
1605
+ }
1606
+ else if (relX < leftBound) {
1607
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "top-left");
1608
+ }
1609
+ break;
1610
+ case "bottom":
1611
+ if (relX > rightBound) {
1612
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "bottom-right");
1613
+ }
1614
+ else if (relX < leftBound) {
1615
+ return checkIntersection(relX, relY, halfWidth, halfHeight, borderRadius, "bottom-left");
1616
+ }
1617
+ break;
1618
+ }
1619
+ }
1620
+ return { x: undefined, y: undefined };
1621
+ }
1622
+ function checkIntersection(relX, relY, halfWidth, halfHeight, r, corner) {
1623
+ const intersect = intersectWithCircle(relX, relY, halfWidth, halfHeight, r, corner);
1624
+ return intersect ?? { x: undefined, y: undefined };
1625
+ }
1626
+ function intersectWithCircle(relX, relY, halfWidth, halfHeight, r, corner) {
1627
+ switch (corner) {
1628
+ case "top-right":
1629
+ corner = "right-top";
1630
+ break;
1631
+ case "bottom-right":
1632
+ corner = "right-bottom";
1633
+ break;
1634
+ case "top-left":
1635
+ corner = "left-top";
1636
+ break;
1637
+ case "bottom-left":
1638
+ corner = "left-bottom";
1639
+ break;
1640
+ }
1641
+ let cx, cy;
1642
+ switch (corner) {
1643
+ case "right-top":
1644
+ cx = halfWidth - r;
1645
+ cy = halfHeight - r;
1646
+ break;
1647
+ case "right-bottom":
1648
+ cx = halfWidth - r;
1649
+ cy = -halfHeight + r;
1650
+ break;
1651
+ case "left-top":
1652
+ cx = -halfWidth + r;
1653
+ cy = halfHeight - r;
1654
+ break;
1655
+ case "left-bottom":
1656
+ cx = -halfWidth + r;
1657
+ cy = -halfHeight + r;
1658
+ break;
1659
+ default:
1660
+ return null;
1661
+ }
1662
+ const a = relX ** 2 + relY ** 2;
1663
+ if (a === 0)
1664
+ return null;
1665
+ const b = -2 * (relX * cx + relY * cy);
1666
+ const c = cx ** 2 + cy ** 2 - r ** 2;
1667
+ const discriminant = b ** 2 - 4 * a * c;
1668
+ if (discriminant < 0)
1669
+ return null;
1670
+ const sqrtD = Math.sqrt(discriminant);
1671
+ const t2 = (-b - sqrtD) / (2 * a);
1672
+ const t1 = (-b + sqrtD) / (2 * a);
1673
+ const t = t2 > 0 ? t2 : t1 > 0 ? t1 : null;
1674
+ if (t === null || t >= 1)
1675
+ return null;
1676
+ const x = t * relX;
1677
+ const y = t * relY;
1678
+ let valid = false;
1679
+ switch (corner) {
1680
+ case "right-top":
1681
+ valid = x >= cx && y >= cy;
1682
+ break;
1683
+ case "right-bottom":
1684
+ valid = x >= cx && y <= cy;
1685
+ break;
1686
+ case "left-top":
1687
+ valid = x <= cx && y >= cy;
1688
+ break;
1689
+ case "left-bottom":
1690
+ valid = x <= cx && y <= cy;
1691
+ break;
1692
+ }
1693
+ return valid ? { x, y } : null;
1694
+ }
345
1695
 
346
1696
  function animationByProgress(initial, additional, progress) {
347
1697
  const max = initial + additional;
@@ -359,20 +1709,53 @@ function isNodeVisible(opts) {
359
1709
  const right = (opts.width - opts.transform.x) / opts.transform.k;
360
1710
  const top = -opts.transform.y / opts.transform.k;
361
1711
  const bottom = (opts.height - opts.transform.y) / opts.transform.k;
362
- return (left < opts.x + opts.radius + ADDITIONAL_VIEWPORT &&
363
- opts.x - opts.radius - ADDITIONAL_VIEWPORT < right &&
364
- top < opts.y + opts.radius + ADDITIONAL_VIEWPORT &&
365
- opts.y - opts.radius - ADDITIONAL_VIEWPORT < bottom);
1712
+ if (opts.node.x == undefined || opts.node.y == undefined)
1713
+ return false;
1714
+ switch (opts.node._shape) {
1715
+ case "circle": {
1716
+ const radius = opts.node._radius ?? COMMON_SETTINGS.nodeRadius;
1717
+ return (left < opts.node.x + radius + ADDITIONAL_VIEWPORT &&
1718
+ opts.node.x - radius - ADDITIONAL_VIEWPORT < right &&
1719
+ top < opts.node.y + radius + ADDITIONAL_VIEWPORT &&
1720
+ opts.node.y - radius - ADDITIONAL_VIEWPORT < bottom);
1721
+ }
1722
+ case "square":
1723
+ case "text": {
1724
+ const width = opts.node._width ?? COMMON_SETTINGS.nodeSize;
1725
+ const height = opts.node._height ?? COMMON_SETTINGS.nodeSize;
1726
+ return (left < opts.node.x + width + ADDITIONAL_VIEWPORT &&
1727
+ opts.node.x - width - ADDITIONAL_VIEWPORT < right &&
1728
+ top < opts.node.y + height + ADDITIONAL_VIEWPORT &&
1729
+ opts.node.y - height - ADDITIONAL_VIEWPORT < bottom);
1730
+ }
1731
+ default: {
1732
+ const radius = opts.node._radius ?? COMMON_SETTINGS.nodeRadius;
1733
+ return (left < opts.node.x + radius + ADDITIONAL_VIEWPORT &&
1734
+ opts.node.x - radius - ADDITIONAL_VIEWPORT < right &&
1735
+ top < opts.node.y + radius + ADDITIONAL_VIEWPORT &&
1736
+ opts.node.y - radius - ADDITIONAL_VIEWPORT < bottom);
1737
+ }
1738
+ }
366
1739
  }
367
1740
 
368
1741
  function getParticlePosition(opts) {
369
- if (opts.particle.wait > 0) {
370
- opts.particle.wait--;
1742
+ const prevStepDifference = opts.particle.step - (opts.particle.prev?.step ?? 0);
1743
+ const nextStepDifference = (opts.particle.next?.step ?? 0) - opts.particle.step;
1744
+ const needWait = opts.particle.next &&
1745
+ opts.particle.next.step > opts.particle.step &&
1746
+ nextStepDifference <= opts.distance;
1747
+ const needSpeed = opts.particle.prev &&
1748
+ opts.particle.prev.step !== 0 &&
1749
+ opts.particle.prev.step < opts.particle.step &&
1750
+ prevStepDifference < opts.distance;
1751
+ if (opts.particle.step === 0 && needWait) {
1752
+ opts.particle.x = undefined;
1753
+ opts.particle.y = undefined;
371
1754
  return;
372
1755
  }
373
1756
  const remainingSteps = opts.totalSteps - opts.particle.step;
374
1757
  const progress = opts.particle.step / opts.totalSteps;
375
- if (remainingSteps === 0) {
1758
+ if (remainingSteps <= 0) {
376
1759
  opts.particle.x = opts.xEnd;
377
1760
  opts.particle.y = opts.yEnd;
378
1761
  opts.particle.step = 0;
@@ -384,675 +1767,1226 @@ function getParticlePosition(opts) {
384
1767
  const newY = opts.yStart + dy * progress;
385
1768
  opts.particle.x = newX;
386
1769
  opts.particle.y = newY;
387
- opts.particle.step++;
1770
+ if (needSpeed) {
1771
+ opts.particle.step += prevStepDifference <= 3 ? prevStepDifference : 3;
1772
+ }
1773
+ else {
1774
+ opts.particle.step++;
1775
+ }
388
1776
  }
389
1777
 
390
- const FORCE_SETTINGS = {
391
- centerPosition: {},
392
- centerStrength: 1,
393
- collideStrength: 0.1,
394
- collideAdditionalRadius: 4,
395
- collideIterations: 2,
396
- collideOffMax: { links: 0, nodes: 0 },
397
- collideOn: true,
398
- chargeStrength: -40,
399
- chargeDistanceMax: Infinity,
400
- chargeDistanceMin: 1,
401
- xForce: 0,
402
- xStrength: 0.1,
403
- yForce: 0,
404
- yStrength: 0.1,
405
- linkDistance: 30,
406
- linkIterations: 1,
407
- linkStrength: 1,
408
- collideRadius: null,
409
- };
410
- const GRAPH_SETTINGS = {
411
- zoomExtent: [0.3, 10],
412
- translateExtent: [[], []],
413
- translateExtentEnable: true,
414
- translateExtentCoefficient: [3, 3],
415
- highlightSizingAdditional: 0.5,
416
- highlightColorFadingMin: 0.15,
417
- highlightTextShiftXAdditional: 0,
418
- highlightTextShiftYAdditional: 2,
419
- highlightTextSizingAdditional: 1,
420
- highlightTextWeightAdditional: 0,
421
- highlightTextWidthAdditional: 10,
422
- highlightOnlyRoot: true,
423
- stickAfterDrag: false,
424
- highlightByHover: true,
425
- highlightLinkFadingMin: 0.21,
426
- highlightFadingMin: 0.21,
427
- highlightTextFadingMin: 0.21,
428
- highlightArrowFadingMin: 0.21,
429
- highlightDownStep: 0.2,
430
- highlightUpStep: 0.2,
431
- dragPlaceCoefficient: dragPlaceCoefficientGetter,
432
- nodeRadiusInitial: 4,
433
- nodeRadiusCoefficient: 5,
434
- nodeRadiusFactor: 1,
435
- nodeRadiusFlexible: true,
436
- zoomInitial: null,
437
- showDrawTime: true,
438
- showDrawTimeEveryTick: false,
439
- };
440
- const NODE_SETTINGS = {
441
- cache: false,
442
- };
443
- const NODE_OPTIONS = {
444
- alpha: 1,
445
- textAlpha: 1,
446
- borderColor: "#000000FF",
447
- borderWidth: 0.1,
448
- textWidth: 20,
449
- textShiftX: 0,
450
- textFont: "Arial",
451
- textAlign: "center",
452
- textColor: "#333",
453
- radius: 4,
454
- textStyle: "normal",
455
- textWeight: 500,
456
- textGap: 1,
457
- highlightFading: true,
458
- highlightColor: false,
459
- highlightSizing: true,
460
- highlightTextFading: true,
461
- highlightTextSizing: true,
462
- };
463
- const LINK_SETTINGS = {
464
- cache: false,
465
- particles: true,
466
- };
467
- const LINK_OPTIONS = {
468
- alpha: 1,
469
- highlightFading: true,
470
- pretty: true,
471
- arrow: true,
472
- arrowAlpha: 1,
473
- arrowSize: 2,
474
- arrowHighlightFading: true,
475
- arrowReverseAppear: true,
476
- particleAlpha: 1,
477
- particleColor: "#000000FF",
478
- particleCount: 2,
479
- particleRadius: 0.5,
480
- particleSteps: 60,
481
- };
482
- const COMMON_SETTINGS = {
483
- linkColorZoomFar: "#999",
484
- linkColorZoomNear: "#000000FF",
485
- linkWidthZoomFar: 1,
486
- linkWidthZoomNear: 0.1,
487
- linkWidthZoomBorder: 1,
488
- linkColorZoomBorder: 1,
489
- nodeTextScaleMin: 1.5,
490
- nodeTextScaleMax: 20,
491
- nodeTextSizeMin: 1.5,
492
- nodeTextSizeMax: 3.5,
493
- nodeTextShiftYMin: 2.5,
494
- nodeTextShiftYMax: 4,
495
- nodeTextChangeStepCount: 200,
496
- };
497
-
498
- function getForceControls(keys) {
499
- const FORCE_CONTROLS = [
500
- {
501
- id: "centerStrength",
502
- initialValue: FORCE_SETTINGS.centerStrength,
503
- label: "Граница центра",
504
- max: 1,
505
- min: 0,
506
- step: 0.01,
507
- type: "range",
508
- },
509
- {
510
- id: "collideStrength",
511
- initialValue: FORCE_SETTINGS.collideStrength,
512
- label: "Сила отталкивания",
513
- max: 1,
514
- min: 0,
515
- step: 0.01,
516
- type: "range",
517
- },
518
- {
519
- id: "collideAdditionalRadius",
520
- initialValue: FORCE_SETTINGS.collideAdditionalRadius,
521
- label: "Радиус отталкивания",
522
- max: 300,
523
- min: 0,
524
- step: 0.1,
525
- type: "range",
526
- },
527
- {
528
- id: "collideIterations",
529
- initialValue: FORCE_SETTINGS.collideIterations,
530
- label: "Итерации отталкивания",
531
- max: 10,
532
- min: 0,
533
- step: 1,
534
- type: "range",
535
- },
536
- {
537
- id: "chargeStrength",
538
- initialValue: FORCE_SETTINGS.chargeStrength ,
539
- label: "Гравитация",
540
- max: 0,
541
- min: -300,
542
- step: 0.1,
543
- type: "range",
544
- },
545
- {
546
- type: "range",
547
- id: "xForce",
548
- min: 0,
549
- max: 1,
550
- step: 0.01,
551
- label: "Граница X",
552
- initialValue: FORCE_SETTINGS.xForce ,
553
- },
554
- {
555
- type: "range",
556
- id: "yForce",
557
- min: 0,
558
- max: 1,
559
- step: 0.01,
560
- label: "Граница Y",
561
- initialValue: FORCE_SETTINGS.yForce ,
562
- },
563
- {
564
- type: "range",
565
- id: "xStrength",
566
- min: 0,
567
- max: 1,
568
- step: 0.01,
569
- label: "Гравитация X",
570
- initialValue: FORCE_SETTINGS.xStrength ,
571
- },
572
- {
573
- type: "range",
574
- id: "yStrength",
575
- min: 0,
576
- max: 1,
577
- step: 0.01,
578
- label: "Гравитация Y",
579
- initialValue: FORCE_SETTINGS.yStrength ,
580
- },
581
- {
582
- type: "range",
583
- id: "linkStrength",
584
- min: 0,
585
- max: 1,
586
- step: 0.01,
587
- label: "Натяжение связей",
588
- initialValue: FORCE_SETTINGS.linkStrength ,
589
- },
590
- {
591
- type: "range",
592
- id: "linkDistance",
593
- min: 0,
594
- max: 300,
595
- step: 0.1,
596
- label: "Расстояние связей",
597
- initialValue: FORCE_SETTINGS.linkDistance ,
598
- },
599
- {
600
- id: "collideOn",
601
- type: "checkbox",
602
- initialValue: FORCE_SETTINGS.collideOn,
603
- label: "Отталкивание",
604
- },
605
- ];
606
- return keys ? FORCE_CONTROLS.filter((control) => keys.includes(control.id)) : FORCE_CONTROLS;
1778
+ /* eslint-disable id-length */
1779
+ function linkByPointerGetter({ areaRect, areaTransform, mouseEvent, links, linkSettings, }) {
1780
+ if (!areaRect)
1781
+ return undefined;
1782
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
1783
+ return d3Array.greatest(links, (link) => {
1784
+ if (isNearLink(pointerX, pointerY, link, linkSettings.hoverLinkThreshold))
1785
+ return link.index;
1786
+ });
607
1787
  }
608
- function getGraphControls(keys) {
609
- const GRAPH_CONTROLS = [
610
- {
611
- id: "highlightSizingAdditional",
612
- initialValue: GRAPH_SETTINGS.highlightSizingAdditional,
613
- max: 10,
614
- min: 0.1,
615
- step: 0.01,
616
- type: "range",
617
- label: "Дополнительный размер при анимации",
618
- },
619
- {
620
- id: "highlightColorFadingMin",
621
- initialValue: GRAPH_SETTINGS.highlightColorFadingMin,
622
- max: 1,
623
- min: 0.01,
624
- step: 0.01,
625
- type: "range",
626
- label: "Граница затухания цвета",
627
- },
628
- {
629
- id: "highlightTextShiftXAdditional",
630
- initialValue: GRAPH_SETTINGS.highlightTextShiftXAdditional,
631
- max: 50,
632
- min: 0,
633
- step: 0.1,
634
- type: "range",
635
- label: "Смещение текста по X при анимации",
636
- },
637
- {
638
- id: "highlightTextShiftYAdditional",
639
- initialValue: GRAPH_SETTINGS.highlightTextShiftYAdditional,
640
- max: 50,
641
- min: 0,
642
- step: 0.1,
643
- type: "range",
644
- label: "Смещение текста по Y при анимации",
645
- },
646
- {
647
- id: "highlightTextSizingAdditional",
648
- initialValue: GRAPH_SETTINGS.highlightTextSizingAdditional,
649
- max: 10,
650
- min: 0.1,
651
- step: 0.1,
652
- type: "range",
653
- label: "Увеличение текста при анимации",
654
- },
655
- {
656
- id: "highlightTextWeightAdditional",
657
- initialValue: GRAPH_SETTINGS.highlightTextWeightAdditional,
658
- max: 1000,
659
- min: 0,
660
- step: 100,
661
- type: "range",
662
- label: "Увеличение жирности текста при анимации",
663
- },
664
- {
665
- id: "highlightTextWidthAdditional",
666
- initialValue: GRAPH_SETTINGS.highlightTextWidthAdditional,
667
- max: 100,
668
- min: 0,
669
- step: 0.1,
670
- type: "range",
671
- label: "Увеличение ширины текста при анимации",
672
- },
673
- {
674
- id: "highlightTextFadingMin",
675
- initialValue: GRAPH_SETTINGS.highlightTextFadingMin,
676
- max: 1,
677
- min: 0,
678
- step: 0.01,
679
- type: "range",
680
- label: "Граница затухания текста при анимации",
681
- },
682
- {
683
- id: "highlightLinkFadingMin",
684
- initialValue: GRAPH_SETTINGS.highlightLinkFadingMin,
685
- max: 1,
686
- min: 0,
687
- step: 0.01,
688
- type: "range",
689
- label: "Граница затухания связи при анимации",
690
- },
691
- {
692
- id: "highlightFadingMin",
693
- initialValue: GRAPH_SETTINGS.highlightFadingMin,
694
- max: 1,
695
- min: 0,
696
- step: 0.01,
697
- type: "range",
698
- label: "Граница затухания ноды при анимации",
699
- },
700
- {
701
- id: "highlightArrowFadingMin",
702
- initialValue: GRAPH_SETTINGS.highlightArrowFadingMin,
703
- max: 1,
704
- min: 0,
705
- step: 0.01,
706
- type: "range",
707
- label: "Граница затухания стрелки при анимации",
708
- },
709
- {
710
- id: "highlightDownStep",
711
- initialValue: GRAPH_SETTINGS.highlightDownStep,
712
- max: 1,
713
- min: 0.01,
714
- step: 0.01,
715
- type: "range",
716
- label: "Скорость отмены анимации",
717
- },
718
- {
719
- id: "highlightUpStep",
720
- initialValue: GRAPH_SETTINGS.highlightUpStep,
721
- max: 1,
722
- min: 0.01,
723
- step: 0.01,
724
- type: "range",
725
- label: "Скорость применения анимации",
726
- },
727
- {
728
- id: "nodeRadiusInitial",
729
- initialValue: GRAPH_SETTINGS.nodeRadiusInitial,
730
- max: 50,
731
- min: 0.1,
732
- step: 0.1,
733
- type: "range",
734
- label: "Изначальный радиус ноды",
735
- },
736
- {
737
- id: "nodeRadiusCoefficient",
738
- initialValue: GRAPH_SETTINGS.nodeRadiusCoefficient,
739
- max: 100,
740
- min: 0.1,
741
- step: 0.1,
742
- type: "range",
743
- label: "Количество связей для увеличения радиуса",
744
- },
745
- {
746
- id: "nodeRadiusFactor",
747
- initialValue: GRAPH_SETTINGS.nodeRadiusFactor,
748
- max: 50,
749
- min: 0.1,
750
- step: 0.1,
751
- type: "range",
752
- label: "Коэффициент увеличения радиуса",
753
- },
754
- {
755
- id: "highlightOnlyRoot",
756
- type: "checkbox",
757
- initialValue: GRAPH_SETTINGS.highlightOnlyRoot,
758
- label: "Дополнительная анимация только главное цели",
759
- },
760
- {
761
- id: "stickAfterDrag",
762
- type: "checkbox",
763
- initialValue: GRAPH_SETTINGS.stickAfterDrag,
764
- label: "Фиксировании ноды после перетаскивания",
765
- },
766
- {
767
- id: "highlightByHover",
768
- type: "checkbox",
769
- initialValue: GRAPH_SETTINGS.highlightByHover,
770
- label: "Анимации при наведении",
771
- },
772
- {
773
- id: "nodeRadiusFlexible",
774
- type: "checkbox",
775
- initialValue: GRAPH_SETTINGS.nodeRadiusFlexible,
776
- label: "Гибкий радиус ноды",
777
- },
778
- ];
779
- return keys ? GRAPH_CONTROLS.filter((control) => keys.includes(control.id)) : GRAPH_CONTROLS;
1788
+ function isNearLink(mouseX, mouseY, link, threshold = 2) {
1789
+ if (!jsHelpers.isObject(link.source) || !jsHelpers.isObject(link.target))
1790
+ return false;
1791
+ const x1 = link.source.x;
1792
+ const y1 = link.source.y;
1793
+ const x2 = link.target.x;
1794
+ const y2 = link.target.y;
1795
+ const distance = distanceToLine(mouseX, mouseY, x1, y1, x2, y2);
1796
+ return distance <= threshold;
1797
+ }
1798
+ function distanceToLine(x, y, x1, y1, x2, y2) {
1799
+ const A = x - x1;
1800
+ const B = y - y1;
1801
+ const C = x2 - x1;
1802
+ const D = y2 - y1;
1803
+ const dot = A * C + B * D;
1804
+ const lenSq = C * C + D * D;
1805
+ let param = -1;
1806
+ if (lenSq !== 0) {
1807
+ param = dot / lenSq;
1808
+ }
1809
+ let xx, yy;
1810
+ if (param < 0) {
1811
+ xx = x1;
1812
+ yy = y1;
1813
+ }
1814
+ else if (param > 1) {
1815
+ xx = x2;
1816
+ yy = y2;
1817
+ }
1818
+ else {
1819
+ xx = x1 + param * C;
1820
+ yy = y1 + param * D;
1821
+ }
1822
+ const dx = x - xx;
1823
+ const dy = y - yy;
1824
+ return Math.sqrt(dx * dx + dy * dy);
1825
+ }
1826
+
1827
+ function initArea() {
1828
+ if (!this.area || !this.context || !this.container) {
1829
+ this.container = d3Selection.create("div")
1830
+ .attr("style", "padding: 0 !important; width: 100%; height: 100%;")
1831
+ .node();
1832
+ if (!this.container)
1833
+ throw new Error("couldn't create container");
1834
+ this.root.appendChild(this.container);
1835
+ const { width, height } = this.root.getBoundingClientRect();
1836
+ this.width = width;
1837
+ this.height = height;
1838
+ this.area = d3Selection.create("canvas")
1839
+ .attr("width", this.dpi * this.width)
1840
+ .attr("height", this.dpi * this.height)
1841
+ .attr("style", `width: 100%; height: 100%; border: none !important;`)
1842
+ .node();
1843
+ if (!this.area)
1844
+ throw new Error("couldn't create canvas");
1845
+ this.container.appendChild(this.area);
1846
+ this.areaRect = this.area.getBoundingClientRect();
1847
+ this.context = this.area.getContext("2d");
1848
+ if (!this.context)
1849
+ throw new Error("couldn't create canvas context");
1850
+ this.context.scale(this.dpi, this.dpi);
1851
+ }
1852
+ }
1853
+
1854
+ function initDnd() {
1855
+ if (!this.area || !this.nodes)
1856
+ throw new Error("bad init data");
1857
+ const dragHandler = d3Drag.drag()
1858
+ .subject((event) => {
1859
+ if (this.listeners.onDragSubject) {
1860
+ return this.listeners.onDragSubject(event, this.state);
1861
+ }
1862
+ if (!this.areaRect)
1863
+ return;
1864
+ const mouseEvent = event.sourceEvent;
1865
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1866
+ return d3Array.greatest(this.nodes, (node) => {
1867
+ if (!node.x || !node.y || (jsHelpers.isBoolean(node.drag) && !node.drag))
1868
+ return undefined;
1869
+ return this.graphSettings.dragPlaceCoefficient(node, pointerX, pointerY);
1870
+ });
1871
+ })
1872
+ .on("start", (event) => {
1873
+ this.listeners.onStartDragFinished?.(event, this.state);
1874
+ })
1875
+ .on("drag", (event) => {
1876
+ if (!this.isDragging) {
1877
+ this.isDragging = true;
1878
+ if (this.simulation)
1879
+ this.simulation.alphaTarget(0.3).restart();
1880
+ }
1881
+ if (!this.areaRect)
1882
+ return;
1883
+ const mouseEvent = event.sourceEvent;
1884
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1885
+ event.subject.fx = pointerX;
1886
+ event.subject.fy = pointerY;
1887
+ this.listeners.onMoveDragFinished?.(event, this.state);
1888
+ })
1889
+ .on("end", (event) => {
1890
+ this.isDragging = false;
1891
+ if (!event.active && this.simulation)
1892
+ this.simulation.alphaTarget(0);
1893
+ const sourceEvent = event.sourceEvent;
1894
+ if (sourceEvent.altKey && this.areaRect) {
1895
+ const [pointerX, pointerY] = pointerGetter(sourceEvent, this.areaRect, this.areaTransform);
1896
+ event.subject.fx = pointerX;
1897
+ event.subject.fy = pointerY;
1898
+ }
1899
+ else {
1900
+ event.subject.fx = null;
1901
+ event.subject.fy = null;
1902
+ }
1903
+ this.listeners.onEndDragFinished?.(event, this.state);
1904
+ });
1905
+ d3Selection.select(this.area).call(dragHandler);
1906
+ }
1907
+
1908
+ function getDrawLink(state) {
1909
+ return function drawLink(link, index) {
1910
+ if (!this.context ||
1911
+ typeof link.source !== "object" ||
1912
+ typeof link.target !== "object" ||
1913
+ !link.source.x ||
1914
+ !link.source.y ||
1915
+ !link.target.x ||
1916
+ !link.target.y)
1917
+ return;
1918
+ if (!link.source._visible && !link.target._visible)
1919
+ return;
1920
+ const id = `${link.target.id}${link.source.id}`;
1921
+ let linkOptions;
1922
+ if (this.linkSettings.cache && this.linkOptionsCache[id]) {
1923
+ linkOptions = this.linkOptionsCache[id];
1924
+ }
1925
+ else {
1926
+ linkOptions = linkIterationExtractor(link, index, this.links, state, this.linkSettings.options ?? {}, linkOptionsGetter);
1927
+ if (this.linkSettings.cache) {
1928
+ this.linkOptionsCache[id] = linkOptions;
1929
+ }
1930
+ }
1931
+ if (linkOptions.drawLink) {
1932
+ linkOptions.drawLink(link, linkOptions, state);
1933
+ return;
1934
+ }
1935
+ let alpha = linkOptions.alpha;
1936
+ let arrowAlpha = this.linkSettings.arrowByHighlight ? 0 : linkOptions.arrowAlpha;
1937
+ /** NODE HIGHLIGHT */
1938
+ if (this.highlightedNeighbors && this.highlightedNode) {
1939
+ /** Not highlighted */
1940
+ if (this.highlightedNode.id != link.source.id && this.highlightedNode.id != link.target.id) {
1941
+ if (this.linkSettings.highlightByNodeLinkFading) {
1942
+ const min = this.linkSettings.highlightByNodeLinkFadingMin < alpha
1943
+ ? this.linkSettings.highlightByNodeLinkFadingMin
1944
+ : alpha;
1945
+ alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
1946
+ }
1947
+ if (this.linkSettings.arrow &&
1948
+ this.linkSettings.highlightByNodeArrowFading &&
1949
+ !this.linkSettings.arrowByHighlight) {
1950
+ const min = this.linkSettings.highlightByNodeArrowFadingMin < arrowAlpha
1951
+ ? this.linkSettings.highlightByNodeArrowFadingMin
1952
+ : arrowAlpha;
1953
+ arrowAlpha = animationByProgress(min, arrowAlpha - min, 1 - this.highlightProgress);
1954
+ }
1955
+ }
1956
+ else {
1957
+ // eslint-disable-next-line no-lonely-if
1958
+ if (this.linkSettings.arrow && this.linkSettings.arrowByHighlight) {
1959
+ /** Highlighted */
1960
+ arrowAlpha = animationByProgress(0, linkOptions.arrowAlpha, this.highlightProgress);
1961
+ }
1962
+ }
1963
+ }
1964
+ /** LINK HIGHLIGHT */
1965
+ if (this.highlightedNeighbors && this.highlightedLink) {
1966
+ /** Not highlighted */
1967
+ if (this.highlightedLink !== link) {
1968
+ if (this.linkSettings.highlightByLinkLinkFading) {
1969
+ const min = this.linkSettings.highlightByLinkLinkFadingMin < alpha
1970
+ ? this.linkSettings.highlightByLinkLinkFadingMin
1971
+ : alpha;
1972
+ alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
1973
+ }
1974
+ if (this.linkSettings.arrow &&
1975
+ this.linkSettings.highlightByLinkArrowFading &&
1976
+ !this.linkSettings.arrowByHighlight) {
1977
+ const min = this.linkSettings.highlightByLinkArrowFadingMin < arrowAlpha
1978
+ ? this.linkSettings.highlightByLinkArrowFadingMin
1979
+ : arrowAlpha;
1980
+ arrowAlpha = animationByProgress(min, arrowAlpha - min, 1 - this.highlightProgress);
1981
+ }
1982
+ }
1983
+ else {
1984
+ // eslint-disable-next-line no-lonely-if
1985
+ if (this.linkSettings.arrow && this.linkSettings.arrowByHighlight) {
1986
+ /** Highlighted */
1987
+ arrowAlpha = animationByProgress(0, linkOptions.arrowAlpha, this.highlightProgress);
1988
+ }
1989
+ }
1990
+ }
1991
+ /** Link */
1992
+ this.context.beginPath();
1993
+ this.context.globalAlpha = alpha;
1994
+ this.context.strokeStyle = linkOptions.color;
1995
+ this.context.lineWidth = linkOptions.width;
1996
+ let xStart = link.source.x;
1997
+ let yStart = link.source.y;
1998
+ let xEnd = link.target.x;
1999
+ let yEnd = link.target.y;
2000
+ let linkDistance = 0;
2001
+ if (this.linkSettings.pretty || this.linkSettings.particleFlexSpeed) {
2002
+ const isHasArrow = this.linkSettings.arrow && arrowAlpha > 0;
2003
+ const position = calculateLinkPositionByNode(link, isHasArrow ? linkOptions.arrowSize : 0);
2004
+ if (position) {
2005
+ xStart = position.x1;
2006
+ xEnd = position.x2;
2007
+ yStart = position.y1;
2008
+ yEnd = position.y2;
2009
+ linkDistance = position.distance;
2010
+ }
2011
+ }
2012
+ this.context.moveTo(xStart, yStart);
2013
+ this.context.lineTo(xEnd, yEnd);
2014
+ this.context.stroke();
2015
+ /** Particle */
2016
+ if (this.linkSettings.particles &&
2017
+ ((this.highlightedNode &&
2018
+ (this.highlightedNode.id === link.source.id ||
2019
+ this.highlightedNode.id === link.target.id)) ||
2020
+ (this.highlightedLink && this.highlightedLink === link))) {
2021
+ const particleSteps = this.linkSettings.particleFlexSpeed
2022
+ ? linkDistance <= 0
2023
+ ? 0
2024
+ : linkDistance * this.linkSettings.particleFlexSpeedCoefficient
2025
+ : linkOptions.particleSteps;
2026
+ const particleCount = linkOptions.particleCount;
2027
+ if (!this.particles[id]) {
2028
+ const sourceId = link.source.id;
2029
+ const targetId = link.target.id;
2030
+ const particles = [];
2031
+ let prevParticle;
2032
+ for (let i = 0; i < particleCount; i++) {
2033
+ const particle = {
2034
+ step: 0,
2035
+ sourceId,
2036
+ targetId,
2037
+ prev: prevParticle,
2038
+ next: undefined,
2039
+ index: i,
2040
+ };
2041
+ if (prevParticle)
2042
+ prevParticle.next = particle;
2043
+ particles.push(particle);
2044
+ prevParticle = particle;
2045
+ }
2046
+ if (particles.length >= 2) {
2047
+ particles[0].prev = particles[particles.length - 1];
2048
+ particles[particles.length - 1].next = particles[0];
2049
+ }
2050
+ this.particles[id] = particles;
2051
+ }
2052
+ if (particleSteps !== 0) {
2053
+ this.particles[id].forEach((particle) => {
2054
+ if (!this.context)
2055
+ return;
2056
+ const distance = particleSteps / particleCount;
2057
+ getParticlePosition({
2058
+ distance,
2059
+ particle,
2060
+ totalSteps: particleSteps,
2061
+ xEnd,
2062
+ xStart,
2063
+ yEnd,
2064
+ yStart,
2065
+ });
2066
+ if (particle.x != undefined && particle.y != undefined) {
2067
+ this.context.beginPath();
2068
+ this.context.strokeStyle = linkOptions.particleBorderColor;
2069
+ this.context.lineWidth = linkOptions.particleBorderWidth;
2070
+ this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
2071
+ this.context.fillStyle = linkOptions.particleColor;
2072
+ this.context.fill();
2073
+ this.context.stroke();
2074
+ }
2075
+ });
2076
+ }
2077
+ }
2078
+ /** Arrow */
2079
+ if (this.linkSettings.arrow && arrowAlpha > 0) {
2080
+ const { x1: xStart, x2: xEnd, y1: yStart, y2: yEnd, } = calculateLinkPositionByNode(link) ?? {
2081
+ x1: 0,
2082
+ x2: 0,
2083
+ y1: 0,
2084
+ y2: 0,
2085
+ };
2086
+ this.context.beginPath();
2087
+ this.context.globalAlpha = arrowAlpha;
2088
+ this.context.strokeStyle = linkOptions.arrowBorderColor;
2089
+ this.context.lineWidth = linkOptions.arrowBorderWidth;
2090
+ this.context.fillStyle = linkOptions.arrowColor;
2091
+ const angle = Math.atan2(yEnd - yStart, xEnd - xStart);
2092
+ this.context.moveTo(xEnd, yEnd);
2093
+ this.context.lineTo(xEnd - linkOptions.arrowSize * Math.cos(angle - Math.PI / 6), yEnd - linkOptions.arrowSize * Math.sin(angle - Math.PI / 6));
2094
+ this.context.lineTo(xEnd - linkOptions.arrowSize * Math.cos(angle + Math.PI / 6), yEnd - linkOptions.arrowSize * Math.sin(angle + Math.PI / 6));
2095
+ this.context.closePath();
2096
+ this.context.fill();
2097
+ this.context.stroke();
2098
+ }
2099
+ if (linkOptions.drawExtraLink) {
2100
+ linkOptions.drawExtraLink(link, { ...linkOptions, alpha }, state);
2101
+ }
2102
+ };
2103
+ }
2104
+
2105
+ const SPACE = " ";
2106
+ function drawText({ context, id, textAlign, textColor, textFont, textStyle, textGap, textWeight, textSize, text, x, y, cachedNodeText, maxWidth, }) {
2107
+ context.font = `${textStyle} normal ${textWeight} ${textSize}px ${textFont}`;
2108
+ context.fillStyle = textColor;
2109
+ context.textAlign = textAlign;
2110
+ if (cachedNodeText[id] != undefined) {
2111
+ cachedNodeText[id].forEach((line, index) => {
2112
+ context.fillText(line, x, y + index * textSize + index * textGap);
2113
+ });
2114
+ return;
2115
+ }
2116
+ if (maxWidth == undefined || context.measureText(text).width <= maxWidth) {
2117
+ cachedNodeText[id] = [text];
2118
+ context.fillText(text, x, y);
2119
+ return;
2120
+ }
2121
+ const lines = getTextLines({
2122
+ context,
2123
+ maxWidth,
2124
+ text,
2125
+ textAlign,
2126
+ textColor,
2127
+ textFont,
2128
+ textSize,
2129
+ textStyle,
2130
+ textWeight,
2131
+ });
2132
+ cachedNodeText[id] = lines;
2133
+ lines.forEach((line, index) => {
2134
+ context.fillText(line, x, y + index * textSize + index * textGap);
2135
+ });
2136
+ }
2137
+ function getTextLines({ context, textAlign, textColor, textFont, textStyle, textWeight, textSize, text, maxWidth, }) {
2138
+ context.font = `${textStyle} normal ${textWeight} ${textSize}px ${textFont}`;
2139
+ context.fillStyle = textColor;
2140
+ context.textAlign = textAlign;
2141
+ const spaceWidth = context.measureText(" ").width;
2142
+ const lines = [];
2143
+ let lineWidth = 0;
2144
+ let line = "";
2145
+ for (const word of text.split(" ")) {
2146
+ const size = context.measureText(word).width;
2147
+ lineWidth += size + spaceWidth;
2148
+ if (line === "") {
2149
+ line = word;
2150
+ continue;
2151
+ }
2152
+ if (lineWidth > maxWidth) {
2153
+ lineWidth = 0;
2154
+ lines.push(line);
2155
+ line = word;
2156
+ }
2157
+ else {
2158
+ lineWidth += spaceWidth;
2159
+ line += `${SPACE}${word}`;
2160
+ }
2161
+ }
2162
+ if (line !== "")
2163
+ lines.push(line);
2164
+ return lines;
2165
+ }
2166
+
2167
+ function getDrawNode(nodeRenders, textRenders, state) {
2168
+ return function drawNode(node, index) {
2169
+ if (!this.context || !node.x || !node.y)
2170
+ return;
2171
+ let nodeOptions;
2172
+ if (this.nodeSettings.cache && this.nodeOptionsCache[node.id]) {
2173
+ nodeOptions = this.nodeOptionsCache[node.id];
2174
+ }
2175
+ else {
2176
+ nodeOptions = nodeIterationExtractor(node, index, this.nodes, state, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2177
+ if (this.nodeSettings.cache) {
2178
+ this.nodeOptionsCache[node.id] = nodeOptions;
2179
+ }
2180
+ }
2181
+ if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
2182
+ nodeRenders.push(() => {
2183
+ nodeOptions?.nodeDraw?.(node, nodeOptions, state);
2184
+ });
2185
+ textRenders.push(() => {
2186
+ nodeOptions?.textDraw?.(node, nodeOptions, state);
2187
+ });
2188
+ return;
2189
+ }
2190
+ let alpha = nodeOptions.alpha;
2191
+ let color = nodeOptions.color;
2192
+ let radiusInitial = nodeOptions.radius;
2193
+ let widthInitial = nodeOptions.width;
2194
+ let heightInitial = nodeOptions.height;
2195
+ let textAlpha = nodeOptions.textAlpha;
2196
+ let textSize = nodeOptions.textSize;
2197
+ let textShiftX = nodeOptions.textShiftX;
2198
+ let textShiftY = nodeOptions.textShiftY;
2199
+ let textWeight = nodeOptions.textWeight;
2200
+ let textWidth = nodeOptions.textWidth;
2201
+ /** Node Highlight */
2202
+ if (this.highlightedNeighbors && this.highlightedNode) {
2203
+ /** Not highlighted */
2204
+ if (!this.highlightedNeighbors.has(node.id) && this.highlightedNode.id != node.id) {
2205
+ if (this.nodeSettings.highlightByNodeNodeFading) {
2206
+ const min = this.nodeSettings.highlightByNodeNodeFadingMin < alpha
2207
+ ? this.nodeSettings.highlightByNodeNodeFadingMin
2208
+ : alpha;
2209
+ alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
2210
+ }
2211
+ if (this.nodeSettings.highlightByNodeTextFading) {
2212
+ const min = this.nodeSettings.highlightByNodeTextFadingMin < textAlpha
2213
+ ? this.nodeSettings.highlightByNodeTextFadingMin
2214
+ : textAlpha;
2215
+ textAlpha = animationByProgress(min, textAlpha - min, 1 - this.highlightProgress);
2216
+ }
2217
+ if (this.nodeSettings.highlightByNodeNodeColor) {
2218
+ const colorRgb = extractRgb(colorToRgb(color));
2219
+ if (colorRgb) {
2220
+ const colorRgbFade = fadeRgb(colorRgb, this.nodeSettings.highlightByNodeNodeColorFadingMin);
2221
+ const colorFadeAnimation = rgbAnimationByProgress(colorRgb, colorRgbFade, this.highlightProgress);
2222
+ color = `rgb(${colorFadeAnimation.r}, ${colorFadeAnimation.g}, ${colorFadeAnimation.b})`;
2223
+ }
2224
+ }
2225
+ }
2226
+ else if (!this.nodeSettings.highlightByNodeOnlyRoot ||
2227
+ (this.nodeSettings.highlightByNodeOnlyRoot && this.highlightedNode.id === node.id)) {
2228
+ /** Highlighted */
2229
+ if (this.nodeSettings.highlightByNodeNodeSizing && nodeOptions.shape === "circle") {
2230
+ radiusInitial = animationByProgress(radiusInitial, this.nodeSettings.highlightByNodeNodeSizingAdditional, this.highlightProgress);
2231
+ }
2232
+ if (this.nodeSettings.highlightByLinkNodeSizing &&
2233
+ (nodeOptions.shape === "square" || nodeOptions.shape === "text")) {
2234
+ const widthCoefficient = animationByProgress(1, this.nodeSettings.highlightByNodeNodeSizingAdditionalCoefficient, this.highlightProgress);
2235
+ const heightCoefficient = animationByProgress(1, this.nodeSettings.highlightByNodeNodeSizingAdditionalCoefficient, this.highlightProgress);
2236
+ widthInitial *= widthCoefficient;
2237
+ heightInitial *= heightCoefficient;
2238
+ }
2239
+ if (this.nodeSettings.highlightByNodeTextSizing) {
2240
+ textSize = animationByProgress(textSize, this.nodeSettings.highlightByNodeTextSizingAdditional, this.highlightProgress);
2241
+ textShiftX = animationByProgress(textShiftX, this.nodeSettings.highlightByNodeTextShiftXAdditional, this.highlightProgress);
2242
+ textShiftY = animationByProgress(textShiftY, this.nodeSettings.highlightByNodeTextShiftYAdditional, this.highlightProgress);
2243
+ textWeight = animationByProgress(textWeight, this.nodeSettings.highlightByNodeTextWeightAdditional, this.highlightProgress);
2244
+ textWidth = animationByProgress(textWidth, this.nodeSettings.highlightByNodeTextWidthAdditional, this.highlightProgress);
2245
+ }
2246
+ }
2247
+ }
2248
+ /** LinkHighlight */
2249
+ if (this.highlightedNeighbors && this.highlightedLink) {
2250
+ /** Not highlighted */
2251
+ if (!this.highlightedNeighbors.has(node.id) &&
2252
+ this.highlightedLink.source !== node &&
2253
+ this.highlightedLink.target !== node) {
2254
+ if (this.nodeSettings.highlightByLinkNodeFading) {
2255
+ const min = this.nodeSettings.highlightByLinkNodeFadingMin < alpha
2256
+ ? this.nodeSettings.highlightByLinkNodeFadingMin
2257
+ : alpha;
2258
+ alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
2259
+ }
2260
+ if (this.nodeSettings.highlightByLinkTextFading) {
2261
+ const min = this.nodeSettings.highlightByLinkTextFadingMin < textAlpha
2262
+ ? this.nodeSettings.highlightByLinkTextFadingMin
2263
+ : textAlpha;
2264
+ textAlpha = animationByProgress(min, textAlpha - min, 1 - this.highlightProgress);
2265
+ }
2266
+ if (this.nodeSettings.highlightByLinkNodeColor) {
2267
+ const colorRgb = extractRgb(colorToRgb(color));
2268
+ if (colorRgb) {
2269
+ const colorRgbFade = fadeRgb(colorRgb, this.nodeSettings.highlightByLinkNodeColorFadingMin);
2270
+ const colorFadeAnimation = rgbAnimationByProgress(colorRgb, colorRgbFade, this.highlightProgress);
2271
+ color = `rgb(${colorFadeAnimation.r}, ${colorFadeAnimation.g}, ${colorFadeAnimation.b})`;
2272
+ }
2273
+ }
2274
+ }
2275
+ else {
2276
+ /** Highlighted */
2277
+ if (this.nodeSettings.highlightByLinkNodeSizing && nodeOptions.shape === "circle") {
2278
+ radiusInitial = animationByProgress(radiusInitial, this.nodeSettings.highlightByLinkNodeSizingAdditional, this.highlightProgress);
2279
+ }
2280
+ if (this.nodeSettings.highlightByLinkNodeSizing &&
2281
+ (nodeOptions.shape === "square" ||
2282
+ nodeOptions.shape === "text" ||
2283
+ nodeOptions.shape === "icon")) {
2284
+ const widthCoefficient = animationByProgress(1, this.nodeSettings.highlightByLinkNodeSizingAdditionalCoefficient, this.highlightProgress);
2285
+ const heightCoefficient = animationByProgress(1, this.nodeSettings.highlightByLinkNodeSizingAdditionalCoefficient, this.highlightProgress);
2286
+ widthInitial *= widthCoefficient;
2287
+ heightInitial *= heightCoefficient;
2288
+ }
2289
+ if (this.nodeSettings.highlightByLinkTextSizing) {
2290
+ textSize = animationByProgress(textSize, this.nodeSettings.highlightByLinkTextSizingAdditional, this.highlightProgress);
2291
+ textShiftX = animationByProgress(textShiftX, this.nodeSettings.highlightByLinkTextShiftXAdditional, this.highlightProgress);
2292
+ textShiftY = animationByProgress(textShiftY, this.nodeSettings.highlightByLinkTextShiftYAdditional, this.highlightProgress);
2293
+ textWeight = animationByProgress(textWeight, this.nodeSettings.highlightByLinkTextWeightAdditional, this.highlightProgress);
2294
+ textWidth = animationByProgress(textWidth, this.nodeSettings.highlightByLinkTextWidthAdditional, this.highlightProgress);
2295
+ }
2296
+ }
2297
+ }
2298
+ /** Flex radius */
2299
+ const radius = nodeOptions.shape === "circle"
2300
+ ? nodeRadiusGetter({
2301
+ radiusFlexible: this.nodeSettings.nodeRadiusFlexible,
2302
+ radiusInitial,
2303
+ radiusCoefficient: this.nodeSettings.nodeRadiusCoefficient,
2304
+ radiusFactor: this.nodeSettings.nodeRadiusFactor,
2305
+ linkCount: node.linkCount,
2306
+ })
2307
+ : radiusInitial;
2308
+ /** Flex size */
2309
+ let height = heightInitial;
2310
+ let width = widthInitial;
2311
+ if (nodeOptions.shape === "square" || nodeOptions.shape === "text") {
2312
+ const size = nodeSizeGetter({
2313
+ heightInitial,
2314
+ widthInitial,
2315
+ linkCount: node.linkCount,
2316
+ sizeCoefficient: this.nodeSettings.nodeSizeCoefficient,
2317
+ sizeFactor: this.nodeSettings.nodeSizeFactor,
2318
+ sizeFlexible: this.nodeSettings.nodeSizeFlexible,
2319
+ });
2320
+ width = size.width;
2321
+ height = size.height;
2322
+ }
2323
+ if (nodeOptions.shape === "text" && nodeOptions.text) {
2324
+ const lines = this.cachedNodeText[node.id] ??
2325
+ getTextLines({
2326
+ context: this.context,
2327
+ text: nodeOptions.text,
2328
+ textAlign: nodeOptions.textAlign,
2329
+ textColor: nodeOptions.textColor,
2330
+ textFont: nodeOptions.textFont,
2331
+ textSize,
2332
+ maxWidth: width,
2333
+ textStyle: nodeOptions.textStyle,
2334
+ textWeight,
2335
+ });
2336
+ if (!this.cachedNodeText[node.id]) {
2337
+ this.cachedNodeText[node.id] = lines;
2338
+ }
2339
+ height =
2340
+ lines.length * textSize +
2341
+ (lines.length - 1) * nodeOptions.textGap +
2342
+ nodeOptions.textNodeYPadding;
2343
+ width += nodeOptions.textNodeXPadding;
2344
+ }
2345
+ /** Node parameters */
2346
+ node._radius = radius;
2347
+ node._width = width;
2348
+ node._height = height;
2349
+ node._borderRadius =
2350
+ jsHelpers.isNumber(nodeOptions.borderRadius) && nodeOptions.borderRadius > 0
2351
+ ? Math.min(nodeOptions.borderRadius, nodeOptions.width / 2, nodeOptions.height / 2)
2352
+ : 0;
2353
+ node._shape = nodeOptions.shape ?? "circle";
2354
+ /** Node Visibility */
2355
+ if (!isNodeVisible({
2356
+ height: this.height,
2357
+ width: this.width,
2358
+ transform: this.areaTransform,
2359
+ node,
2360
+ })) {
2361
+ node._visible = false;
2362
+ return;
2363
+ }
2364
+ node._visible = true;
2365
+ /** Node draw */
2366
+ nodeRenders.push(() => {
2367
+ if (!this.context || !node.x || !node.y)
2368
+ return;
2369
+ this.context.beginPath();
2370
+ this.context.globalAlpha = alpha;
2371
+ this.context.lineWidth = nodeOptions.borderWidth;
2372
+ this.context.strokeStyle = nodeOptions.borderColor;
2373
+ this.context.fillStyle = color;
2374
+ switch (nodeOptions.shape) {
2375
+ case "circle": {
2376
+ this.context.arc(node.x, node.y, radius, 0, 2 * Math.PI);
2377
+ break;
2378
+ }
2379
+ case "square": {
2380
+ this.context.roundRect(node.x - width / 2, node.y - height / 2, width, height, node._borderRadius);
2381
+ break;
2382
+ }
2383
+ case "text": {
2384
+ if (this.nodeSettings.textNodeDebug) {
2385
+ this.context.strokeRect(node.x - width / 2, node.y - height / 2, width, height);
2386
+ }
2387
+ const lines = this.cachedNodeText[node.id];
2388
+ if (nodeOptions.text && lines)
2389
+ drawText({
2390
+ id: node.id,
2391
+ cachedNodeText: this.cachedNodeText,
2392
+ context: this.context,
2393
+ text: nodeOptions.text,
2394
+ textAlign: nodeOptions.textAlign,
2395
+ textColor: nodeOptions.textColor,
2396
+ textFont: nodeOptions.textFont,
2397
+ textSize,
2398
+ x: node.x,
2399
+ y: node.y + textSize / 4 - (lines.length - 1) * (textSize / 2),
2400
+ maxWidth: widthInitial,
2401
+ textStyle: nodeOptions.textStyle,
2402
+ textWeight,
2403
+ textGap: nodeOptions.textGap,
2404
+ });
2405
+ break;
2406
+ }
2407
+ default: {
2408
+ this.context.arc(node.x, node.y, radius, 0, 2 * Math.PI);
2409
+ break;
2410
+ }
2411
+ }
2412
+ this.context.fill();
2413
+ this.context.stroke();
2414
+ });
2415
+ if (nodeOptions.nodeExtraDraw) {
2416
+ nodeRenders.push(() => {
2417
+ nodeOptions?.nodeExtraDraw?.(node, {
2418
+ ...nodeOptions,
2419
+ radius,
2420
+ alpha,
2421
+ color,
2422
+ textAlpha,
2423
+ textSize,
2424
+ textShiftX,
2425
+ textShiftY,
2426
+ textWeight,
2427
+ textWidth,
2428
+ }, state);
2429
+ });
2430
+ }
2431
+ /** Text draw */
2432
+ if (nodeOptions.textVisible && nodeOptions.text && nodeOptions.shape !== "text") {
2433
+ textRenders.push(() => {
2434
+ if (nodeOptions.textDraw) {
2435
+ nodeOptions.textDraw(node, {
2436
+ ...nodeOptions,
2437
+ radius,
2438
+ alpha,
2439
+ color,
2440
+ textAlpha,
2441
+ textSize,
2442
+ textShiftX,
2443
+ textShiftY,
2444
+ textWeight,
2445
+ textWidth,
2446
+ }, state);
2447
+ return;
2448
+ }
2449
+ if (!this.context || !node.x || !node.y || !nodeOptions.text)
2450
+ return;
2451
+ this.context.beginPath();
2452
+ this.context.globalAlpha = textAlpha;
2453
+ let y = node.y + textShiftY;
2454
+ if (nodeOptions.shape === "circle") {
2455
+ y += radius;
2456
+ }
2457
+ if (nodeOptions.shape === "square") {
2458
+ y += height / 2;
2459
+ }
2460
+ drawText({
2461
+ id: node.id,
2462
+ cachedNodeText: this.cachedNodeText,
2463
+ context: this.context,
2464
+ text: nodeOptions.text,
2465
+ textAlign: nodeOptions.textAlign,
2466
+ textColor: nodeOptions.textColor,
2467
+ textFont: nodeOptions.textFont,
2468
+ textSize,
2469
+ x: node.x + textShiftX,
2470
+ y,
2471
+ maxWidth: textWidth,
2472
+ textStyle: nodeOptions.textStyle,
2473
+ textWeight,
2474
+ textGap: nodeOptions.textGap,
2475
+ });
2476
+ if (nodeOptions.textExtraDraw) {
2477
+ nodeOptions.textExtraDraw(node, {
2478
+ ...nodeOptions,
2479
+ radius,
2480
+ alpha,
2481
+ color,
2482
+ textAlpha,
2483
+ textSize,
2484
+ textShiftX,
2485
+ textShiftY,
2486
+ textWeight,
2487
+ textWidth,
2488
+ }, state);
2489
+ }
2490
+ });
2491
+ }
2492
+ };
2493
+ }
2494
+
2495
+ function initDraw() {
2496
+ function calculateHighlight() {
2497
+ this.highlightDrawing = true;
2498
+ if (!this.highlightWorking && this.highlightProgress > 0) {
2499
+ const highlightDownStep = 1 / this.graphSettings.highlightDownFrames;
2500
+ this.highlightProgress -= highlightDownStep;
2501
+ if (!this.simulationWorking) {
2502
+ return void requestAnimationFrame(() => this.draw());
2503
+ }
2504
+ if (!this.linkSettings.particles)
2505
+ return;
2506
+ }
2507
+ if (this.highlightWorking && this.highlightProgress < 1) {
2508
+ const highlightUpStep = 1 / this.graphSettings.highlightUpFrames;
2509
+ this.highlightProgress += highlightUpStep;
2510
+ if (!this.simulationWorking) {
2511
+ return void requestAnimationFrame(() => this.draw());
2512
+ }
2513
+ if (!this.linkSettings.particles)
2514
+ return;
2515
+ }
2516
+ if (this.linkSettings.particles && this.highlightWorking && !this.simulationWorking) {
2517
+ return void requestAnimationFrame(() => this.draw());
2518
+ }
2519
+ if (!this.highlightWorking && this.highlightProgress <= 0) {
2520
+ if (this.highlightedNeighbors || this.highlightedNode || this.highlightedLink) {
2521
+ this.highlightedNeighbors = null;
2522
+ this.highlightedNode = null;
2523
+ this.highlightedLink = null;
2524
+ this.particles = {};
2525
+ if (!this.simulationWorking) {
2526
+ return void requestAnimationFrame(() => this.draw());
2527
+ }
2528
+ }
2529
+ }
2530
+ this.highlightDrawing = false;
2531
+ }
2532
+ function draw() {
2533
+ if (!this.context)
2534
+ return;
2535
+ const state = this.state;
2536
+ if (this.listeners.onDraw) {
2537
+ this.listeners.onDraw(state, (status) => {
2538
+ this.highlightDrawing = status;
2539
+ }, () => {
2540
+ if (this.highlightedNeighbors)
2541
+ this.highlightedNeighbors = null;
2542
+ if (this.highlightedNode)
2543
+ this.highlightedNode = null;
2544
+ if (this.highlightedLink)
2545
+ this.highlightedLink = null;
2546
+ });
2547
+ return;
2548
+ }
2549
+ this.context.save();
2550
+ this.context.clearRect(0, 0, this.width, this.height);
2551
+ this.context.translate(this.areaTransform.x, this.areaTransform.y);
2552
+ this.context.scale(this.areaTransform.k, this.areaTransform.k);
2553
+ const textRenders = [];
2554
+ const nodeRenders = [];
2555
+ this.nodes.forEach(getDrawNode(nodeRenders, textRenders, state).bind(this));
2556
+ /** links */
2557
+ this.links.forEach(getDrawLink(state).bind(this));
2558
+ /** nodes */
2559
+ nodeRenders.forEach((render) => render());
2560
+ textRenders.forEach((render) => render());
2561
+ this.context.restore();
2562
+ this.listeners.onDrawFinished?.(state);
2563
+ calculateHighlight.bind(this)();
2564
+ }
2565
+ if (this.graphSettings.showDrawTime) {
2566
+ return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
2567
+ }
2568
+ return draw.bind(this);
2569
+ }
2570
+
2571
+ function initPointer() {
2572
+ if (!this.area || !this.nodes)
2573
+ throw new Error("bad init data");
2574
+ function onHover(event) {
2575
+ if (!this.area)
2576
+ return;
2577
+ let currentNode;
2578
+ let currentLink;
2579
+ const checkHighlightNode = this.nodeSettings.highlightByHoverNode && !this.isDragging;
2580
+ const checkHighlightLink = this.linkSettings.highlightByHoverLink && !this.isDragging;
2581
+ if (checkHighlightNode) {
2582
+ currentNode = nodeByPointerGetter({
2583
+ areaRect: this.areaRect,
2584
+ areaTransform: this.areaTransform,
2585
+ mouseEvent: event,
2586
+ nodes: this.nodes,
2587
+ });
2588
+ }
2589
+ if (currentNode) {
2590
+ this.area.style.cursor = "pointer";
2591
+ }
2592
+ else if (checkHighlightLink) {
2593
+ currentLink = linkByPointerGetter({
2594
+ linkSettings: this.linkSettings,
2595
+ areaRect: this.areaRect,
2596
+ areaTransform: this.areaTransform,
2597
+ mouseEvent: event,
2598
+ links: this.links,
2599
+ });
2600
+ if (currentLink) {
2601
+ this.area.style.cursor = "pointer";
2602
+ }
2603
+ else {
2604
+ this.area.style.cursor = "default";
2605
+ }
2606
+ }
2607
+ else {
2608
+ this.area.style.cursor = "default";
2609
+ }
2610
+ if (currentNode && this.highlightedNode !== currentNode) {
2611
+ this.highlightedNode = currentNode;
2612
+ this.highlightedLink = null;
2613
+ this.highlightedNeighbors = new Set(this.highlightedNode?.neighbors ?? []);
2614
+ this.highlightWorking = true;
2615
+ if (!this.simulationWorking && !this.highlightDrawing)
2616
+ requestAnimationFrame(() => {
2617
+ this.draw();
2618
+ });
2619
+ }
2620
+ else if (currentLink &&
2621
+ checkType(currentLink.source, lodash.isObject(currentLink.source)) &&
2622
+ checkType(currentLink.target, lodash.isObject(currentLink.target)) &&
2623
+ this.highlightedLink !== currentLink) {
2624
+ this.highlightProgress = 0;
2625
+ this.highlightedLink = currentLink;
2626
+ this.highlightedNode = null;
2627
+ this.highlightedNeighbors = new Set([currentLink.source.id, currentLink.target.id]);
2628
+ this.highlightWorking = true;
2629
+ if (!this.simulationWorking && !this.highlightDrawing)
2630
+ requestAnimationFrame(() => {
2631
+ this.draw();
2632
+ });
2633
+ }
2634
+ else if (!currentNode && !currentLink && (this.highlightedNode || this.highlightedLink)) {
2635
+ this.highlightWorking = false;
2636
+ if (!this.simulationWorking && !this.highlightDrawing)
2637
+ requestAnimationFrame(() => {
2638
+ this.draw();
2639
+ });
2640
+ }
2641
+ if (!this.listeners.onMove)
2642
+ return;
2643
+ if (!currentNode && !checkHighlightNode)
2644
+ currentNode = nodeByPointerGetter({
2645
+ areaRect: this.areaRect,
2646
+ areaTransform: this.areaTransform,
2647
+ mouseEvent: event,
2648
+ nodes: this.nodes,
2649
+ });
2650
+ if (!currentNode && (!checkHighlightNode || (!checkHighlightLink && !currentLink))) {
2651
+ currentLink = linkByPointerGetter({
2652
+ linkSettings: this.linkSettings,
2653
+ areaRect: this.areaRect,
2654
+ areaTransform: this.areaTransform,
2655
+ mouseEvent: event,
2656
+ links: this.links,
2657
+ });
2658
+ }
2659
+ if (!currentNode)
2660
+ return void this.listeners.onMove(event, currentNode, currentLink);
2661
+ }
2662
+ function onWheelClick(event) {
2663
+ if (this.isDragging ||
2664
+ !this.listeners.onWheelClick ||
2665
+ !("button" in event) ||
2666
+ event.button !== 1)
2667
+ return;
2668
+ const currentNode = nodeByPointerGetter({
2669
+ areaRect: this.areaRect,
2670
+ areaTransform: this.areaTransform,
2671
+ mouseEvent: event,
2672
+ nodes: this.nodes,
2673
+ });
2674
+ if (!currentNode) {
2675
+ const currentLink = linkByPointerGetter({
2676
+ linkSettings: this.linkSettings,
2677
+ areaRect: this.areaRect,
2678
+ areaTransform: this.areaTransform,
2679
+ mouseEvent: event,
2680
+ links: this.links,
2681
+ });
2682
+ return void this.listeners.onWheelClick(event, undefined, currentLink);
2683
+ }
2684
+ return void this.listeners.onWheelClick(event, currentNode, undefined);
2685
+ }
2686
+ function onRightClick(event) {
2687
+ if (!this.listeners.onContextMenu)
2688
+ return;
2689
+ const currentNode = nodeByPointerGetter({
2690
+ areaRect: this.areaRect,
2691
+ areaTransform: this.areaTransform,
2692
+ mouseEvent: event,
2693
+ nodes: this.nodes,
2694
+ });
2695
+ if (!currentNode) {
2696
+ const currentLink = linkByPointerGetter({
2697
+ linkSettings: this.linkSettings,
2698
+ areaRect: this.areaRect,
2699
+ areaTransform: this.areaTransform,
2700
+ mouseEvent: event,
2701
+ links: this.links,
2702
+ });
2703
+ return void this.listeners.onContextMenu(event, undefined, currentLink);
2704
+ }
2705
+ return void this.listeners.onContextMenu(event, currentNode, undefined);
2706
+ }
2707
+ function onDoubleClick(event) {
2708
+ if (!this.listeners.onDoubleClick)
2709
+ return;
2710
+ const currentNode = nodeByPointerGetter({
2711
+ areaRect: this.areaRect,
2712
+ areaTransform: this.areaTransform,
2713
+ mouseEvent: event,
2714
+ nodes: this.nodes,
2715
+ });
2716
+ if (!currentNode) {
2717
+ const currentLink = linkByPointerGetter({
2718
+ linkSettings: this.linkSettings,
2719
+ areaRect: this.areaRect,
2720
+ areaTransform: this.areaTransform,
2721
+ mouseEvent: event,
2722
+ links: this.links,
2723
+ });
2724
+ return void this.listeners.onDoubleClick(event, undefined, currentLink);
2725
+ }
2726
+ return void this.listeners.onDoubleClick(event, currentNode, undefined);
2727
+ }
2728
+ function onClick(event) {
2729
+ if (this.isDragging || !this.listeners.onClick || ("button" in event && event.button !== 0))
2730
+ return;
2731
+ const currentNode = nodeByPointerGetter({
2732
+ areaRect: this.areaRect,
2733
+ areaTransform: this.areaTransform,
2734
+ mouseEvent: event,
2735
+ nodes: this.nodes,
2736
+ });
2737
+ if (!currentNode) {
2738
+ const currentLink = linkByPointerGetter({
2739
+ linkSettings: this.linkSettings,
2740
+ areaRect: this.areaRect,
2741
+ areaTransform: this.areaTransform,
2742
+ mouseEvent: event,
2743
+ links: this.links,
2744
+ });
2745
+ return void this.listeners.onClick(event, undefined, currentLink);
2746
+ }
2747
+ return void this.listeners.onClick(event, currentNode, undefined);
2748
+ }
2749
+ /** hover */
2750
+ this.area.addEventListener("mousemove", onHover.bind(this), {
2751
+ signal: this.eventAbortController.signal,
2752
+ });
2753
+ this.area.addEventListener("touchmove", onHover.bind(this), {
2754
+ signal: this.eventAbortController.signal,
2755
+ });
2756
+ /** dblclick */
2757
+ this.area.addEventListener("dblclick", onDoubleClick.bind(this), {
2758
+ signal: this.eventAbortController.signal,
2759
+ });
2760
+ /** wheel click */
2761
+ this.area.addEventListener("mousedown", onWheelClick.bind(this), {
2762
+ signal: this.eventAbortController.signal,
2763
+ });
2764
+ /** click */
2765
+ this.area.addEventListener("click", onClick.bind(this), {
2766
+ signal: this.eventAbortController.signal,
2767
+ });
2768
+ /** right click */
2769
+ this.area.addEventListener("contextmenu", onRightClick.bind(this), {
2770
+ signal: this.eventAbortController.signal,
2771
+ });
780
2772
  }
781
- function getNodeControls(keys) {
782
- const NODE_CONTROLS = [
783
- {
784
- id: "alpha",
785
- type: "range",
786
- max: 1,
787
- min: 0,
788
- step: 0.1,
789
- label: "Прозрачность",
790
- initialValue: NODE_OPTIONS.alpha,
791
- },
792
- {
793
- id: "radius",
794
- type: "range",
795
- max: 50,
796
- min: 1,
797
- step: 1,
798
- label: "Радиус",
799
- initialValue: NODE_OPTIONS.radius,
800
- },
801
- {
802
- id: "borderWidth",
803
- type: "range",
804
- max: 5,
805
- min: 0,
806
- step: 0.1,
807
- label: "Толщина границы",
808
- initialValue: NODE_OPTIONS.borderWidth,
809
- },
810
- {
811
- id: "borderColor",
812
- type: "color",
813
- initialValue: "#000000FF",
814
- label: "Цвет границы",
815
- },
816
- {
817
- id: "color",
818
- type: "color",
819
- initialValue: "#000000FF",
820
- label: "Цвет",
821
- },
822
- {
823
- id: "textAlpha",
824
- type: "range",
825
- max: 1,
826
- min: 0,
827
- step: 0.1,
828
- label: "Прозрачность текста",
829
- initialValue: NODE_OPTIONS.textAlpha,
830
- },
831
- {
832
- id: "textWidth",
833
- type: "range",
834
- max: 200,
835
- min: 5,
836
- step: 1,
837
- label: "Ширина текста",
838
- initialValue: NODE_OPTIONS.textWidth,
839
- },
840
- {
841
- id: "textShiftX",
842
- type: "range",
843
- max: 100,
844
- min: 0,
845
- step: 0.1,
846
- label: "Смещение X текста",
847
- initialValue: NODE_OPTIONS.textShiftX,
848
- },
849
- {
850
- id: "textShiftY",
851
- type: "range",
852
- max: 100,
853
- min: 0,
854
- step: 0.1,
855
- label: "Смещение Y текста",
856
- initialValue: 4,
857
- },
858
- {
859
- id: "textWeight",
860
- type: "range",
861
- max: 1000,
862
- min: 100,
863
- step: 100,
864
- label: "Жирность текста",
865
- initialValue: NODE_OPTIONS.textWeight,
866
- },
867
- {
868
- id: "textGap",
869
- type: "range",
870
- max: 30,
871
- min: 0,
872
- step: 0.1,
873
- label: "Расстояние между строками",
874
- initialValue: NODE_OPTIONS.textGap,
875
- },
876
- {
877
- id: "highlightFading",
878
- type: "checkbox",
879
- initialValue: NODE_OPTIONS.highlightFading,
880
- label: "Анимация затухания",
881
- },
882
- {
883
- id: "highlightColor",
884
- type: "checkbox",
885
- initialValue: NODE_OPTIONS.highlightColor,
886
- label: "Анимация затухания цвета",
887
- },
888
- {
889
- id: "highlightSizing",
890
- type: "checkbox",
891
- initialValue: NODE_OPTIONS.highlightSizing,
892
- label: "Анимация размера",
893
- },
894
- {
895
- id: "highlightTextFading",
896
- type: "checkbox",
897
- initialValue: NODE_OPTIONS.highlightTextFading,
898
- label: "Анимация затухания текста",
899
- },
900
- {
901
- id: "highlightTextSizing",
902
- type: "checkbox",
903
- initialValue: NODE_OPTIONS.highlightTextSizing,
904
- label: "Анимация размера текста",
905
- },
906
- ];
907
- return keys ? NODE_CONTROLS.filter((control) => keys.includes(control.id)) : NODE_CONTROLS;
2773
+
2774
+ function initResize() {
2775
+ if (!this.area)
2776
+ throw new Error("bad init data");
2777
+ let initialResizeCall = true;
2778
+ const abortController = this.eventAbortController;
2779
+ const observer = new ResizeObserver(() => {
2780
+ if (initialResizeCall) {
2781
+ initialResizeCall = false;
2782
+ return;
2783
+ }
2784
+ if (abortController.signal.aborted) {
2785
+ observer.disconnect();
2786
+ return;
2787
+ }
2788
+ requestAnimationFrame(() => {
2789
+ this.updateSize();
2790
+ });
2791
+ });
2792
+ document.addEventListener("scroll", this.updateRect.bind(this), {
2793
+ capture: true,
2794
+ passive: true,
2795
+ signal: abortController.signal,
2796
+ });
2797
+ observer.observe(this.area);
908
2798
  }
909
- function getLinkControls(keys) {
910
- const LINK_CONTROLS = [
911
- {
912
- id: "alpha",
913
- type: "range",
914
- max: 1,
915
- min: 0,
916
- step: 0.1,
917
- label: "Прозрачность",
918
- initialValue: LINK_OPTIONS.alpha,
919
- },
920
- {
921
- id: "width",
922
- type: "range",
923
- max: 10,
924
- min: 0.1,
925
- step: 0.1,
926
- label: "Толщина",
927
- initialValue: 0.1,
928
- },
929
- {
930
- id: "arrowAlpha",
931
- type: "range",
932
- max: 1,
933
- min: 0,
934
- step: 0.1,
935
- label: "Прозрачность стрелки",
936
- initialValue: LINK_OPTIONS.arrowAlpha,
937
- },
938
- {
939
- id: "arrowSize",
940
- type: "range",
941
- max: 10,
942
- min: 0.1,
943
- step: 0.1,
944
- label: "Размер стрелки",
945
- initialValue: LINK_OPTIONS.arrowSize,
946
- },
947
- {
948
- id: "particleAlpha",
949
- type: "range",
950
- max: 1,
951
- min: 0,
952
- step: 0.1,
953
- label: "Прозрачность частиц",
954
- initialValue: LINK_OPTIONS.particleAlpha,
955
- },
956
- {
957
- id: "particleRadius",
958
- type: "range",
959
- max: 10,
960
- min: 0.1,
961
- step: 0.1,
962
- label: "Радиус частиц",
963
- initialValue: LINK_OPTIONS.particleRadius,
964
- },
965
- {
966
- id: "particleCount",
967
- type: "range",
968
- max: 20,
969
- min: 1,
970
- step: 1,
971
- label: "Количество частиц",
972
- initialValue: LINK_OPTIONS.particleCount,
973
- },
974
- {
975
- id: "particleSteps",
976
- type: "range",
977
- max: 200,
978
- min: 1,
979
- step: 1,
980
- label: "Количество кадров у частиц",
981
- initialValue: LINK_OPTIONS.particleSteps,
982
- },
983
- {
984
- id: "color",
985
- type: "color",
986
- initialValue: "#000000FF",
987
- label: "Цвет",
988
- },
989
- {
990
- id: "arrowColor",
991
- type: "color",
992
- initialValue: "#000000FF",
993
- label: "Цвет стрелки",
994
- },
995
- {
996
- id: "particleColor",
997
- type: "color",
998
- initialValue: "#000000FF",
999
- label: "Цвет частиц",
1000
- },
1001
- {
1002
- id: "highlightFading",
1003
- type: "checkbox",
1004
- label: "Анимация Затухания",
1005
- initialValue: LINK_OPTIONS.highlightFading,
1006
- },
1007
- {
1008
- id: "pretty",
1009
- type: "checkbox",
1010
- label: "Обсчет соединения",
1011
- initialValue: LINK_OPTIONS.pretty,
1012
- },
1013
- {
1014
- id: "arrow",
1015
- type: "checkbox",
1016
- label: "Стрелка",
1017
- initialValue: LINK_OPTIONS.arrow,
1018
- },
1019
- {
1020
- id: "arrowHighlightFading",
1021
- type: "checkbox",
1022
- label: "Анимация затухания стрелки",
1023
- initialValue: LINK_OPTIONS.arrowHighlightFading,
1024
- },
1025
- {
1026
- id: "arrowReverseAppear",
1027
- type: "checkbox",
1028
- label: "Появление стрелки при анимации",
1029
- initialValue: LINK_OPTIONS.arrowReverseAppear,
1030
- },
1031
- ];
1032
- return keys ? LINK_CONTROLS.filter((control) => keys.includes(control.id)) : LINK_CONTROLS;
2799
+
2800
+ function initSimulation() {
2801
+ if (!this.simulation) {
2802
+ this.simulation = d3Force.forceSimulation()
2803
+ .nodes(this.nodes)
2804
+ .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter))
2805
+ .on("tick", () => {
2806
+ this.draw();
2807
+ })
2808
+ .on("end", () => {
2809
+ this.listeners.onSimulationEnd?.(this.state);
2810
+ if (this.graphSettings.showDrawTime) {
2811
+ getDrawTime();
2812
+ // eslint-disable-next-line no-console
2813
+ console.log(`nodes: ${this.nodes.length} | links: ${this.links.length}`);
2814
+ resetDrawTime();
2815
+ }
2816
+ });
2817
+ initSimulationForces.call(this);
2818
+ }
2819
+ }
2820
+ function initSimulationForces() {
2821
+ if (!this.simulation)
2822
+ return;
2823
+ const linkForce = this.simulation.force("link");
2824
+ if (!linkForce)
2825
+ return;
2826
+ if ((!this.forceSettings.forces || !this.forceSettings.xForce) && this.simulation.force("x")) {
2827
+ this.simulation.force("x", null);
2828
+ }
2829
+ if ((!this.forceSettings.forces || !this.forceSettings.yForce) && this.simulation.force("y")) {
2830
+ this.simulation.force("y", null);
2831
+ }
2832
+ if ((!this.forceSettings.forces || !this.forceSettings.chargeForce) &&
2833
+ this.simulation.force("charge")) {
2834
+ this.simulation.force("charge", null);
2835
+ }
2836
+ if ((!this.forceSettings.forces || !this.forceSettings.centerForce) &&
2837
+ this.simulation.force("center")) {
2838
+ this.simulation.force("center", null);
2839
+ }
2840
+ if (!this.forceSettings.forces || !this.forceSettings.linkForce) {
2841
+ linkForce.distance(0).strength(0).iterations(0);
2842
+ }
2843
+ if (this.forceSettings.forces && this.forceSettings.linkForce) {
2844
+ linkForce
2845
+ .distance(this.forceSettings.linkDistance)
2846
+ .strength(this.forceSettings.linkStrength)
2847
+ .iterations(this.forceSettings.linkIterations);
2848
+ }
2849
+ if (this.forceSettings.forces && this.forceSettings.xForce) {
2850
+ this.simulation.force("x", d3Force.forceX(this.forceSettings.xPosition).strength(this.forceSettings.xStrength));
2851
+ }
2852
+ if (this.forceSettings.forces && this.forceSettings.yForce) {
2853
+ this.simulation.force("y", d3Force.forceY(this.forceSettings.yPosition).strength(this.forceSettings.yStrength));
2854
+ }
2855
+ if (this.forceSettings.forces && this.forceSettings.chargeForce) {
2856
+ this.simulation.force("charge", d3Force.forceManyBody()
2857
+ .strength(this.forceSettings.chargeStrength)
2858
+ .distanceMax(this.forceSettings.chargeDistanceMax)
2859
+ .distanceMin(this.forceSettings.chargeDistanceMin));
2860
+ }
2861
+ if (this.forceSettings.forces && this.forceSettings.centerForce) {
2862
+ this.simulation.force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
2863
+ }
2864
+ initCollideForce.call(this, true);
1033
2865
  }
1034
-
1035
- let countTime = 0;
1036
- let countOperation = 0;
1037
- function setDrawTime(cb, showEveryTick) {
1038
- return () => {
1039
- const start = performance.now();
1040
- cb();
1041
- const end = performance.now();
1042
- const result = end - start;
1043
- countTime += result;
1044
- countOperation++;
1045
- if (showEveryTick) {
1046
- const average = countTime / countOperation;
1047
- // eslint-disable-next-line no-console
1048
- console.log("average draw: ", average, "current draw: ", result);
2866
+ function initCollideForce(forceUpdate) {
2867
+ if (!this.simulation)
2868
+ return;
2869
+ if ((!this.forceSettings.collideForce || !this.forceSettings.forces) &&
2870
+ this.simulation.force("collide")) {
2871
+ this.simulation.force("collide", null);
2872
+ }
2873
+ if (this.forceSettings.forces && this.forceSettings.collideForce) {
2874
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
2875
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
2876
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
2877
+ if (isMaxCollideNodes && isMaxCollideLinks) {
2878
+ this.simulation.force("collide", null);
1049
2879
  }
1050
- };
2880
+ else if (!this.simulation.force("collide") || forceUpdate) {
2881
+ this.simulation.force("collide", d3Force.forceCollide()
2882
+ .radius((node, index) => {
2883
+ const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.state, this.nodeSettings.options ?? {}, nodeOptionsGetter);
2884
+ switch (nodeOptions.shape) {
2885
+ case "circle": {
2886
+ if (this.forceSettings.collideRadius) {
2887
+ return nodeIterationExtractor(node, index, this.nodes, this.state, this.forceSettings.collideRadius, undefined);
2888
+ }
2889
+ const radius = nodeRadiusGetter({
2890
+ radiusFlexible: this.nodeSettings.nodeRadiusFlexible,
2891
+ radiusInitial: nodeOptions.radius,
2892
+ radiusCoefficient: this.nodeSettings.nodeRadiusCoefficient,
2893
+ radiusFactor: this.nodeSettings.nodeRadiusFactor,
2894
+ linkCount: node.linkCount,
2895
+ });
2896
+ return radius + this.forceSettings.collideAdditionalRadius;
2897
+ }
2898
+ case "square": {
2899
+ const { height, width } = nodeSizeGetter({
2900
+ heightInitial: nodeOptions.width,
2901
+ widthInitial: nodeOptions.height,
2902
+ linkCount: node.linkCount,
2903
+ sizeCoefficient: this.nodeSettings.nodeSizeCoefficient,
2904
+ sizeFactor: this.nodeSettings.nodeSizeFactor,
2905
+ sizeFlexible: this.nodeSettings.nodeSizeFlexible,
2906
+ });
2907
+ return (Math.sqrt(width ** 2 + height ** 2) / 2 +
2908
+ this.forceSettings.collideAdditionalRadius);
2909
+ }
2910
+ case "text": {
2911
+ let { height, width } = nodeSizeGetter({
2912
+ heightInitial: nodeOptions.width,
2913
+ widthInitial: nodeOptions.height,
2914
+ linkCount: node.linkCount,
2915
+ sizeCoefficient: this.nodeSettings.nodeSizeCoefficient,
2916
+ sizeFactor: this.nodeSettings.nodeSizeFactor,
2917
+ sizeFlexible: this.nodeSettings.nodeSizeFlexible,
2918
+ });
2919
+ if (this.context && nodeOptions.text) {
2920
+ const lines = this.cachedNodeText[node.id] ??
2921
+ getTextLines({
2922
+ context: this.context,
2923
+ text: nodeOptions.text,
2924
+ textAlign: nodeOptions.textAlign,
2925
+ textColor: nodeOptions.textColor,
2926
+ textFont: nodeOptions.textFont,
2927
+ textSize: nodeOptions.textSize,
2928
+ maxWidth: width,
2929
+ textStyle: nodeOptions.textStyle,
2930
+ textWeight: nodeOptions.textWeight,
2931
+ });
2932
+ height =
2933
+ lines.length * nodeOptions.textSize +
2934
+ (lines.length - 1) * nodeOptions.textGap +
2935
+ nodeOptions.textNodeYPadding;
2936
+ width += nodeOptions.textNodeXPadding;
2937
+ }
2938
+ return (Math.sqrt(width ** 2 + height ** 2) / 2 +
2939
+ this.forceSettings.collideAdditionalRadius);
2940
+ }
2941
+ default: {
2942
+ if (this.forceSettings.collideRadius) {
2943
+ return nodeIterationExtractor(node, index, this.nodes, this.state, this.forceSettings.collideRadius, undefined);
2944
+ }
2945
+ const radius = nodeRadiusGetter({
2946
+ radiusFlexible: this.nodeSettings.nodeRadiusFlexible,
2947
+ radiusInitial: nodeOptions.radius,
2948
+ radiusCoefficient: this.nodeSettings.nodeRadiusCoefficient,
2949
+ radiusFactor: this.nodeSettings.nodeRadiusFactor,
2950
+ linkCount: node.linkCount,
2951
+ });
2952
+ return radius + this.forceSettings.collideAdditionalRadius;
2953
+ }
2954
+ }
2955
+ })
2956
+ .strength(this.forceSettings.collideStrength)
2957
+ .iterations(this.forceSettings.collideIterations));
2958
+ }
2959
+ }
1051
2960
  }
1052
- function getDrawTime() {
1053
- const average = countTime / countOperation;
1054
- // eslint-disable-next-line no-console
1055
- console.log("average draw: ", average);
2961
+
2962
+ function initZoom(currentZoom) {
2963
+ if (!this.area)
2964
+ throw new Error("bad init data");
2965
+ const zoomInstance = d3Zoom.zoom()
2966
+ .scaleExtent(this.graphSettings.zoomExtent)
2967
+ .on("zoom", (event) => {
2968
+ this.listeners.onZoom?.(event);
2969
+ this.areaTransform = event.transform;
2970
+ this.linkOptionsCache = {};
2971
+ this.nodeOptionsCache = {};
2972
+ if (!this.simulationWorking && !this.highlightWorking)
2973
+ requestAnimationFrame(() => this.draw());
2974
+ });
2975
+ if (this.graphSettings.translateExtentEnable) {
2976
+ const coefficient = this.graphSettings.translateExtentCoefficient;
2977
+ const [coefficientX, coefficientY] = jsHelpers.isArray(coefficient)
2978
+ ? coefficient
2979
+ : [coefficient, coefficient];
2980
+ const [[minX = -this.width * coefficientX, minY = -this.height * coefficientY], [maxX = this.width * coefficientX, maxY = this.height * coefficientY],] = this.graphSettings.translateExtent;
2981
+ zoomInstance.translateExtent([
2982
+ [minX, minY],
2983
+ [maxX, maxY],
2984
+ ]);
2985
+ }
2986
+ d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
2987
+ const zoomInitial = currentZoom ?? this.graphSettings.zoomInitial;
2988
+ this.areaTransform = new d3Zoom.ZoomTransform(zoomInitial?.k ?? 1, zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
2989
+ d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
1056
2990
  }
1057
2991
 
1058
2992
  class GraphCanvas {
@@ -1083,6 +3017,7 @@ class GraphCanvas {
1083
3017
  nodeOptionsCache = {};
1084
3018
  isDragging = false;
1085
3019
  highlightedNode = null;
3020
+ highlightedLink = null;
1086
3021
  highlightedNeighbors = null;
1087
3022
  highlightProgress = 1;
1088
3023
  highlightWorking = false;
@@ -1104,6 +3039,7 @@ class GraphCanvas {
1104
3039
  highlightDrawing: this.highlightDrawing,
1105
3040
  highlightedNeighbors: this.highlightedNeighbors,
1106
3041
  highlightedNode: this.highlightedNode,
3042
+ highlightedLink: this.highlightedLink,
1107
3043
  highlightWorking: this.highlightWorking,
1108
3044
  isDragging: this.isDragging,
1109
3045
  simulation: this.simulation,
@@ -1132,7 +3068,7 @@ class GraphCanvas {
1132
3068
  this.links = links;
1133
3069
  this.height = 0;
1134
3070
  this.width = 0;
1135
- this.draw = this.initDraw();
3071
+ this.draw = initDraw.call(this);
1136
3072
  this.init();
1137
3073
  }
1138
3074
  get dpi() {
@@ -1155,8 +3091,8 @@ class GraphCanvas {
1155
3091
  changeSettings(options) {
1156
3092
  if (options.graphSettings) {
1157
3093
  this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
1158
- this.draw = this.initDraw();
1159
- this.initZoom(this.areaTransform);
3094
+ this.draw = initDraw.call(this);
3095
+ initZoom.call(this, this.areaTransform);
1160
3096
  }
1161
3097
  if (options.forceSettings) {
1162
3098
  this.forceSettings = forceSettingsGetter(options.forceSettings, this.forceSettings);
@@ -1169,6 +3105,7 @@ class GraphCanvas {
1169
3105
  this.nodeSettings = nodeSettingsGetter(options.nodeSettings, this.nodeSettings);
1170
3106
  this.cachedNodeText = {};
1171
3107
  this.nodeOptionsCache = {};
3108
+ initCollideForce.call(this, true);
1172
3109
  }
1173
3110
  if (options.forceSettings) {
1174
3111
  return void this.updateSimulation();
@@ -1226,7 +3163,7 @@ class GraphCanvas {
1226
3163
  }
1227
3164
  updateSimulation() {
1228
3165
  if (this.simulation) {
1229
- this.initSimulationForces();
3166
+ initSimulationForces.call(this);
1230
3167
  this.simulation.alpha(1);
1231
3168
  this.simulation.restart();
1232
3169
  }
@@ -1234,6 +3171,7 @@ class GraphCanvas {
1234
3171
  clearState() {
1235
3172
  this.isDragging = false;
1236
3173
  this.highlightedNode = null;
3174
+ this.highlightedLink = null;
1237
3175
  this.highlightedNeighbors = null;
1238
3176
  this.highlightProgress = 0;
1239
3177
  this.highlightWorking = false;
@@ -1247,757 +3185,64 @@ class GraphCanvas {
1247
3185
  updateData(alpha) {
1248
3186
  this.clearDataDependencies();
1249
3187
  if (this.simulation) {
1250
- this.initCollideForce();
3188
+ initCollideForce.call(this, false);
1251
3189
  this.simulation
1252
3190
  .nodes(this.nodes)
1253
3191
  .force("link", d3Force.forceLink(this.links)
1254
3192
  .id(this.nodeSettings.idGetter)
1255
- .distance(this.forceSettings.linkDistance)
1256
- .strength(this.forceSettings.linkStrength)
1257
- .iterations(this.forceSettings.linkIterations))
3193
+ .distance(this.forceSettings.forces && this.forceSettings.linkForce
3194
+ ? this.forceSettings.linkDistance
3195
+ : 0)
3196
+ .strength(this.forceSettings.forces && this.forceSettings.linkForce
3197
+ ? this.forceSettings.linkStrength
3198
+ : 0)
3199
+ .iterations(this.forceSettings.forces && this.forceSettings.linkForce
3200
+ ? this.forceSettings.linkIterations
3201
+ : 0))
1258
3202
  .alpha(alpha ?? 0.5)
1259
3203
  .restart();
1260
3204
  }
1261
3205
  }
1262
3206
  updateSize() {
1263
3207
  this.clearHTMLElements();
1264
- this.initArea();
1265
- this.initDnd();
1266
- this.initZoom();
1267
- this.initResize();
1268
- this.initPointer();
3208
+ initArea.call(this);
3209
+ initDnd.call(this);
3210
+ initZoom.call(this);
3211
+ initResize.call(this);
3212
+ initPointer.call(this);
1269
3213
  if (!this.simulationWorking && !this.highlightWorking)
1270
3214
  this.draw();
1271
3215
  }
1272
3216
  init() {
1273
- this.initArea();
1274
- this.initSimulation();
1275
- this.initDnd();
1276
- this.initZoom();
1277
- this.initResize();
1278
- this.initPointer();
1279
- }
1280
- initSimulation() {
1281
- if (!this.simulation) {
1282
- this.simulation = d3Force.forceSimulation()
1283
- .nodes(this.nodes)
1284
- .force("link", d3Force.forceLink(this.links).id(this.nodeSettings.idGetter))
1285
- .on("tick", () => {
1286
- this.draw();
1287
- })
1288
- .on("end", () => {
1289
- this.listeners.onSimulationEnd?.(this.state);
1290
- if (this.graphSettings.showDrawTime)
1291
- getDrawTime();
1292
- });
1293
- this.initSimulationForces();
1294
- }
1295
- }
1296
- initSimulationForces() {
1297
- if (!this.simulation)
1298
- return;
1299
- const linkForce = this.simulation.force("link");
1300
- if (!linkForce)
1301
- return;
1302
- linkForce
1303
- .distance(this.forceSettings.linkDistance)
1304
- .strength(this.forceSettings.linkStrength)
1305
- .iterations(this.forceSettings.linkIterations);
1306
- this.simulation
1307
- .force("x", d3Force.forceX(this.forceSettings.xForce).strength(this.forceSettings.xStrength))
1308
- .force("y", d3Force.forceY(this.forceSettings.yForce).strength(this.forceSettings.yStrength))
1309
- .force("charge", d3Force.forceManyBody()
1310
- .strength(this.forceSettings.chargeStrength)
1311
- .distanceMax(this.forceSettings.chargeDistanceMax)
1312
- .distanceMin(this.forceSettings.chargeDistanceMin))
1313
- .force("center", d3Force.forceCenter(this.forceSettings.centerPosition.x ?? 0, this.forceSettings.centerPosition.y ?? 0).strength(this.forceSettings.centerStrength));
1314
- this.initCollideForce(true);
1315
- }
1316
- initCollideForce(forceUpdate) {
1317
- if (!this.simulation)
1318
- return;
1319
- if (!this.forceSettings.collideOn) {
1320
- if (this.simulation.force("collide"))
1321
- this.simulation.force("collide", null);
1322
- return;
1323
- }
1324
- const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
1325
- const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
1326
- const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
1327
- if (isMaxCollideNodes && isMaxCollideLinks) {
1328
- this.simulation.force("collide", null);
1329
- }
1330
- else if (!this.simulation.force("collide") || forceUpdate) {
1331
- this.simulation.force("collide", d3Force.forceCollide()
1332
- .radius((node, index) => {
1333
- if (this.forceSettings.collideRadius) {
1334
- return nodeIterationExtractor(node, index, this.nodes, this.state, this.forceSettings.collideRadius, undefined);
1335
- }
1336
- const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.state, this.nodeSettings.options ?? {}, nodeOptionsGetter);
1337
- const radius = nodeRadiusGetter({
1338
- radiusFlexible: this.graphSettings.nodeRadiusFlexible,
1339
- radiusInitial: nodeOptions.radius ?? this.graphSettings.nodeRadiusInitial,
1340
- radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
1341
- radiusFactor: this.graphSettings.nodeRadiusFactor,
1342
- linkCount: node.linkCount,
1343
- });
1344
- return radius + this.forceSettings.collideAdditionalRadius;
1345
- })
1346
- .strength(this.forceSettings.collideStrength)
1347
- .iterations(this.forceSettings.collideIterations));
1348
- }
1349
- }
1350
- initArea() {
1351
- if (!this.area || !this.context || !this.container) {
1352
- this.container = d3Selection.create("div")
1353
- .attr("style", "padding: 0 !important; width: 100%; height: 100%;")
1354
- .node();
1355
- if (!this.container)
1356
- throw new Error("couldn't create container");
1357
- this.root.appendChild(this.container);
1358
- const { width, height } = this.root.getBoundingClientRect();
1359
- this.width = width;
1360
- this.height = height;
1361
- this.area = d3Selection.create("canvas")
1362
- .attr("width", this.dpi * this.width)
1363
- .attr("height", this.dpi * this.height)
1364
- .attr("style", `width: 100%; height: 100%; border: none !important;`)
1365
- .node();
1366
- if (!this.area)
1367
- throw new Error("couldn't create canvas");
1368
- this.container.appendChild(this.area);
1369
- this.areaRect = this.area.getBoundingClientRect();
1370
- this.context = this.area.getContext("2d");
1371
- if (!this.context)
1372
- throw new Error("couldn't create canvas context");
1373
- this.context.scale(this.dpi, this.dpi);
1374
- }
1375
- }
1376
- initDraw() {
1377
- function calculateHighlightFading() {
1378
- this.highlightDrawing = true;
1379
- if (!this.highlightWorking && this.highlightProgress > 0) {
1380
- this.highlightProgress -= this.graphSettings.highlightDownStep;
1381
- if (!this.simulationWorking)
1382
- return void requestAnimationFrame(() => this.draw());
1383
- if (!this.linkSettings.particles)
1384
- return;
1385
- }
1386
- if (this.highlightWorking && this.highlightProgress < 1) {
1387
- this.highlightProgress += this.graphSettings.highlightUpStep;
1388
- if (!this.simulationWorking)
1389
- return void requestAnimationFrame(() => this.draw());
1390
- if (!this.linkSettings.particles)
1391
- return;
1392
- }
1393
- if (this.linkSettings.particles && this.highlightWorking && !this.simulationWorking) {
1394
- return void requestAnimationFrame(() => this.draw());
1395
- }
1396
- if (!this.highlightWorking && this.highlightProgress <= 0) {
1397
- if (this.highlightedNeighbors || this.highlightedNode) {
1398
- this.highlightedNeighbors = null;
1399
- this.highlightedNode = null;
1400
- this.particles = {};
1401
- if (!this.simulationWorking)
1402
- return void requestAnimationFrame(() => this.draw());
1403
- }
1404
- }
1405
- this.highlightDrawing = false;
1406
- }
1407
- function draw() {
1408
- if (!this.context)
1409
- return;
1410
- const state = this.state;
1411
- if (this.listeners.onDraw) {
1412
- this.listeners.onDraw(state, (status) => {
1413
- this.highlightDrawing = status;
1414
- }, () => {
1415
- if (this.highlightedNeighbors)
1416
- this.highlightedNeighbors = null;
1417
- if (this.highlightedNode)
1418
- this.highlightedNode = null;
1419
- });
1420
- return;
1421
- }
1422
- this.context.save();
1423
- this.context.clearRect(0, 0, this.width, this.height);
1424
- this.context.translate(this.areaTransform.x, this.areaTransform.y);
1425
- this.context.scale(this.areaTransform.k, this.areaTransform.k);
1426
- const textRenders = [];
1427
- const nodeRenders = [];
1428
- this.nodes.forEach(getDrawNode(nodeRenders, textRenders, state).bind(this));
1429
- /** links */
1430
- this.links.forEach(getDrawLink(state).bind(this));
1431
- /** nodes */
1432
- nodeRenders.forEach((render) => render());
1433
- textRenders.forEach((render) => render());
1434
- this.context.restore();
1435
- this.listeners.onDrawFinished?.(state);
1436
- calculateHighlightFading.bind(this)();
1437
- }
1438
- function getDrawLink(state) {
1439
- return function drawLink(link, index) {
1440
- if (!this.context ||
1441
- typeof link.source !== "object" ||
1442
- typeof link.target !== "object" ||
1443
- !link.source.x ||
1444
- !link.source.y ||
1445
- !link.target.x ||
1446
- !link.target.y)
1447
- return;
1448
- if (!link.source._visible && !link.target._visible)
1449
- return;
1450
- const id = `${link.target.id}${link.source.id}`;
1451
- let linkOptions;
1452
- if (this.linkSettings.cache && this.linkOptionsCache[id]) {
1453
- linkOptions = this.linkOptionsCache[id];
1454
- }
1455
- else {
1456
- linkOptions = linkIterationExtractor(link, index, this.links, state, this.linkSettings.options ?? {}, linkOptionsGetter);
1457
- if (this.linkSettings.cache) {
1458
- this.linkOptionsCache[id] = linkOptions;
1459
- }
1460
- }
1461
- if (linkOptions.drawLink) {
1462
- linkOptions.drawLink(link, linkOptions, state);
1463
- return;
1464
- }
1465
- let alpha = linkOptions.alpha;
1466
- let arrowAlpha = linkOptions.arrowReverseAppear ? 0 : linkOptions.arrowAlpha;
1467
- if (this.highlightedNeighbors && this.highlightedNode) {
1468
- /** Not highlighted */
1469
- if (this.highlightedNode.id != link.source.id &&
1470
- this.highlightedNode.id != link.target.id) {
1471
- if (linkOptions.highlightFading) {
1472
- const min = this.graphSettings.highlightLinkFadingMin < alpha
1473
- ? this.graphSettings.highlightLinkFadingMin
1474
- : alpha;
1475
- alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
1476
- }
1477
- if (linkOptions.arrow &&
1478
- linkOptions.arrowHighlightFading &&
1479
- !linkOptions.arrowReverseAppear) {
1480
- const min = this.graphSettings.highlightArrowFadingMin < arrowAlpha
1481
- ? this.graphSettings.highlightArrowFadingMin
1482
- : arrowAlpha;
1483
- arrowAlpha = animationByProgress(min, arrowAlpha - min, 1 - this.highlightProgress);
1484
- }
1485
- }
1486
- else {
1487
- // eslint-disable-next-line no-lonely-if
1488
- if (linkOptions.arrow && linkOptions.arrowReverseAppear) {
1489
- /** Highlighted */
1490
- arrowAlpha = animationByProgress(0, linkOptions.arrowAlpha, this.highlightProgress);
1491
- }
1492
- }
1493
- }
1494
- /** Link */
1495
- this.context.beginPath();
1496
- this.context.globalAlpha = alpha;
1497
- this.context.strokeStyle = linkOptions.color;
1498
- this.context.lineWidth = linkOptions.width;
1499
- let xStart = link.source.x;
1500
- let yStart = link.source.y;
1501
- let xEnd = link.target.x;
1502
- let yEnd = link.target.y;
1503
- if (linkOptions.pretty) {
1504
- const isHasArrow = linkOptions.arrow && arrowAlpha > 0;
1505
- const { x1, x2, y1, y2 } = calculateLinkPositionByRadius(link, isHasArrow ? linkOptions.arrowSize : 0) ?? {
1506
- x1: 0,
1507
- x2: 0,
1508
- y1: 0,
1509
- y2: 0,
1510
- };
1511
- xStart = x1;
1512
- xEnd = x2;
1513
- yStart = y1;
1514
- yEnd = y2;
1515
- }
1516
- this.context.moveTo(xStart, yStart);
1517
- this.context.lineTo(xEnd, yEnd);
1518
- this.context.stroke();
1519
- /** Particle */
1520
- if (this.linkSettings.particles &&
1521
- this.highlightedNode &&
1522
- (this.highlightedNode.id === link.source.id || this.highlightedNode.id === link.target.id)) {
1523
- if (!this.particles[id]) {
1524
- const sourceId = link.source.id;
1525
- const targetId = link.target.id;
1526
- this.particles[id] = Array.from({ length: linkOptions.particleCount }, (_, index) => {
1527
- return {
1528
- step: 0,
1529
- wait: index * (linkOptions.particleSteps / linkOptions.particleCount),
1530
- sourceId,
1531
- targetId,
1532
- };
1533
- });
1534
- }
1535
- this.particles[id].forEach((particle) => {
1536
- if (!this.context)
1537
- return;
1538
- getParticlePosition({
1539
- particle,
1540
- totalSteps: linkOptions.particleSteps,
1541
- xEnd,
1542
- xStart,
1543
- yEnd,
1544
- yStart,
1545
- });
1546
- if (particle.x != undefined && particle.y != undefined) {
1547
- this.context.beginPath();
1548
- this.context.arc(particle.x, particle.y, linkOptions.particleRadius, 0, Math.PI * 2);
1549
- this.context.fillStyle = linkOptions.particleColor;
1550
- this.context.fill();
1551
- this.context.stroke();
1552
- }
1553
- });
1554
- }
1555
- /** Arrow */
1556
- if (linkOptions.arrow && arrowAlpha > 0) {
1557
- const { x1: xStart, x2: xEnd, y1: yStart, y2: yEnd, } = calculateLinkPositionByRadius(link) ?? {
1558
- x1: 0,
1559
- x2: 0,
1560
- y1: 0,
1561
- y2: 0,
1562
- };
1563
- const angle = Math.atan2(yEnd - yStart, xEnd - xStart);
1564
- this.context.beginPath();
1565
- this.context.globalAlpha = arrowAlpha;
1566
- this.context.moveTo(xEnd, yEnd);
1567
- this.context.lineTo(xEnd - linkOptions.arrowSize * Math.cos(angle - Math.PI / 6), yEnd - linkOptions.arrowSize * Math.sin(angle - Math.PI / 6));
1568
- this.context.lineTo(xEnd - linkOptions.arrowSize * Math.cos(angle + Math.PI / 6), yEnd - linkOptions.arrowSize * Math.sin(angle + Math.PI / 6));
1569
- this.context.closePath();
1570
- this.context.fillStyle = linkOptions.arrowColor;
1571
- this.context.fill();
1572
- this.context.stroke();
1573
- }
1574
- if (linkOptions.drawExtraLink) {
1575
- linkOptions.drawExtraLink(link, { ...linkOptions, alpha }, state);
1576
- }
1577
- };
1578
- }
1579
- function getDrawNode(nodeRenders, textRenders, state) {
1580
- return function drawNode(node, index) {
1581
- if (!this.context || !node.x || !node.y)
1582
- return;
1583
- let nodeOptions;
1584
- if (this.nodeSettings.cache && this.nodeOptionsCache[node.id]) {
1585
- nodeOptions = this.nodeOptionsCache[node.id];
1586
- }
1587
- else {
1588
- nodeOptions = nodeIterationExtractor(node, index, this.nodes, state, this.nodeSettings.options ?? {}, nodeOptionsGetter);
1589
- if (this.nodeSettings.cache) {
1590
- this.nodeOptionsCache[node.id] = nodeOptions;
1591
- }
1592
- }
1593
- if (nodeOptions.nodeDraw && nodeOptions.textDraw) {
1594
- nodeRenders.push(() => {
1595
- nodeOptions?.nodeDraw?.(node, nodeOptions, state);
1596
- });
1597
- textRenders.push(() => {
1598
- nodeOptions?.textDraw?.(node, nodeOptions, state);
1599
- });
1600
- return;
1601
- }
1602
- let alpha = nodeOptions.alpha;
1603
- let color = nodeOptions.color;
1604
- let radiusInitial = nodeOptions.radius ?? this.graphSettings.nodeRadiusInitial;
1605
- let textAlpha = nodeOptions.textAlpha;
1606
- let textSize = nodeOptions.textSize;
1607
- let textShiftX = nodeOptions.textShiftX;
1608
- let textShiftY = nodeOptions.textShiftY;
1609
- let textWeight = nodeOptions.textWeight;
1610
- let textWidth = nodeOptions.textWidth;
1611
- if (this.highlightedNeighbors && this.highlightedNode) {
1612
- /** Not highlighted */
1613
- if (!this.highlightedNeighbors.has(node.id) && this.highlightedNode.id != node.id) {
1614
- if (nodeOptions.highlightFading) {
1615
- const min = this.graphSettings.highlightFadingMin < alpha
1616
- ? this.graphSettings.highlightFadingMin
1617
- : alpha;
1618
- alpha = animationByProgress(min, alpha - min, 1 - this.highlightProgress);
1619
- }
1620
- if (nodeOptions.highlightTextFading) {
1621
- const min = this.graphSettings.highlightTextFadingMin < textAlpha
1622
- ? this.graphSettings.highlightTextFadingMin
1623
- : textAlpha;
1624
- textAlpha = animationByProgress(min, textAlpha - min, 1 - this.highlightProgress);
1625
- }
1626
- if (nodeOptions.highlightColor) {
1627
- const colorRgb = extractRgb(colorToRgb(color));
1628
- if (colorRgb) {
1629
- const colorRgbFade = fadeRgb(colorRgb, this.graphSettings.highlightColorFadingMin);
1630
- const colorFadeAnimation = rgbAnimationByProgress(colorRgb, colorRgbFade, this.highlightProgress);
1631
- color = `rgb(${colorFadeAnimation.r}, ${colorFadeAnimation.g}, ${colorFadeAnimation.b})`;
1632
- }
1633
- }
1634
- }
1635
- else if (!this.graphSettings.highlightOnlyRoot ||
1636
- (this.graphSettings.highlightOnlyRoot && this.highlightedNode.id === node.id)) {
1637
- /** Highlighted */
1638
- if (nodeOptions.highlightSizing) {
1639
- radiusInitial = animationByProgress(radiusInitial, this.graphSettings.highlightSizingAdditional, this.highlightProgress);
1640
- }
1641
- if (nodeOptions.highlightTextSizing) {
1642
- textSize = animationByProgress(textSize, this.graphSettings.highlightTextSizingAdditional, this.highlightProgress);
1643
- textShiftX = animationByProgress(textShiftX, this.graphSettings.highlightTextShiftXAdditional, this.highlightProgress);
1644
- textShiftY = animationByProgress(textShiftY, this.graphSettings.highlightTextShiftYAdditional, this.highlightProgress);
1645
- textWeight = animationByProgress(textWeight, this.graphSettings.highlightTextWeightAdditional, this.highlightProgress);
1646
- textWidth = animationByProgress(textWidth, this.graphSettings.highlightTextWidthAdditional, this.highlightProgress);
1647
- }
1648
- }
1649
- }
1650
- const radius = nodeRadiusGetter({
1651
- radiusFlexible: this.graphSettings.nodeRadiusFlexible,
1652
- radiusInitial,
1653
- radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
1654
- radiusFactor: this.graphSettings.nodeRadiusFactor,
1655
- linkCount: node.linkCount,
1656
- });
1657
- node._radius = radius;
1658
- if (!isNodeVisible({
1659
- height: this.height,
1660
- width: this.width,
1661
- x: node.x,
1662
- y: node.y,
1663
- radius,
1664
- transform: this.areaTransform,
1665
- })) {
1666
- node._visible = false;
1667
- return;
1668
- }
1669
- node._visible = true;
1670
- nodeRenders.push(() => {
1671
- if (!this.context || !node.x || !node.y)
1672
- return;
1673
- this.context.beginPath();
1674
- this.context.globalAlpha = alpha;
1675
- /** circle */
1676
- this.context.lineWidth = nodeOptions.borderWidth;
1677
- this.context.strokeStyle = nodeOptions.borderColor;
1678
- this.context.fillStyle = color;
1679
- this.context.arc(node.x, node.y, radius, 0, 2 * Math.PI);
1680
- this.context.fill();
1681
- this.context.stroke();
1682
- });
1683
- if (nodeOptions.nodeExtraDraw) {
1684
- nodeRenders.push(() => {
1685
- nodeOptions?.nodeExtraDraw?.(node, {
1686
- ...nodeOptions,
1687
- radius,
1688
- alpha,
1689
- color,
1690
- textAlpha,
1691
- textSize,
1692
- textShiftX,
1693
- textShiftY,
1694
- textWeight,
1695
- textWidth,
1696
- }, state);
1697
- });
1698
- }
1699
- /** text */
1700
- if (nodeOptions.textVisible && nodeOptions.text) {
1701
- textRenders.push(() => {
1702
- if (nodeOptions.textDraw) {
1703
- nodeOptions.textDraw(node, {
1704
- ...nodeOptions,
1705
- radius,
1706
- alpha,
1707
- color,
1708
- textAlpha,
1709
- textSize,
1710
- textShiftX,
1711
- textShiftY,
1712
- textWeight,
1713
- textWidth,
1714
- }, state);
1715
- return;
1716
- }
1717
- if (!this.context || !node.x || !node.y || !nodeOptions.text)
1718
- return;
1719
- this.context.beginPath();
1720
- this.context.globalAlpha = textAlpha;
1721
- drawText({
1722
- id: node.id,
1723
- cachedNodeText: this.cachedNodeText,
1724
- context: this.context,
1725
- text: nodeOptions.text,
1726
- textAlign: nodeOptions.textAlign,
1727
- textColor: nodeOptions.textColor,
1728
- textFont: nodeOptions.textFont,
1729
- textSize,
1730
- x: node.x + textShiftX,
1731
- y: node.y + radius + textShiftY,
1732
- maxWidth: textWidth,
1733
- textStyle: nodeOptions.textStyle,
1734
- textWeight,
1735
- textGap: nodeOptions.textGap,
1736
- });
1737
- if (nodeOptions.textExtraDraw) {
1738
- nodeOptions.textExtraDraw(node, {
1739
- ...nodeOptions,
1740
- radius,
1741
- alpha,
1742
- color,
1743
- textAlpha,
1744
- textSize,
1745
- textShiftX,
1746
- textShiftY,
1747
- textWeight,
1748
- textWidth,
1749
- }, state);
1750
- }
1751
- });
1752
- }
1753
- };
1754
- }
1755
- if (this.graphSettings.showDrawTime) {
1756
- return setDrawTime(draw.bind(this), this.graphSettings.showDrawTimeEveryTick);
1757
- }
1758
- return draw;
1759
- }
1760
- initResize() {
1761
- if (!this.area)
1762
- throw new Error("bad init data");
1763
- let initialResizeCall = true;
1764
- const abortController = this.eventAbortController;
1765
- const observer = new ResizeObserver(() => {
1766
- if (initialResizeCall) {
1767
- initialResizeCall = false;
1768
- return;
1769
- }
1770
- if (abortController.signal.aborted) {
1771
- observer.disconnect();
1772
- return;
1773
- }
1774
- requestAnimationFrame(() => {
1775
- this.updateSize();
1776
- });
1777
- });
1778
- document.addEventListener("scroll", this.updateRect.bind(this), {
1779
- capture: true,
1780
- passive: true,
1781
- signal: abortController.signal,
1782
- });
1783
- observer.observe(this.area);
1784
- }
1785
- initPointer() {
1786
- if (!this.area || !this.nodes || !this.simulation)
1787
- throw new Error("bad init data");
1788
- /** hover */
1789
- this.area.addEventListener("pointermove", (event) => {
1790
- let currentNode;
1791
- if (this.graphSettings.highlightByHover && !this.isDragging) {
1792
- currentNode = nodeByPointerGetter({
1793
- graphSettings: this.graphSettings,
1794
- areaRect: this.areaRect,
1795
- areaTransform: this.areaTransform,
1796
- mouseEvent: event,
1797
- nodes: this.nodes,
1798
- });
1799
- if (currentNode && this.highlightedNode !== currentNode) {
1800
- this.highlightedNode = currentNode;
1801
- this.highlightedNeighbors = new Set(this.highlightedNode?.neighbors ?? []);
1802
- this.highlightWorking = true;
1803
- if (!this.simulationWorking && !this.highlightDrawing)
1804
- requestAnimationFrame(() => {
1805
- this.draw();
1806
- });
1807
- }
1808
- else if (!currentNode && this.highlightedNode) {
1809
- this.highlightWorking = false;
1810
- if (!this.simulationWorking && !this.highlightDrawing)
1811
- requestAnimationFrame(() => {
1812
- this.draw();
1813
- });
1814
- }
1815
- }
1816
- if (!this.listeners.onMove)
1817
- return;
1818
- if (!currentNode)
1819
- currentNode = nodeByPointerGetter({
1820
- graphSettings: this.graphSettings,
1821
- areaRect: this.areaRect,
1822
- areaTransform: this.areaTransform,
1823
- mouseEvent: event,
1824
- nodes: this.nodes,
1825
- });
1826
- return void this.listeners.onMove(event, currentNode);
1827
- }, {
1828
- signal: this.eventAbortController.signal,
1829
- });
1830
- /** dblclick */
1831
- this.area.addEventListener("dblclick", (event) => {
1832
- if (!this.listeners.onDoubleClick)
1833
- return;
1834
- const currentNode = nodeByPointerGetter({
1835
- graphSettings: this.graphSettings,
1836
- areaRect: this.areaRect,
1837
- areaTransform: this.areaTransform,
1838
- mouseEvent: event,
1839
- nodes: this.nodes,
1840
- });
1841
- return void this.listeners.onDoubleClick(event, currentNode);
1842
- });
1843
- /** wheel click */
1844
- this.area.addEventListener("mousedown", (event) => {
1845
- if (this.isDragging || !this.listeners.onWheelClick || event.button !== 1)
1846
- return;
1847
- const currentNode = nodeByPointerGetter({
1848
- graphSettings: this.graphSettings,
1849
- areaRect: this.areaRect,
1850
- areaTransform: this.areaTransform,
1851
- mouseEvent: event,
1852
- nodes: this.nodes,
1853
- });
1854
- return void this.listeners.onWheelClick(event, currentNode);
1855
- }, {
1856
- signal: this.eventAbortController.signal,
1857
- });
1858
- /** click */
1859
- this.area.addEventListener("click", (event) => {
1860
- if (this.isDragging || !this.listeners.onClick || event.button !== 0)
1861
- return;
1862
- const currentNode = nodeByPointerGetter({
1863
- graphSettings: this.graphSettings,
1864
- areaRect: this.areaRect,
1865
- areaTransform: this.areaTransform,
1866
- mouseEvent: event,
1867
- nodes: this.nodes,
1868
- });
1869
- return void this.listeners.onClick(event, currentNode);
1870
- }, {
1871
- signal: this.eventAbortController.signal,
1872
- });
1873
- /** right click */
1874
- this.area.addEventListener("contextmenu", (event) => {
1875
- if (!this.listeners.onContextMenu)
1876
- return;
1877
- const currentNode = nodeByPointerGetter({
1878
- graphSettings: this.graphSettings,
1879
- areaRect: this.areaRect,
1880
- areaTransform: this.areaTransform,
1881
- mouseEvent: event,
1882
- nodes: this.nodes,
1883
- });
1884
- return void this.listeners.onContextMenu(event, currentNode);
1885
- }, {
1886
- signal: this.eventAbortController.signal,
1887
- });
1888
- }
1889
- initDnd() {
1890
- if (!this.area || !this.nodes || !this.simulation)
1891
- throw new Error("bad init data");
1892
- d3Selection.select(this.area).call(d3Drag.drag()
1893
- .subject((event) => {
1894
- if (this.listeners.onDragSubject) {
1895
- return this.listeners.onDragSubject(event, this.state);
1896
- }
1897
- if (!this.areaRect)
1898
- return;
1899
- const mouseEvent = event.sourceEvent;
1900
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1901
- let index = 0;
1902
- return d3Array.greatest(this.nodes, (node) => {
1903
- if (!node.x || !node.y || (jsHelpers.isBoolean(node.drag) && !node.drag))
1904
- return undefined;
1905
- let radius = node._radius;
1906
- if (!radius) {
1907
- const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.state, this.nodeSettings.options ?? {}, nodeOptionsGetter);
1908
- radius = nodeRadiusGetter({
1909
- radiusFlexible: this.graphSettings.nodeRadiusFlexible,
1910
- radiusInitial: nodeOptions.radius ?? this.graphSettings.nodeRadiusInitial,
1911
- radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
1912
- radiusFactor: this.graphSettings.nodeRadiusFactor,
1913
- linkCount: node.linkCount,
1914
- });
1915
- }
1916
- index++;
1917
- return this.graphSettings.dragPlaceCoefficient(node, pointerX, pointerY, radius);
1918
- });
1919
- })
1920
- .on("start", (event) => {
1921
- this.listeners.onStartDragFinished?.(event, this.state);
1922
- })
1923
- .on("drag", (event) => {
1924
- if (!this.isDragging) {
1925
- this.isDragging = true;
1926
- if (this.simulation)
1927
- this.simulation.alphaTarget(0.3).restart();
1928
- }
1929
- if (!this.areaRect)
1930
- return;
1931
- const mouseEvent = event.sourceEvent;
1932
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1933
- event.subject.fx = pointerX;
1934
- event.subject.fy = pointerY;
1935
- this.listeners.onMoveDragFinished?.(event, this.state);
1936
- })
1937
- .on("end", (event) => {
1938
- this.isDragging = false;
1939
- if (!event.active && this.simulation)
1940
- this.simulation.alphaTarget(0);
1941
- if (this.graphSettings.stickAfterDrag && this.areaRect) {
1942
- if (!this.areaRect)
1943
- return;
1944
- const mouseEvent = event.sourceEvent;
1945
- const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
1946
- event.subject.fx = pointerX;
1947
- event.subject.fy = pointerY;
1948
- }
1949
- else {
1950
- event.subject.fx = null;
1951
- event.subject.fy = null;
1952
- }
1953
- this.listeners.onEndDragFinished?.(event, this.state);
1954
- }));
1955
- }
1956
- initZoom(currentZoom) {
1957
- if (!this.area)
1958
- throw new Error("bad init data");
1959
- const zoomInstance = d3Zoom.zoom()
1960
- .scaleExtent(this.graphSettings.zoomExtent)
1961
- .on("zoom", (event) => {
1962
- this.listeners.onZoom?.(event);
1963
- this.areaTransform = event.transform;
1964
- this.linkOptionsCache = {};
1965
- this.nodeOptionsCache = {};
1966
- if (!this.simulationWorking && !this.highlightWorking)
1967
- requestAnimationFrame(() => this.draw());
1968
- });
1969
- if (this.graphSettings.translateExtentEnable) {
1970
- const coefficient = this.graphSettings.translateExtentCoefficient;
1971
- const [coefficientX, coefficientY] = jsHelpers.isArray(coefficient)
1972
- ? coefficient
1973
- : [coefficient, coefficient];
1974
- const [[minX = -this.width * coefficientX, minY = -this.height * coefficientY], [maxX = this.width * coefficientX, maxY = this.height * coefficientY],] = this.graphSettings.translateExtent;
1975
- zoomInstance.translateExtent([
1976
- [minX, minY],
1977
- [maxX, maxY],
1978
- ]);
1979
- }
1980
- d3Selection.select(this.area).call(zoomInstance).on("dblclick.zoom", null);
1981
- const zoomInitial = currentZoom ?? this.graphSettings.zoomInitial;
1982
- this.areaTransform = new d3Zoom.ZoomTransform(zoomInitial?.k ?? 1, zoomInitial?.x ?? this.width / 2, zoomInitial?.y ?? this.height / 2);
1983
- d3Zoom.zoom().transform(d3Selection.select(this.area), this.areaTransform);
3217
+ initArea.call(this);
3218
+ initSimulation.call(this);
3219
+ initDnd.call(this);
3220
+ initZoom.call(this);
3221
+ initResize.call(this);
3222
+ initPointer.call(this);
1984
3223
  }
1985
3224
  }
1986
3225
 
1987
3226
  exports.GraphCanvas = GraphCanvas;
1988
3227
  exports.animationByProgress = animationByProgress;
1989
- exports.calculateLinkPositionByRadius = calculateLinkPositionByRadius;
3228
+ exports.calculateLinkPositionByNode = calculateLinkPositionByNode;
1990
3229
  exports.colorToRgb = colorToRgb;
1991
3230
  exports.drawText = drawText;
1992
3231
  exports.extractRgb = extractRgb;
1993
3232
  exports.fadeRgb = fadeRgb;
3233
+ exports.getDrawLink = getDrawLink;
3234
+ exports.getDrawNode = getDrawNode;
1994
3235
  exports.getForceControls = getForceControls;
1995
3236
  exports.getGraphControls = getGraphControls;
1996
- exports.getLinkControls = getLinkControls;
1997
- exports.getNodeControls = getNodeControls;
3237
+ exports.getLinkOptionsControls = getLinkOptionsControls;
3238
+ exports.getLinkSettingsControls = getLinkSettingsControls;
3239
+ exports.getNodeOptionsControls = getNodeOptionsControls;
3240
+ exports.getNodeSettingControls = getNodeSettingControls;
1998
3241
  exports.getParticlePosition = getParticlePosition;
3242
+ exports.linkByPointerGetter = linkByPointerGetter;
1999
3243
  exports.linkIterationExtractor = linkIterationExtractor;
2000
3244
  exports.linkOptionsGetter = linkOptionsGetter;
3245
+ exports.nodeByPointerGetter = nodeByPointerGetter;
2001
3246
  exports.nodeIterationExtractor = nodeIterationExtractor;
2002
3247
  exports.nodeOptionsGetter = nodeOptionsGetter;
2003
3248
  exports.nodeRadiusGetter = nodeRadiusGetter;