@krainovsd/graph 0.1.2 → 0.3.0

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.
package/lib/esm/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { least } from 'd3-array';
1
+ import { greatest } from 'd3-array';
2
2
  import { drag } from 'd3-drag';
3
- import { forceLink, forceCenter, forceSimulation, forceX, forceY, forceManyBody, forceCollide } from 'd3-force';
3
+ import { forceLink, forceSimulation, forceX, forceY, forceManyBody, forceCenter, forceCollide } from 'd3-force';
4
4
  import { create, select } from 'd3-selection';
5
- import { zoomIdentity, zoom } from 'd3-zoom';
5
+ import { zoomIdentity, zoom, ZoomTransform } from 'd3-zoom';
6
6
  import debounce from 'lodash/debounce';
7
7
 
8
8
  function checkType(value, condition) {
@@ -38,9 +38,9 @@ function colorGetter() {
38
38
  const FORCE_SETTINGS = {
39
39
  centerPosition: {},
40
40
  centerStrength: 1,
41
- collideAdditionalRadius: 2,
42
- collideStrength: 1,
43
- collideIterations: 1,
41
+ collideStrength: 0.1,
42
+ collideAdditionalRadius: 4,
43
+ collideIterations: 2,
44
44
  collideOffMax: { links: 0, nodes: 0 },
45
45
  collideOn: true,
46
46
  chargeStrength: -40,
@@ -50,100 +50,84 @@ const FORCE_SETTINGS = {
50
50
  xStrength: 0.1,
51
51
  yForce: 0,
52
52
  yStrength: 0.1,
53
- linkDistance: 10,
53
+ linkDistance: 30,
54
54
  linkIterations: 1,
55
55
  linkStrength: 1,
56
+ collideRadius: null,
56
57
  };
57
58
  const GRAPH_SETTINGS = {
58
- zoomExtent: [0.1, 10],
59
+ zoomExtent: [0.1, 20],
59
60
  stickAfterDrag: false,
60
61
  highlightByHover: false,
61
- minFading: 0.2,
62
+ highlightFadingMin: 0.21,
63
+ highlightDownStep: 0.09,
64
+ highlightUpStep: 0.09,
65
+ dragPlaceCoefficient: dragPlaceCoefficientGetter,
66
+ nodeRadiusInitial: 4,
67
+ nodeRadiusCoefficient: 5,
68
+ nodeRadiusFactor: 1,
69
+ nodeRadiusFlexible: true,
62
70
  };
63
71
  const NODE_SETTINGS = {
64
72
  alpha: 1,
65
- colorOuter: "#fff",
66
- font: "8px Arial",
67
- fontAlign: "center",
68
- fontColor: "#333",
73
+ borderColor: "#000000FF",
74
+ borderWidth: 0.1,
75
+ textWidth: 20,
76
+ textShiftX: 0,
77
+ textFont: "Arial",
78
+ textAlign: "center",
79
+ textColor: "#333",
69
80
  width: 1,
70
- zoomTextBorder: 2,
71
- initialRadius: 4,
72
- radiusCoefficient: 5,
73
- radiusFactor: 1,
74
- flexibleRadius: true,
81
+ radius: 4,
82
+ textStyle: "normal",
83
+ textWeight: "500",
84
+ textGap: 1,
75
85
  };
76
86
  const LINK_SETTINGS = {
77
87
  alpha: 1,
78
- colorFar: "#999",
79
- colorNear: "#000000FF",
80
- widthFar: 1,
81
- widthNear: 0.1,
82
- zoomWidthBorder: 1,
83
- zoomColorBorder: 1,
88
+ };
89
+ const COMMON_SETTINGS = {
90
+ linkColorZoomFar: "#999",
91
+ linkColorZoomNear: "#000000FF",
92
+ linkWidthZoomFar: 1,
93
+ linkWidthZoomNear: 0.1,
94
+ linkWidthZoomBorder: 1,
95
+ linkColorZoomBorder: 1,
96
+ nodeTextScaleMin: 1.5,
97
+ nodeTextScaleMax: 20,
98
+ nodeTextSizeMin: 1.5,
99
+ nodeTextSizeMax: 3.5,
100
+ nodeTextShiftYMin: 2.5,
101
+ nodeTextShiftYMax: 4,
102
+ nodeTextChangeStepCount: 200,
84
103
  };
85
104
 
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;
105
+ function forceSettingsGetter(settings, prevSettings) {
106
+ return {
107
+ ...(prevSettings ?? FORCE_SETTINGS),
108
+ ...settings,
109
+ };
110
110
  }
111
111
 
112
- function linkOptionsGetter(_, __, ___, transform) {
112
+ function graphSettingsGetter(settings, prevSettings) {
113
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,
114
+ ...(prevSettings ?? GRAPH_SETTINGS),
115
+ ...settings,
121
116
  };
122
117
  }
123
118
 
124
119
  function linkSettingsGetter(settings) {
125
120
  return { options: settings?.options };
126
121
  }
127
-
128
- function forceSettingsGetter(settings) {
122
+ function linkOptionsGetter(_, __, ___, transform) {
129
123
  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,
124
+ ...LINK_SETTINGS,
125
+ color: transform && transform.k > COMMON_SETTINGS.linkColorZoomBorder
126
+ ? COMMON_SETTINGS.linkColorZoomNear
127
+ : COMMON_SETTINGS.linkColorZoomFar,
128
+ width: transform && transform.k > COMMON_SETTINGS.linkWidthZoomBorder
129
+ ? COMMON_SETTINGS.linkWidthZoomNear
130
+ : COMMON_SETTINGS.linkWidthZoomFar,
147
131
  };
148
132
  }
149
133
 
@@ -151,6 +135,59 @@ function listenersGetter(settings) {
151
135
  return settings || {};
152
136
  }
153
137
 
138
+ function nodeSettingsGetter(settings) {
139
+ return {
140
+ idGetter: settings?.idGetter ?? nodeIdGetter,
141
+ options: settings?.options,
142
+ };
143
+ }
144
+ const color = colorGetter();
145
+ function nodeOptionsGetter(node, _, __, transform) {
146
+ const { textShiftY, textSize } = nodeTextSizeGetter(transform);
147
+ return {
148
+ ...NODE_SETTINGS,
149
+ color: color(String(node.group || "_DEFAULT")),
150
+ textVisible: Boolean(transform && transform.k > COMMON_SETTINGS.nodeTextScaleMin),
151
+ text: node.id != undefined ? String(node.id) : null,
152
+ textShiftY,
153
+ textSize,
154
+ };
155
+ }
156
+ function nodeTextSizeGetter(transform) {
157
+ let textSize = COMMON_SETTINGS.nodeTextSizeMax;
158
+ let textShiftY = COMMON_SETTINGS.nodeTextShiftYMax;
159
+ if (transform) {
160
+ const scaleStepCoefficient = (COMMON_SETTINGS.nodeTextScaleMax - COMMON_SETTINGS.nodeTextScaleMin) /
161
+ COMMON_SETTINGS.nodeTextChangeStepCount;
162
+ const textStepCoefficient = (COMMON_SETTINGS.nodeTextSizeMax - COMMON_SETTINGS.nodeTextSizeMin) /
163
+ COMMON_SETTINGS.nodeTextChangeStepCount;
164
+ const shiftStepCoefficient = (COMMON_SETTINGS.nodeTextShiftYMax - COMMON_SETTINGS.nodeTextShiftYMin) /
165
+ COMMON_SETTINGS.nodeTextChangeStepCount;
166
+ if (transform.k >= COMMON_SETTINGS.nodeTextScaleMax) {
167
+ textSize = COMMON_SETTINGS.nodeTextSizeMin;
168
+ textShiftY = COMMON_SETTINGS.nodeTextShiftYMin;
169
+ }
170
+ else if (transform.k > COMMON_SETTINGS.nodeTextScaleMin) {
171
+ const transformSteps = (transform.k - COMMON_SETTINGS.nodeTextScaleMin) / scaleStepCoefficient;
172
+ textSize -= transformSteps * textStepCoefficient;
173
+ textShiftY -= transformSteps * shiftStepCoefficient;
174
+ }
175
+ }
176
+ return { textSize, textShiftY };
177
+ }
178
+ function nodeRadiusGetter({ radiusFlexible, radiusInitial, linkCount, radiusCoefficient, radiusFactor, }) {
179
+ return ((radiusFlexible && linkCount ? linkCount / radiusCoefficient : 0) * radiusFactor + radiusInitial);
180
+ }
181
+ function nodeIdGetter(d) {
182
+ return d.id;
183
+ }
184
+
185
+ function pointerGetter(mouseEvent, areaRect, areaTransform) {
186
+ const px = (mouseEvent.clientX - areaRect.left - areaTransform.x) / areaTransform.k;
187
+ const py = (mouseEvent.clientY - areaRect.top - areaTransform.y) / areaTransform.k;
188
+ return [px, py];
189
+ }
190
+
154
191
  function isOverlapsNode(node, radius, pointerX, pointerY) {
155
192
  if (node.x == undefined || node.y == undefined)
156
193
  return false;
@@ -163,36 +200,19 @@ function dragPlaceCoefficientGetter(node, pointerX, pointerY, radius) {
163
200
  if (!node.x || !node.y)
164
201
  return undefined;
165
202
  if (isOverlapsNode(node, radius, pointerX, pointerY))
166
- return (node.x - pointerX) ** 2 + (node.y - pointerY) ** 2;
167
- return undefined;
203
+ return node.index;
168
204
  }
169
205
 
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) {
206
+ function linkIterationExtractor(link, i, links, transform, option, optionConstantGetter) {
187
207
  let customOptions;
188
208
  let constantOptions;
189
209
  if (typeof option === "function")
190
- customOptions = option(node, i, nodes, transform);
210
+ customOptions = option(link, i, links, transform);
191
211
  else
192
212
  customOptions = option;
193
213
  if (optionConstantGetter) {
194
214
  if (typeof optionConstantGetter === "function")
195
- constantOptions = optionConstantGetter(node, i, nodes, transform);
215
+ constantOptions = optionConstantGetter(link, i, links, transform);
196
216
  else
197
217
  constantOptions = optionConstantGetter;
198
218
  if (constantOptions &&
@@ -202,7 +222,6 @@ function nodeIterationExtractor(node, i, nodes, transform, option, optionConstan
202
222
  (typeof customOptions === "object" && !Array.isArray(constantOptions)))) {
203
223
  return {
204
224
  ...constantOptions,
205
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
206
225
  ...(customOptions || {}),
207
226
  };
208
227
  }
@@ -210,47 +229,93 @@ function nodeIterationExtractor(node, i, nodes, transform, option, optionConstan
210
229
  return customOptions;
211
230
  }
212
231
 
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, }) {
232
+ function nodeByPointerGetter({ areaRect, areaTransform, mouseEvent, nodes, graphSettings, }) {
229
233
  if (!areaRect)
230
234
  return undefined;
231
235
  const [pointerX, pointerY] = pointerGetter(mouseEvent, areaRect, areaTransform);
232
- return nodes.find((node, index) => {
233
- const nodeOptions = nodeIterationExtractor(node, index, nodes, areaTransform, nodeCustomOptions || {}, nodeOptionsGetter);
236
+ return greatest(nodes, (node) => {
234
237
  const radius = nodeRadiusGetter({
235
- flexibleRadius: nodeOptions.flexibleRadius,
236
- initialRadius: nodeOptions.initialRadius,
237
- radiusCoefficient: nodeOptions.radiusCoefficient,
238
- radiusFactor: nodeOptions.radiusFactor,
238
+ radiusFlexible: graphSettings.nodeRadiusFlexible,
239
+ radiusInitial: graphSettings.nodeRadiusInitial,
240
+ radiusCoefficient: graphSettings.nodeRadiusCoefficient,
241
+ radiusFactor: graphSettings.nodeRadiusFactor,
239
242
  linkCount: node.linkCount,
240
243
  });
241
- return isOverlapsNode(node, radius, pointerX, pointerY);
244
+ if (isOverlapsNode(node, radius, pointerX, pointerY))
245
+ return node.index;
242
246
  });
243
247
  }
244
248
 
245
- function nodeIdGetter(d) {
246
- return d.id;
249
+ function nodeIterationExtractor(node, i, nodes, transform, option, optionConstantGetter) {
250
+ let customOptions;
251
+ let constantOptions;
252
+ if (typeof option === "function")
253
+ customOptions = option(node, i, nodes, transform);
254
+ else
255
+ customOptions = option;
256
+ if (optionConstantGetter) {
257
+ if (typeof optionConstantGetter === "function")
258
+ constantOptions = optionConstantGetter(node, i, nodes, transform);
259
+ else
260
+ constantOptions = optionConstantGetter;
261
+ if (constantOptions &&
262
+ typeof constantOptions === "object" &&
263
+ !Array.isArray(constantOptions) &&
264
+ checkType(customOptions, customOptions === undefined ||
265
+ (typeof customOptions === "object" && !Array.isArray(constantOptions)))) {
266
+ return {
267
+ ...constantOptions,
268
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
269
+ ...(customOptions || {}),
270
+ };
271
+ }
272
+ }
273
+ return customOptions;
247
274
  }
248
275
 
249
- function nodeSettingsGetter(settings) {
250
- return {
251
- idGetter: settings?.idGetter ?? nodeIdGetter,
252
- options: settings?.options,
253
- };
276
+ const SPACE = " ";
277
+ function drawText({ context, id, textAlign, textColor, textFont, textStyle, textGap, textWeight, textSize, text, x, y, cachedNodeText, maxWidth, }) {
278
+ context.font = `${textStyle} normal ${textWeight} ${textSize}px ${textFont}`;
279
+ context.fillStyle = textColor;
280
+ context.textAlign = textAlign;
281
+ if (cachedNodeText[id] != undefined) {
282
+ cachedNodeText[id].forEach((line, index) => {
283
+ context.fillText(line, x, y + index * textSize + index * textGap);
284
+ });
285
+ return;
286
+ }
287
+ if (maxWidth == undefined || context.measureText(text).width <= maxWidth) {
288
+ cachedNodeText[id] = [text];
289
+ context.fillText(text, x, y);
290
+ return;
291
+ }
292
+ const spaceWidth = context.measureText(" ").width;
293
+ const lines = [];
294
+ let lineWidth = 0;
295
+ let line = "";
296
+ for (const word of text.split(" ")) {
297
+ const size = context.measureText(word).width;
298
+ lineWidth += size;
299
+ if (line === "") {
300
+ line = word;
301
+ continue;
302
+ }
303
+ if (lineWidth > maxWidth) {
304
+ lineWidth = 0;
305
+ lines.push(line);
306
+ line = word;
307
+ }
308
+ else {
309
+ lineWidth += spaceWidth;
310
+ line += `${SPACE}${word}`;
311
+ }
312
+ }
313
+ if (line !== "")
314
+ lines.push(line);
315
+ cachedNodeText[id] = lines;
316
+ lines.forEach((line, index) => {
317
+ context.fillText(line, x, y + index * textSize + index * textGap);
318
+ });
254
319
  }
255
320
 
256
321
  class GraphCanvas {
@@ -275,12 +340,15 @@ class GraphCanvas {
275
340
  areaTransform = zoomIdentity;
276
341
  areaRect;
277
342
  draw;
343
+ eventAbortController;
344
+ cachedNodeText = {};
278
345
  simulationWorking = false;
279
346
  isDragging = false;
280
- eventAbortController;
281
347
  highlightedNode = null;
282
348
  highlightedNeighbors = null;
283
- fading = 0;
349
+ highlighFading = 1;
350
+ highlightFadingWorking = false;
351
+ highlightDrawing = false;
284
352
  constructor({ links, nodes, root, forceSettings, linkSettings, listeners, nodeSettings, graphSettings, }) {
285
353
  root.style.position = "relative";
286
354
  root.style.overflow = "hidden";
@@ -309,20 +377,37 @@ class GraphCanvas {
309
377
  this.links = options.links;
310
378
  if (options.nodes != undefined)
311
379
  this.nodes = options.nodes;
312
- this.updateData();
380
+ if (options.nodes != undefined || options.links != undefined)
381
+ this.updateData();
313
382
  }
314
383
  changeSettings(options) {
384
+ if (options.graphSettings)
385
+ this.graphSettings = graphSettingsGetter(options.graphSettings, this.graphSettings);
315
386
  if (options.forceSettings)
316
- this.forceSettings = forceSettingsGetter(options.forceSettings);
387
+ this.forceSettings = forceSettingsGetter(options.forceSettings, this.forceSettings);
317
388
  if (options.linkSettings)
318
389
  this.linkSettings = linkSettingsGetter(options.linkSettings);
319
390
  if (options.nodeSettings)
320
391
  this.nodeSettings = nodeSettingsGetter(options.nodeSettings);
321
- if (options.graphSettings)
322
- this.graphSettings = graphSettingsGetter(options.graphSettings);
323
392
  this.updateSettings();
324
393
  }
394
+ tick() {
395
+ if (!this.simulationWorking)
396
+ this.draw();
397
+ }
325
398
  start() {
399
+ if (this.simulation)
400
+ this.simulation.alpha(1).restart();
401
+ if (this.container)
402
+ this.container.style.display = "block";
403
+ }
404
+ stop() {
405
+ if (this.simulation)
406
+ this.simulation.stop();
407
+ if (this.container)
408
+ this.container.style.display = "none";
409
+ }
410
+ create() {
326
411
  this.init();
327
412
  }
328
413
  destroy() {
@@ -336,26 +421,40 @@ class GraphCanvas {
336
421
  this.container = undefined;
337
422
  this.eventAbortController.abort();
338
423
  this.eventAbortController = new AbortController();
424
+ this.clearState();
425
+ this.clearDataDependencies();
339
426
  }
340
427
  updateSettings() {
341
428
  if (this.simulation) {
342
- this.simulation.stop().alpha(1);
343
429
  this.initSimulationForces();
430
+ this.simulation.alpha(1);
344
431
  this.simulation.restart();
345
432
  }
346
433
  }
434
+ clearState() {
435
+ this.isDragging = false;
436
+ this.simulationWorking = false;
437
+ this.highlightedNode = null;
438
+ this.highlightedNeighbors = null;
439
+ this.highlighFading = 1;
440
+ this.highlightFadingWorking = false;
441
+ this.highlightDrawing = false;
442
+ }
443
+ clearDataDependencies() {
444
+ this.cachedNodeText = {};
445
+ }
347
446
  updateData() {
447
+ this.clearDataDependencies();
348
448
  if (this.simulation) {
349
449
  this.initCollideForce();
350
450
  this.simulation
351
- .stop()
352
- .alpha(1)
353
451
  .nodes(this.nodes)
354
452
  .force("link", forceLink(this.links)
355
453
  .id(this.nodeSettings.idGetter)
356
454
  .distance(this.forceSettings.linkDistance)
357
455
  .strength(this.forceSettings.linkStrength)
358
456
  .iterations(this.forceSettings.linkIterations))
457
+ .alpha(0.5)
359
458
  .restart();
360
459
  }
361
460
  }
@@ -368,11 +467,7 @@ class GraphCanvas {
368
467
  this.area.width = this.dpi * width;
369
468
  this.area.height = this.dpi * height;
370
469
  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();
470
+ this.simulation.alpha(0.1).restart();
376
471
  if (!this.simulationWorking)
377
472
  this.draw();
378
473
  }
@@ -415,7 +510,7 @@ class GraphCanvas {
415
510
  .strength(this.forceSettings.chargeStrength)
416
511
  .distanceMax(this.forceSettings.chargeDistanceMax)
417
512
  .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));
513
+ .force("center", forceCenter(this.forceSettings.centerPosition.x || 0, this.forceSettings.centerPosition.y || 0).strength(this.forceSettings.centerStrength));
419
514
  this.initCollideForce();
420
515
  }
421
516
  initCollideForce() {
@@ -439,13 +534,13 @@ class GraphCanvas {
439
534
  }
440
535
  const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
441
536
  const radius = nodeRadiusGetter({
442
- flexibleRadius: nodeOptions.flexibleRadius,
443
- initialRadius: nodeOptions.initialRadius,
444
- radiusCoefficient: nodeOptions.radiusCoefficient,
445
- radiusFactor: nodeOptions.radiusFactor,
537
+ radiusFlexible: this.graphSettings.nodeRadiusFlexible,
538
+ radiusInitial: this.graphSettings.nodeRadiusInitial,
539
+ radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
540
+ radiusFactor: this.graphSettings.nodeRadiusFactor,
446
541
  linkCount: node.linkCount,
447
- });
448
- return radius + FORCE_SETTINGS.collideAdditionalRadius;
542
+ }) ?? nodeOptions.radius;
543
+ return radius + this.forceSettings.collideAdditionalRadius;
449
544
  })
450
545
  .strength(this.forceSettings.collideStrength)
451
546
  .iterations(this.forceSettings.collideIterations));
@@ -477,16 +572,43 @@ class GraphCanvas {
477
572
  }
478
573
  }
479
574
  initDraw() {
575
+ function calculateHighlightFading() {
576
+ this.highlightDrawing = true;
577
+ if (!this.highlightFadingWorking) {
578
+ switch (true) {
579
+ case this.highlighFading < 1: {
580
+ this.highlighFading += this.graphSettings.highlightUpStep;
581
+ break;
582
+ }
583
+ default: {
584
+ if (this.highlightedNeighbors)
585
+ this.highlightedNeighbors = null;
586
+ if (this.highlightedNode)
587
+ this.highlightedNode = null;
588
+ break;
589
+ }
590
+ }
591
+ if (this.highlighFading < 1 && !this.simulationWorking)
592
+ return void requestAnimationFrame(() => this.draw());
593
+ }
594
+ if (this.highlightFadingWorking) {
595
+ switch (true) {
596
+ case this.highlighFading > this.graphSettings.highlightFadingMin: {
597
+ this.highlighFading -= this.graphSettings.highlightDownStep;
598
+ break;
599
+ }
600
+ }
601
+ if (this.highlighFading > this.graphSettings.highlightFadingMin && !this.simulationWorking)
602
+ return void requestAnimationFrame(() => this.draw());
603
+ }
604
+ this.highlightDrawing = false;
605
+ }
480
606
  function draw() {
481
607
  if (!this.context)
482
608
  return;
483
609
  if (this.listeners.onDraw) {
484
610
  return void this.listeners.onDraw(this.context, this.areaTransform);
485
611
  }
486
- if (this.highlightedNeighbors && this.highlightedNode) {
487
- if (this.fading === 0)
488
- this.fading = 1;
489
- }
490
612
  this.context.save();
491
613
  this.context.clearRect(0, 0, this.width, this.height);
492
614
  this.context.translate(this.areaTransform.x, this.areaTransform.y);
@@ -496,15 +618,12 @@ class GraphCanvas {
496
618
  this.links.forEach(drawLink.bind(this));
497
619
  this.context.stroke();
498
620
  /** nodes */
499
- this.nodes.forEach(drawNode.bind(this));
621
+ const textRenders = [];
622
+ this.nodes.forEach(drawNode(textRenders).bind(this));
623
+ textRenders.forEach((r) => r());
500
624
  this.context.restore();
501
625
  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
- }
626
+ calculateHighlightFading.bind(this)();
508
627
  }
509
628
  function drawLink(link, index) {
510
629
  if (!this.context ||
@@ -520,7 +639,7 @@ class GraphCanvas {
520
639
  if (this.highlightedNeighbors && this.highlightedNode) {
521
640
  if (this.highlightedNode.id != link.source.id &&
522
641
  this.highlightedNode.id != link.target.id) {
523
- alpha = this.fading;
642
+ alpha = this.highlighFading;
524
643
  }
525
644
  this.context.beginPath();
526
645
  }
@@ -532,41 +651,58 @@ class GraphCanvas {
532
651
  if (this.highlightedNeighbors && this.highlightedNode)
533
652
  this.context.stroke();
534
653
  }
535
- function drawNode(node, index) {
654
+ const drawNode = (textRenders) => function drawNode(node, index) {
536
655
  if (!this.context || !node.x || !node.y)
537
656
  return;
538
657
  const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
539
658
  const radius = nodeRadiusGetter({
540
- flexibleRadius: nodeOptions.flexibleRadius,
541
- initialRadius: nodeOptions.initialRadius,
542
- radiusCoefficient: nodeOptions.radiusCoefficient,
543
- radiusFactor: nodeOptions.radiusFactor,
659
+ radiusFlexible: this.graphSettings.nodeRadiusFlexible,
660
+ radiusInitial: this.graphSettings.nodeRadiusInitial,
661
+ radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
662
+ radiusFactor: this.graphSettings.nodeRadiusFactor,
544
663
  linkCount: node.linkCount,
545
- });
664
+ }) ?? nodeOptions.radius;
546
665
  let alpha = nodeOptions.alpha;
547
666
  if (this.highlightedNeighbors && this.highlightedNode) {
548
667
  if (!this.highlightedNeighbors.has(node.id) && this.highlightedNode.id != node.id) {
549
- alpha = this.fading;
668
+ alpha = this.highlighFading;
550
669
  }
551
670
  }
552
671
  this.context.beginPath();
553
672
  this.context.globalAlpha = alpha;
554
673
  /** 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);
674
+ if (nodeOptions.textVisible && nodeOptions.text) {
675
+ textRenders.push(() => {
676
+ if (!this.context || !node.x || !node.y || !nodeOptions.text)
677
+ return;
678
+ this.context.beginPath();
679
+ this.context.globalAlpha = alpha;
680
+ drawText({
681
+ id: node.id,
682
+ cachedNodeText: this.cachedNodeText,
683
+ context: this.context,
684
+ text: nodeOptions.text,
685
+ textAlign: nodeOptions.textAlign,
686
+ textColor: nodeOptions.textColor,
687
+ textFont: nodeOptions.textFont,
688
+ textSize: nodeOptions.textSize,
689
+ x: node.x + nodeOptions.textShiftX,
690
+ y: node.y + radius + nodeOptions.textShiftY,
691
+ maxWidth: nodeOptions.textWidth,
692
+ textStyle: nodeOptions.textStyle,
693
+ textWeight: nodeOptions.textWeight,
694
+ textGap: nodeOptions.textGap,
695
+ });
696
+ });
560
697
  }
561
698
  /** circle */
562
- this.context.strokeStyle = nodeOptions.colorOuter;
563
- this.context.lineWidth = nodeOptions.width;
564
- this.context.moveTo(node.x + radius, node.y);
699
+ this.context.lineWidth = nodeOptions.borderWidth;
700
+ this.context.strokeStyle = nodeOptions.borderColor;
701
+ this.context.fillStyle = nodeOptions.color;
565
702
  this.context.arc(node.x, node.y, radius, 0, 2 * Math.PI);
566
- this.context.fillStyle = nodeOptions.colorInner;
567
703
  this.context.fill();
568
704
  this.context.stroke();
569
- }
705
+ };
570
706
  return draw;
571
707
  }
572
708
  initResize() {
@@ -586,9 +722,9 @@ class GraphCanvas {
586
722
  throw new Error("bad init data");
587
723
  this.area.addEventListener("pointermove", (event) => {
588
724
  let currentNode;
589
- if (this.graphSettings.highlightByHover) {
725
+ if (this.graphSettings.highlightByHover && !this.isDragging) {
590
726
  currentNode = nodeByPointerGetter({
591
- nodeCustomOptions: this.nodeSettings.options,
727
+ graphSettings: this.graphSettings,
592
728
  areaRect: this.areaRect,
593
729
  areaTransform: this.areaTransform,
594
730
  mouseEvent: event,
@@ -597,23 +733,25 @@ class GraphCanvas {
597
733
  if (currentNode && currentNode.neighbors && this.highlightedNode !== currentNode) {
598
734
  this.highlightedNode = currentNode;
599
735
  this.highlightedNeighbors = new Set(this.highlightedNode.neighbors);
600
- requestAnimationFrame(() => {
601
- this.draw();
602
- });
736
+ this.highlightFadingWorking = true;
737
+ if (!this.simulationWorking && !this.highlightDrawing)
738
+ requestAnimationFrame(() => {
739
+ this.draw();
740
+ });
603
741
  }
604
742
  else if (!currentNode && this.highlightedNode) {
605
- this.highlightedNode = null;
606
- this.highlightedNeighbors = null;
607
- requestAnimationFrame(() => {
608
- this.draw();
609
- });
743
+ this.highlightFadingWorking = false;
744
+ if (!this.simulationWorking && !this.highlightDrawing)
745
+ requestAnimationFrame(() => {
746
+ this.draw();
747
+ });
610
748
  }
611
749
  }
612
750
  if (!this.listeners.onMove)
613
751
  return;
614
752
  if (!currentNode)
615
753
  currentNode = nodeByPointerGetter({
616
- nodeCustomOptions: this.nodeSettings.options,
754
+ graphSettings: this.graphSettings,
617
755
  areaRect: this.areaRect,
618
756
  areaTransform: this.areaTransform,
619
757
  mouseEvent: event,
@@ -627,7 +765,7 @@ class GraphCanvas {
627
765
  if (!this.listeners.onDoubleClick)
628
766
  return;
629
767
  const currentNode = nodeByPointerGetter({
630
- nodeCustomOptions: this.nodeSettings.options,
768
+ graphSettings: this.graphSettings,
631
769
  areaRect: this.areaRect,
632
770
  areaTransform: this.areaTransform,
633
771
  mouseEvent: event,
@@ -642,7 +780,7 @@ class GraphCanvas {
642
780
  if (!this.listeners.onClick)
643
781
  return;
644
782
  const currentNode = nodeByPointerGetter({
645
- nodeCustomOptions: this.nodeSettings.options,
783
+ graphSettings: this.graphSettings,
646
784
  areaRect: this.areaRect,
647
785
  areaTransform: this.areaTransform,
648
786
  mouseEvent: event,
@@ -654,7 +792,7 @@ class GraphCanvas {
654
792
  if (!this.listeners.onWheelClick)
655
793
  return;
656
794
  const currentNode = nodeByPointerGetter({
657
- nodeCustomOptions: this.nodeSettings.options,
795
+ graphSettings: this.graphSettings,
658
796
  areaRect: this.areaRect,
659
797
  areaTransform: this.areaTransform,
660
798
  mouseEvent: event,
@@ -669,7 +807,7 @@ class GraphCanvas {
669
807
  if (!this.listeners.onContextMenu)
670
808
  return;
671
809
  const currentNode = nodeByPointerGetter({
672
- nodeCustomOptions: this.nodeSettings.options,
810
+ graphSettings: this.graphSettings,
673
811
  areaRect: this.areaRect,
674
812
  areaTransform: this.areaTransform,
675
813
  mouseEvent: event,
@@ -693,17 +831,17 @@ class GraphCanvas {
693
831
  const mouseEvent = event.sourceEvent;
694
832
  const [pointerX, pointerY] = pointerGetter(mouseEvent, this.areaRect, this.areaTransform);
695
833
  let index = 0;
696
- return least(this.nodes, (node) => {
834
+ return greatest(this.nodes, (node) => {
697
835
  if (!node.x || !node.y)
698
836
  return undefined;
699
837
  const nodeOptions = nodeIterationExtractor(node, index, this.nodes, this.areaTransform, this.nodeSettings.options || {}, nodeOptionsGetter);
700
838
  const radius = nodeRadiusGetter({
701
- flexibleRadius: nodeOptions.flexibleRadius,
702
- initialRadius: nodeOptions.initialRadius,
703
- radiusCoefficient: nodeOptions.radiusCoefficient,
704
- radiusFactor: nodeOptions.radiusFactor,
839
+ radiusFlexible: this.graphSettings.nodeRadiusFlexible,
840
+ radiusInitial: this.graphSettings.nodeRadiusInitial,
841
+ radiusCoefficient: this.graphSettings.nodeRadiusCoefficient,
842
+ radiusFactor: this.graphSettings.nodeRadiusFactor,
705
843
  linkCount: node.linkCount,
706
- });
844
+ }) ?? nodeOptions.radius;
707
845
  index++;
708
846
  return this.graphSettings.dragPlaceCoefficient(node, pointerX, pointerY, radius);
709
847
  });
@@ -757,6 +895,8 @@ class GraphCanvas {
757
895
  requestAnimationFrame(() => this.draw());
758
896
  }))
759
897
  .on("dblclick.zoom", null);
898
+ this.areaTransform = new ZoomTransform(1, this.width / 2, this.height / 2);
899
+ zoom().transform(select(this.area), this.areaTransform);
760
900
  }
761
901
  }
762
902