@krainovsd/graph 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +31 -0
  2. package/lib/esm/index.js +764 -0
  3. package/lib/esm/index.js.map +1 -0
  4. package/package.json +73 -0
  5. package/tmp/app/index.d.ts +1 -0
  6. package/tmp/app/lib/get-link-count.d.ts +3 -0
  7. package/tmp/app/lib/get-node-neighbors.d.ts +3 -0
  8. package/tmp/app/lib/index.d.ts +3 -0
  9. package/tmp/app/lib/perf-test-wrapper.d.ts +1 -0
  10. package/tmp/app/mock/custom-mock.d.ts +3 -0
  11. package/tmp/app/mock/dynamic-mock.d.ts +3 -0
  12. package/tmp/app/mock/index.d.ts +7 -0
  13. package/tmp/app/types.d.ts +4 -0
  14. package/tmp/index.d.ts +1 -0
  15. package/tmp/lib/check-type.d.ts +1 -0
  16. package/tmp/lib/color-getter.d.ts +1 -0
  17. package/tmp/lib/index.d.ts +3 -0
  18. package/tmp/lib/underscore.d.ts +2 -0
  19. package/tmp/module/Graph/Graph.d.ts +0 -0
  20. package/tmp/module/Graph/Graph.types.d.ts +0 -0
  21. package/tmp/module/Graph/index.d.ts +0 -0
  22. package/tmp/module/GraphCanvas/GraphCanvas.d.ts +49 -0
  23. package/tmp/module/GraphCanvas/constants/index.d.ts +1 -0
  24. package/tmp/module/GraphCanvas/constants/settings.d.ts +50 -0
  25. package/tmp/module/GraphCanvas/index.d.ts +2 -0
  26. package/tmp/module/GraphCanvas/lib/force-settings-getter.d.ts +2 -0
  27. package/tmp/module/GraphCanvas/lib/graph-settings-getter.d.ts +2 -0
  28. package/tmp/module/GraphCanvas/lib/index.d.ts +6 -0
  29. package/tmp/module/GraphCanvas/lib/links/index.d.ts +3 -0
  30. package/tmp/module/GraphCanvas/lib/links/link-iteration-extractor.d.ts +5 -0
  31. package/tmp/module/GraphCanvas/lib/links/link-options-getter.d.ts +4 -0
  32. package/tmp/module/GraphCanvas/lib/links/link-settings-getter.d.ts +2 -0
  33. package/tmp/module/GraphCanvas/lib/listeners-getter.d.ts +2 -0
  34. package/tmp/module/GraphCanvas/lib/nodes/drag-place-coefficient-getter.d.ts +2 -0
  35. package/tmp/module/GraphCanvas/lib/nodes/index.d.ts +8 -0
  36. package/tmp/module/GraphCanvas/lib/nodes/is-overlaps-node.d.ts +2 -0
  37. package/tmp/module/GraphCanvas/lib/nodes/node-by-pointer-getter.d.ts +11 -0
  38. package/tmp/module/GraphCanvas/lib/nodes/node-id-getter.d.ts +2 -0
  39. package/tmp/module/GraphCanvas/lib/nodes/node-iteration-extractor.d.ts +5 -0
  40. package/tmp/module/GraphCanvas/lib/nodes/node-options-getter.d.ts +4 -0
  41. package/tmp/module/GraphCanvas/lib/nodes/node-radius-getter.d.ts +8 -0
  42. package/tmp/module/GraphCanvas/lib/nodes/node-settings-getter.d.ts +2 -0
  43. package/tmp/module/GraphCanvas/lib/pointer-getter.d.ts +2 -0
  44. package/tmp/module/GraphCanvas/types/graph.d.ts +15 -0
  45. package/tmp/module/GraphCanvas/types/index.d.ts +3 -0
  46. package/tmp/module/GraphCanvas/types/listeners.d.ts +21 -0
  47. package/tmp/module/GraphCanvas/types/settings.d.ts +62 -0
  48. package/tmp/module/GraphCanvasEngine/GraphCanvasEngine.d.ts +0 -0
  49. package/tmp/module/GraphCanvasEngine/GraphCanvasEngine.types.d.ts +0 -0
  50. package/tmp/module/GraphCanvasEngine/index.d.ts +0 -0
  51. package/tmp/types/index.d.ts +2 -0
  52. package/tmp/types/links.d.ts +5 -0
  53. package/tmp/types/nodes.d.ts +8 -0
@@ -0,0 +1,764 @@
1
+ import { least } from 'd3-array';
2
+ import { drag } from 'd3-drag';
3
+ import { forceLink, forceCenter, forceSimulation, forceX, forceY, forceManyBody, forceCollide } from 'd3-force';
4
+ import { create, select } from 'd3-selection';
5
+ import { zoomIdentity, zoom } from 'd3-zoom';
6
+ import debounce from 'lodash/debounce';
7
+
8
+ function checkType(value, condition) {
9
+ return condition;
10
+ }
11
+
12
+ function colorGetter() {
13
+ const chosenColors = {};
14
+ const colors = [
15
+ "#1f77b4",
16
+ "#ff7f0e",
17
+ "#2ca02c",
18
+ "#d62728",
19
+ "#9467bd",
20
+ "#8c564b",
21
+ "#e377c2",
22
+ "#7f7f7f",
23
+ "#bcbd22",
24
+ "#17becf",
25
+ ];
26
+ let cursor = 0;
27
+ return function color(key) {
28
+ if (chosenColors[key])
29
+ return chosenColors[key];
30
+ chosenColors[key] = colors[cursor];
31
+ cursor++;
32
+ if (cursor >= colors.length)
33
+ cursor = 0;
34
+ return chosenColors[key];
35
+ };
36
+ }
37
+
38
+ const FORCE_SETTINGS = {
39
+ centerPosition: {},
40
+ centerStrength: 1,
41
+ collideAdditionalRadius: 2,
42
+ collideStrength: 1,
43
+ collideIterations: 1,
44
+ collideOffMax: { links: 0, nodes: 0 },
45
+ collideOn: true,
46
+ chargeStrength: -40,
47
+ chargeDistanceMax: Infinity,
48
+ chargeDistanceMin: 1,
49
+ xForce: 0,
50
+ xStrength: 0.1,
51
+ yForce: 0,
52
+ yStrength: 0.1,
53
+ linkDistance: 10,
54
+ linkIterations: 1,
55
+ linkStrength: 1,
56
+ };
57
+ const GRAPH_SETTINGS = {
58
+ zoomExtent: [0.1, 10],
59
+ stickAfterDrag: false,
60
+ highlightByHover: false,
61
+ minFading: 0.2,
62
+ };
63
+ const NODE_SETTINGS = {
64
+ alpha: 1,
65
+ colorOuter: "#fff",
66
+ font: "8px Arial",
67
+ fontAlign: "center",
68
+ fontColor: "#333",
69
+ width: 1,
70
+ zoomTextBorder: 2,
71
+ initialRadius: 4,
72
+ radiusCoefficient: 5,
73
+ radiusFactor: 1,
74
+ flexibleRadius: true,
75
+ };
76
+ const LINK_SETTINGS = {
77
+ alpha: 1,
78
+ colorFar: "#999",
79
+ colorNear: "#000000FF",
80
+ widthFar: 1,
81
+ widthNear: 0.1,
82
+ zoomWidthBorder: 1,
83
+ zoomColorBorder: 1,
84
+ };
85
+
86
+ function linkIterationExtractor(link, i, links, transform, option, optionConstantGetter) {
87
+ let customOptions;
88
+ let constantOptions;
89
+ if (typeof option === "function")
90
+ customOptions = option(link, i, links, transform);
91
+ else
92
+ customOptions = option;
93
+ if (optionConstantGetter) {
94
+ if (typeof optionConstantGetter === "function")
95
+ constantOptions = optionConstantGetter(link, i, links, transform);
96
+ else
97
+ constantOptions = optionConstantGetter;
98
+ if (constantOptions &&
99
+ typeof constantOptions === "object" &&
100
+ !Array.isArray(constantOptions) &&
101
+ checkType(customOptions, customOptions === undefined ||
102
+ (typeof customOptions === "object" && !Array.isArray(constantOptions)))) {
103
+ return {
104
+ ...constantOptions,
105
+ ...(customOptions || {}),
106
+ };
107
+ }
108
+ }
109
+ return customOptions;
110
+ }
111
+
112
+ function linkOptionsGetter(_, __, ___, transform) {
113
+ return {
114
+ ...LINK_SETTINGS,
115
+ color: transform && transform.k > LINK_SETTINGS.zoomColorBorder
116
+ ? LINK_SETTINGS.colorNear
117
+ : LINK_SETTINGS.colorFar,
118
+ width: transform && transform.k > LINK_SETTINGS.zoomWidthBorder
119
+ ? LINK_SETTINGS.widthNear
120
+ : LINK_SETTINGS.widthFar,
121
+ };
122
+ }
123
+
124
+ function linkSettingsGetter(settings) {
125
+ return { options: settings?.options };
126
+ }
127
+
128
+ function forceSettingsGetter(settings) {
129
+ return {
130
+ centerPosition: settings?.centerPosition ?? FORCE_SETTINGS.centerPosition,
131
+ centerStrength: settings?.centerStrength ?? FORCE_SETTINGS.centerStrength,
132
+ collideRadius: settings?.collideRadius ?? null,
133
+ collideStrength: settings?.collideStrength ?? FORCE_SETTINGS.collideStrength,
134
+ collideIterations: settings?.collideIterations ?? FORCE_SETTINGS.collideIterations,
135
+ collideOffMax: settings?.collideOffMax ?? FORCE_SETTINGS.collideOffMax,
136
+ collideOn: settings?.collideOn ?? FORCE_SETTINGS.collideOn,
137
+ chargeStrength: settings?.chargeStrength ?? FORCE_SETTINGS.chargeStrength,
138
+ chargeDistanceMax: settings?.chargeDistanceMax ?? FORCE_SETTINGS.chargeDistanceMax,
139
+ chargeDistanceMin: settings?.chargeDistanceMin ?? FORCE_SETTINGS.chargeDistanceMin,
140
+ xForce: settings?.xForce ?? FORCE_SETTINGS.xForce,
141
+ xStrength: settings?.xStrength ?? FORCE_SETTINGS.xStrength,
142
+ yForce: settings?.yForce ?? FORCE_SETTINGS.yForce,
143
+ yStrength: settings?.yStrength ?? FORCE_SETTINGS.yStrength,
144
+ linkDistance: settings?.linkDistance ?? FORCE_SETTINGS.linkDistance,
145
+ linkIterations: settings?.linkIterations ?? FORCE_SETTINGS.linkIterations,
146
+ linkStrength: settings?.linkStrength ?? FORCE_SETTINGS.linkStrength,
147
+ };
148
+ }
149
+
150
+ function listenersGetter(settings) {
151
+ return settings || {};
152
+ }
153
+
154
+ function isOverlapsNode(node, radius, pointerX, pointerY) {
155
+ if (node.x == undefined || node.y == undefined)
156
+ return false;
157
+ const isOverX = node.x - radius <= pointerX && pointerX <= node.x + radius;
158
+ const isOverY = node.y - radius <= pointerY && pointerY <= node.y + radius;
159
+ return isOverX && isOverY;
160
+ }
161
+
162
+ function dragPlaceCoefficientGetter(node, pointerX, pointerY, radius) {
163
+ if (!node.x || !node.y)
164
+ return undefined;
165
+ if (isOverlapsNode(node, radius, pointerX, pointerY))
166
+ return (node.x - pointerX) ** 2 + (node.y - pointerY) ** 2;
167
+ return undefined;
168
+ }
169
+
170
+ function graphSettingsGetter(settings) {
171
+ return {
172
+ zoomExtent: settings?.zoomExtent || GRAPH_SETTINGS.zoomExtent,
173
+ dragPlaceCoefficient: settings?.dragPlaceCoefficient || dragPlaceCoefficientGetter,
174
+ stickAfterDrag: settings?.stickAfterDrag || GRAPH_SETTINGS.stickAfterDrag,
175
+ highlightByHover: settings?.highlightByHover || GRAPH_SETTINGS.highlightByHover,
176
+ minFading: settings?.minFading || GRAPH_SETTINGS.minFading,
177
+ };
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 nodeIterationExtractor(node, i, nodes, transform, option, optionConstantGetter) {
187
+ let customOptions;
188
+ let constantOptions;
189
+ if (typeof option === "function")
190
+ customOptions = option(node, i, nodes, transform);
191
+ else
192
+ customOptions = option;
193
+ if (optionConstantGetter) {
194
+ if (typeof optionConstantGetter === "function")
195
+ constantOptions = optionConstantGetter(node, i, nodes, transform);
196
+ else
197
+ constantOptions = optionConstantGetter;
198
+ if (constantOptions &&
199
+ typeof constantOptions === "object" &&
200
+ !Array.isArray(constantOptions) &&
201
+ checkType(customOptions, customOptions === undefined ||
202
+ (typeof customOptions === "object" && !Array.isArray(constantOptions)))) {
203
+ return {
204
+ ...constantOptions,
205
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
206
+ ...(customOptions || {}),
207
+ };
208
+ }
209
+ }
210
+ return customOptions;
211
+ }
212
+
213
+ const color = colorGetter();
214
+ function nodeOptionsGetter(node, _, __, transform) {
215
+ return {
216
+ ...NODE_SETTINGS,
217
+ colorInner: color(String(node.group || "_DEFAULT")),
218
+ text: transform && node.id != undefined && transform.k > NODE_SETTINGS.zoomTextBorder
219
+ ? String(node.id)
220
+ : null,
221
+ };
222
+ }
223
+
224
+ function nodeRadiusGetter({ flexibleRadius, initialRadius, linkCount, radiusCoefficient, radiusFactor, }) {
225
+ return ((flexibleRadius && linkCount ? linkCount / radiusCoefficient : 0) * radiusFactor + initialRadius);
226
+ }
227
+
228
+ function nodeByPointerGetter({ areaRect, areaTransform, mouseEvent, nodes, nodeCustomOptions, }) {
229
+ if (!areaRect)
230
+ return undefined;
231
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
232
+ return nodes.find((node, index) => {
233
+ const nodeOptions = nodeIterationExtractor(node, index, nodes, areaTransform, nodeCustomOptions || {}, nodeOptionsGetter);
234
+ const radius = nodeRadiusGetter({
235
+ flexibleRadius: nodeOptions.flexibleRadius,
236
+ initialRadius: nodeOptions.initialRadius,
237
+ radiusCoefficient: nodeOptions.radiusCoefficient,
238
+ radiusFactor: nodeOptions.radiusFactor,
239
+ linkCount: node.linkCount,
240
+ });
241
+ return isOverlapsNode(node, radius, pointerX, pointerY);
242
+ });
243
+ }
244
+
245
+ function nodeIdGetter(d) {
246
+ return d.id;
247
+ }
248
+
249
+ function nodeSettingsGetter(settings) {
250
+ return {
251
+ idGetter: settings?.idGetter ?? nodeIdGetter,
252
+ options: settings?.options,
253
+ };
254
+ }
255
+
256
+ class GraphCanvas {
257
+ /** initial data */
258
+ nodes;
259
+ links;
260
+ width;
261
+ height;
262
+ root;
263
+ container;
264
+ area;
265
+ dpi = devicePixelRatio;
266
+ /** settings */
267
+ graphSettings;
268
+ forceSettings;
269
+ nodeSettings;
270
+ linkSettings;
271
+ listeners;
272
+ /** service */
273
+ context;
274
+ simulation;
275
+ areaTransform = zoomIdentity;
276
+ areaRect;
277
+ draw;
278
+ simulationWorking = false;
279
+ isDragging = false;
280
+ eventAbortController;
281
+ highlightedNode = null;
282
+ highlightedNeighbors = null;
283
+ fading = 0;
284
+ constructor({ links, nodes, root, forceSettings, linkSettings, listeners, nodeSettings, graphSettings, }) {
285
+ root.style.position = "relative";
286
+ root.style.overflow = "hidden";
287
+ this.root = root;
288
+ this.forceSettings = forceSettingsGetter(forceSettings);
289
+ this.linkSettings = linkSettingsGetter(linkSettings);
290
+ this.nodeSettings = nodeSettingsGetter(nodeSettings);
291
+ this.listeners = listenersGetter(listeners);
292
+ this.graphSettings = graphSettingsGetter(graphSettings);
293
+ this.eventAbortController = new AbortController();
294
+ this.nodes = nodes;
295
+ this.links = links;
296
+ this.height = 0;
297
+ this.width = 0;
298
+ this.draw = this.initDraw();
299
+ this.init();
300
+ }
301
+ getData() {
302
+ return {
303
+ links: this.links,
304
+ nodes: this.nodes,
305
+ };
306
+ }
307
+ changeData(options) {
308
+ if (options.links != undefined)
309
+ this.links = options.links;
310
+ if (options.nodes != undefined)
311
+ this.nodes = options.nodes;
312
+ this.updateData();
313
+ }
314
+ changeSettings(options) {
315
+ if (options.forceSettings)
316
+ this.forceSettings = forceSettingsGetter(options.forceSettings);
317
+ if (options.linkSettings)
318
+ this.linkSettings = linkSettingsGetter(options.linkSettings);
319
+ if (options.nodeSettings)
320
+ this.nodeSettings = nodeSettingsGetter(options.nodeSettings);
321
+ if (options.graphSettings)
322
+ this.graphSettings = graphSettingsGetter(options.graphSettings);
323
+ this.updateSettings();
324
+ }
325
+ start() {
326
+ this.init();
327
+ }
328
+ destroy() {
329
+ if (this.simulation) {
330
+ this.simulation.stop();
331
+ this.simulation = undefined;
332
+ }
333
+ this.root.replaceChildren();
334
+ this.area = undefined;
335
+ this.context = undefined;
336
+ this.container = undefined;
337
+ this.eventAbortController.abort();
338
+ this.eventAbortController = new AbortController();
339
+ }
340
+ updateSettings() {
341
+ if (this.simulation) {
342
+ this.simulation.stop().alpha(1);
343
+ this.initSimulationForces();
344
+ this.simulation.restart();
345
+ }
346
+ }
347
+ updateData() {
348
+ if (this.simulation) {
349
+ this.initCollideForce();
350
+ this.simulation
351
+ .stop()
352
+ .alpha(1)
353
+ .nodes(this.nodes)
354
+ .force("link", forceLink(this.links)
355
+ .id(this.nodeSettings.idGetter)
356
+ .distance(this.forceSettings.linkDistance)
357
+ .strength(this.forceSettings.linkStrength)
358
+ .iterations(this.forceSettings.linkIterations))
359
+ .restart();
360
+ }
361
+ }
362
+ updateSize() {
363
+ if (!this.area || !this.simulation || !this.container)
364
+ return;
365
+ const { width, height } = this.container.getBoundingClientRect();
366
+ this.width = width;
367
+ this.height = height;
368
+ this.area.width = this.dpi * width;
369
+ this.area.height = this.dpi * height;
370
+ this.areaRect = this.area.getBoundingClientRect();
371
+ this.simulation
372
+ .stop()
373
+ .alpha(1)
374
+ .force("center", forceCenter(this.forceSettings.centerPosition.x || this.width / 2, this.forceSettings.centerPosition.y || this.height / 2).strength(this.forceSettings.centerStrength))
375
+ .restart();
376
+ if (!this.simulationWorking)
377
+ this.draw();
378
+ }
379
+ init() {
380
+ this.initArea();
381
+ this.initSimulation();
382
+ this.initDnd();
383
+ this.initZoom();
384
+ this.initResize();
385
+ this.initPointer();
386
+ }
387
+ initSimulation() {
388
+ if (!this.simulation) {
389
+ this.simulation = forceSimulation()
390
+ .nodes(this.nodes)
391
+ .force("link", forceLink(this.links).id(this.nodeSettings.idGetter))
392
+ .on("tick", () => {
393
+ this.simulationWorking = true;
394
+ this.draw();
395
+ })
396
+ .on("end", () => {
397
+ this.listeners.onSimulationEnd?.(this.simulation);
398
+ this.simulationWorking = false;
399
+ });
400
+ this.initSimulationForces();
401
+ }
402
+ }
403
+ initSimulationForces() {
404
+ if (!this.simulation)
405
+ return;
406
+ const linkForce = this.simulation.force("link");
407
+ linkForce
408
+ .distance(this.forceSettings.linkDistance)
409
+ .strength(this.forceSettings.linkStrength)
410
+ .iterations(this.forceSettings.linkIterations);
411
+ this.simulation
412
+ .force("x", forceX(this.forceSettings.xForce).strength(this.forceSettings.xStrength))
413
+ .force("y", forceY(this.forceSettings.yForce).strength(this.forceSettings.yStrength))
414
+ .force("charge", forceManyBody()
415
+ .strength(this.forceSettings.chargeStrength)
416
+ .distanceMax(this.forceSettings.chargeDistanceMax)
417
+ .distanceMin(this.forceSettings.chargeDistanceMin))
418
+ .force("center", forceCenter(this.forceSettings.centerPosition.x || this.width / 2, this.forceSettings.centerPosition.y || this.height / 2).strength(this.forceSettings.centerStrength));
419
+ this.initCollideForce();
420
+ }
421
+ initCollideForce() {
422
+ if (!this.simulation)
423
+ return;
424
+ if (!this.forceSettings.collideOn) {
425
+ if (this.simulation.force("collide"))
426
+ this.simulation.force("collide", null);
427
+ return;
428
+ }
429
+ const isHasMax = this.forceSettings.collideOffMax.links != 0 && this.forceSettings.collideOffMax.nodes != 0;
430
+ const isMaxCollideNodes = isHasMax && this.forceSettings.collideOffMax.nodes < this.nodes.length;
431
+ const isMaxCollideLinks = isHasMax && this.forceSettings.collideOffMax.links < this.links.length;
432
+ if (isMaxCollideNodes && isMaxCollideLinks)
433
+ this.simulation.force("collide", null);
434
+ else if (!this.simulation.force("collide"))
435
+ this.simulation.force("collide", forceCollide()
436
+ .radius((node, index) => {
437
+ if (this.forceSettings.collideRadius) {
438
+ return nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.forceSettings.collideIterations, undefined);
439
+ }
440
+ const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
441
+ const radius = nodeRadiusGetter({
442
+ flexibleRadius: nodeOptions.flexibleRadius,
443
+ initialRadius: nodeOptions.initialRadius,
444
+ radiusCoefficient: nodeOptions.radiusCoefficient,
445
+ radiusFactor: nodeOptions.radiusFactor,
446
+ linkCount: node.linkCount,
447
+ });
448
+ return radius + FORCE_SETTINGS.collideAdditionalRadius;
449
+ })
450
+ .strength(this.forceSettings.collideStrength)
451
+ .iterations(this.forceSettings.collideIterations));
452
+ }
453
+ initArea() {
454
+ if (!this.area || !this.context || !this.container) {
455
+ this.container = create("div")
456
+ .attr("style", "padding: 0 !important; width: 100%; height: 100%;")
457
+ .node();
458
+ if (!this.container)
459
+ throw new Error("couldn't create container");
460
+ this.root.appendChild(this.container);
461
+ const { width, height } = this.root.getBoundingClientRect();
462
+ this.width = width;
463
+ this.height = height;
464
+ this.area = create("canvas")
465
+ .attr("width", this.dpi * this.width)
466
+ .attr("height", this.dpi * this.height)
467
+ .attr("style", `width: 100%; height: 100%; border: none !important;`)
468
+ .node();
469
+ if (!this.area)
470
+ throw new Error("couldn't create canvas");
471
+ this.container.appendChild(this.area);
472
+ this.areaRect = this.area.getBoundingClientRect();
473
+ this.context = this.area.getContext("2d");
474
+ if (!this.context)
475
+ throw new Error("couldn't create canvas context");
476
+ this.context.scale(this.dpi, this.dpi);
477
+ }
478
+ }
479
+ initDraw() {
480
+ function draw() {
481
+ if (!this.context)
482
+ return;
483
+ if (this.listeners.onDraw) {
484
+ return void this.listeners.onDraw(this.context, this.areaTransform);
485
+ }
486
+ if (this.highlightedNeighbors && this.highlightedNode) {
487
+ if (this.fading === 0)
488
+ this.fading = 1;
489
+ }
490
+ this.context.save();
491
+ this.context.clearRect(0, 0, this.width, this.height);
492
+ this.context.translate(this.areaTransform.x, this.areaTransform.y);
493
+ this.context.scale(this.areaTransform.k, this.areaTransform.k);
494
+ /** links */
495
+ this.context.beginPath();
496
+ this.links.forEach(drawLink.bind(this));
497
+ this.context.stroke();
498
+ /** nodes */
499
+ this.nodes.forEach(drawNode.bind(this));
500
+ this.context.restore();
501
+ this.listeners.onDrawFinished?.(this.context, this.areaTransform);
502
+ if (this.fading > this.graphSettings.minFading) {
503
+ this.fading -= 0.1;
504
+ requestAnimationFrame(() => {
505
+ this.draw();
506
+ });
507
+ }
508
+ }
509
+ function drawLink(link, index) {
510
+ if (!this.context ||
511
+ typeof link.source !== "object" ||
512
+ typeof link.target !== "object" ||
513
+ !link.source.x ||
514
+ !link.source.y ||
515
+ !link.target.x ||
516
+ !link.target.y)
517
+ return;
518
+ const linkOptions = linkIterationExtractor(link, index, this.links, this.areaTransform, this.linkSettings.options || {}, linkOptionsGetter);
519
+ let alpha = linkOptions.alpha;
520
+ if (this.highlightedNeighbors && this.highlightedNode) {
521
+ if (this.highlightedNode.id != link.source.id &&
522
+ this.highlightedNode.id != link.target.id) {
523
+ alpha = this.fading;
524
+ }
525
+ this.context.beginPath();
526
+ }
527
+ this.context.globalAlpha = alpha;
528
+ this.context.strokeStyle = linkOptions.color;
529
+ this.context.lineWidth = linkOptions.width;
530
+ this.context.moveTo(link.source.x, link.source.y);
531
+ this.context.lineTo(link.target.x, link.target.y);
532
+ if (this.highlightedNeighbors && this.highlightedNode)
533
+ this.context.stroke();
534
+ }
535
+ function drawNode(node, index) {
536
+ if (!this.context || !node.x || !node.y)
537
+ return;
538
+ const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
539
+ const radius = nodeRadiusGetter({
540
+ flexibleRadius: nodeOptions.flexibleRadius,
541
+ initialRadius: nodeOptions.initialRadius,
542
+ radiusCoefficient: nodeOptions.radiusCoefficient,
543
+ radiusFactor: nodeOptions.radiusFactor,
544
+ linkCount: node.linkCount,
545
+ });
546
+ let alpha = nodeOptions.alpha;
547
+ if (this.highlightedNeighbors && this.highlightedNode) {
548
+ if (!this.highlightedNeighbors.has(node.id) && this.highlightedNode.id != node.id) {
549
+ alpha = this.fading;
550
+ }
551
+ }
552
+ this.context.beginPath();
553
+ this.context.globalAlpha = alpha;
554
+ /** text */
555
+ if (nodeOptions.text) {
556
+ this.context.font = nodeOptions.font;
557
+ this.context.fillStyle = nodeOptions.fontColor;
558
+ this.context.textAlign = nodeOptions.fontAlign;
559
+ this.context.fillText(nodeOptions.text, node.x, node.y + radius + 10);
560
+ }
561
+ /** circle */
562
+ this.context.strokeStyle = nodeOptions.colorOuter;
563
+ this.context.lineWidth = nodeOptions.width;
564
+ this.context.moveTo(node.x + radius, node.y);
565
+ this.context.arc(node.x, node.y, radius, 0, 2 * Math.PI);
566
+ this.context.fillStyle = nodeOptions.colorInner;
567
+ this.context.fill();
568
+ this.context.stroke();
569
+ }
570
+ return draw;
571
+ }
572
+ initResize() {
573
+ if (!this.area)
574
+ throw new Error("bad init data");
575
+ const resize = debounce(() => {
576
+ this.updateSize();
577
+ }, 10);
578
+ window.addEventListener("resize", () => {
579
+ requestAnimationFrame(() => {
580
+ resize();
581
+ });
582
+ }, { signal: this.eventAbortController.signal });
583
+ }
584
+ initPointer() {
585
+ if (!this.area || !this.nodes || !this.simulation)
586
+ throw new Error("bad init data");
587
+ this.area.addEventListener("pointermove", (event) => {
588
+ let currentNode;
589
+ if (this.graphSettings.highlightByHover) {
590
+ currentNode = nodeByPointerGetter({
591
+ nodeCustomOptions: this.nodeSettings.options,
592
+ areaRect: this.areaRect,
593
+ areaTransform: this.areaTransform,
594
+ mouseEvent: event,
595
+ nodes: this.nodes,
596
+ });
597
+ if (currentNode && currentNode.neighbors && this.highlightedNode !== currentNode) {
598
+ this.highlightedNode = currentNode;
599
+ this.highlightedNeighbors = new Set(this.highlightedNode.neighbors);
600
+ requestAnimationFrame(() => {
601
+ this.draw();
602
+ });
603
+ }
604
+ else if (!currentNode && this.highlightedNode) {
605
+ this.highlightedNode = null;
606
+ this.highlightedNeighbors = null;
607
+ requestAnimationFrame(() => {
608
+ this.draw();
609
+ });
610
+ }
611
+ }
612
+ if (!this.listeners.onMove)
613
+ return;
614
+ if (!currentNode)
615
+ currentNode = nodeByPointerGetter({
616
+ nodeCustomOptions: this.nodeSettings.options,
617
+ areaRect: this.areaRect,
618
+ areaTransform: this.areaTransform,
619
+ mouseEvent: event,
620
+ nodes: this.nodes,
621
+ });
622
+ return void this.listeners.onMove(event, currentNode);
623
+ }, {
624
+ signal: this.eventAbortController.signal,
625
+ });
626
+ this.area.addEventListener("dblclick", (event) => {
627
+ if (!this.listeners.onDoubleClick)
628
+ return;
629
+ const currentNode = nodeByPointerGetter({
630
+ nodeCustomOptions: this.nodeSettings.options,
631
+ areaRect: this.areaRect,
632
+ areaTransform: this.areaTransform,
633
+ mouseEvent: event,
634
+ nodes: this.nodes,
635
+ });
636
+ return void this.listeners.onDoubleClick(event, currentNode);
637
+ });
638
+ this.area.addEventListener("pointerup", (event) => {
639
+ if (this.isDragging)
640
+ return;
641
+ if (event.button === 0) {
642
+ if (!this.listeners.onClick)
643
+ return;
644
+ const currentNode = nodeByPointerGetter({
645
+ nodeCustomOptions: this.nodeSettings.options,
646
+ areaRect: this.areaRect,
647
+ areaTransform: this.areaTransform,
648
+ mouseEvent: event,
649
+ nodes: this.nodes,
650
+ });
651
+ return void this.listeners.onClick(event, currentNode);
652
+ }
653
+ if (event.button === 1) {
654
+ if (!this.listeners.onWheelClick)
655
+ return;
656
+ const currentNode = nodeByPointerGetter({
657
+ nodeCustomOptions: this.nodeSettings.options,
658
+ areaRect: this.areaRect,
659
+ areaTransform: this.areaTransform,
660
+ mouseEvent: event,
661
+ nodes: this.nodes,
662
+ });
663
+ return void this.listeners.onWheelClick(event, currentNode);
664
+ }
665
+ }, {
666
+ signal: this.eventAbortController.signal,
667
+ });
668
+ this.area.addEventListener("contextmenu", (event) => {
669
+ if (!this.listeners.onContextMenu)
670
+ return;
671
+ const currentNode = nodeByPointerGetter({
672
+ nodeCustomOptions: this.nodeSettings.options,
673
+ areaRect: this.areaRect,
674
+ areaTransform: this.areaTransform,
675
+ mouseEvent: event,
676
+ nodes: this.nodes,
677
+ });
678
+ return void this.listeners.onContextMenu(event, currentNode);
679
+ }, {
680
+ signal: this.eventAbortController.signal,
681
+ });
682
+ }
683
+ initDnd() {
684
+ if (!this.area || !this.nodes || !this.simulation)
685
+ throw new Error("bad init data");
686
+ select(this.area).call(drag()
687
+ .subject((event) => {
688
+ if (this.listeners.onDragSubject) {
689
+ return this.listeners.onDragSubject(event, this.areaTransform, this.nodes);
690
+ }
691
+ if (!this.areaRect)
692
+ return;
693
+ const mouseEvent = event.sourceEvent;
694
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
695
+ let index = 0;
696
+ return least(this.nodes, (node) => {
697
+ if (!node.x || !node.y)
698
+ return undefined;
699
+ const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
700
+ const radius = nodeRadiusGetter({
701
+ flexibleRadius: nodeOptions.flexibleRadius,
702
+ initialRadius: nodeOptions.initialRadius,
703
+ radiusCoefficient: nodeOptions.radiusCoefficient,
704
+ radiusFactor: nodeOptions.radiusFactor,
705
+ linkCount: node.linkCount,
706
+ });
707
+ index++;
708
+ return this.graphSettings.dragPlaceCoefficient(node, pointerX, pointerY, radius);
709
+ });
710
+ })
711
+ .on("start", (event) => {
712
+ this.listeners.onStartDragFinished?.(event, this.simulation, this.areaTransform);
713
+ })
714
+ .on("drag", (event) => {
715
+ if (!this.isDragging) {
716
+ this.isDragging = true;
717
+ if (this.simulation)
718
+ this.simulation.alphaTarget(0.3).restart();
719
+ }
720
+ if (!this.areaRect)
721
+ return;
722
+ const mouseEvent = event.sourceEvent;
723
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
724
+ event.subject.fx = pointerX;
725
+ event.subject.fy = pointerY;
726
+ this.listeners.onMoveDragFinished?.(event, this.simulation, this.areaTransform);
727
+ })
728
+ .on("end", (event) => {
729
+ this.isDragging = false;
730
+ if (!event.active && this.simulation)
731
+ this.simulation.alphaTarget(0);
732
+ if (this.graphSettings.stickAfterDrag && this.areaRect) {
733
+ if (!this.areaRect)
734
+ return;
735
+ const mouseEvent = event.sourceEvent;
736
+ const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
737
+ event.subject.fx = pointerX;
738
+ event.subject.fy = pointerY;
739
+ }
740
+ else {
741
+ event.subject.fx = null;
742
+ event.subject.fy = null;
743
+ }
744
+ this.listeners.onEndDragFinished?.(event, this.simulation, this.areaTransform);
745
+ }));
746
+ }
747
+ initZoom() {
748
+ if (!this.area)
749
+ throw new Error("bad init data");
750
+ select(this.area)
751
+ .call(zoom()
752
+ .scaleExtent(this.graphSettings.zoomExtent)
753
+ .on("zoom", (event) => {
754
+ this.listeners.onZoom?.(event);
755
+ this.areaTransform = event.transform;
756
+ if (!this.simulationWorking)
757
+ requestAnimationFrame(() => this.draw());
758
+ }))
759
+ .on("dblclick.zoom", null);
760
+ }
761
+ }
762
+
763
+ export { GraphCanvas };
764
+ //# sourceMappingURL=index.js.map