@antv/layout 0.2.5 → 0.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/layout.min.js +1 -1
  2. package/dist/layout.min.js.map +1 -1
  3. package/es/layout/circular.js +4 -4
  4. package/es/layout/circular.js.map +1 -1
  5. package/es/layout/concentric.js +1 -1
  6. package/es/layout/concentric.js.map +1 -1
  7. package/es/layout/constants.js +1 -0
  8. package/es/layout/constants.js.map +1 -1
  9. package/es/layout/dagre.js +3 -3
  10. package/es/layout/dagre.js.map +1 -1
  11. package/es/layout/force2/ForceNBody.d.ts +7 -0
  12. package/es/layout/force2/ForceNBody.js +94 -0
  13. package/es/layout/force2/ForceNBody.js.map +1 -0
  14. package/es/layout/force2/index.d.ts +123 -0
  15. package/es/layout/force2/index.js +609 -0
  16. package/es/layout/force2/index.js.map +1 -0
  17. package/es/layout/gForce.js +2 -2
  18. package/es/layout/gForce.js.map +1 -1
  19. package/es/layout/gpu/gForce.js +1 -1
  20. package/es/layout/gpu/gForce.js.map +1 -1
  21. package/es/layout/grid.js +1 -1
  22. package/es/layout/grid.js.map +1 -1
  23. package/es/layout/index.d.ts +2 -1
  24. package/es/layout/index.js +2 -1
  25. package/es/layout/index.js.map +1 -1
  26. package/es/layout/layout.js +2 -0
  27. package/es/layout/layout.js.map +1 -1
  28. package/es/layout/types.d.ts +61 -0
  29. package/es/util/math.d.ts +21 -2
  30. package/es/util/math.js +111 -4
  31. package/es/util/math.js.map +1 -1
  32. package/lib/layout/circular.js +4 -4
  33. package/lib/layout/circular.js.map +1 -1
  34. package/lib/layout/concentric.js +1 -1
  35. package/lib/layout/concentric.js.map +1 -1
  36. package/lib/layout/constants.js +1 -0
  37. package/lib/layout/constants.js.map +1 -1
  38. package/lib/layout/dagre.js +4 -4
  39. package/lib/layout/dagre.js.map +1 -1
  40. package/lib/layout/force2/ForceNBody.d.ts +7 -0
  41. package/lib/layout/force2/ForceNBody.js +98 -0
  42. package/lib/layout/force2/ForceNBody.js.map +1 -0
  43. package/lib/layout/force2/index.d.ts +123 -0
  44. package/lib/layout/force2/index.js +644 -0
  45. package/lib/layout/force2/index.js.map +1 -0
  46. package/lib/layout/gForce.js +2 -2
  47. package/lib/layout/gForce.js.map +1 -1
  48. package/lib/layout/gpu/gForce.js +1 -1
  49. package/lib/layout/gpu/gForce.js.map +1 -1
  50. package/lib/layout/grid.js +1 -1
  51. package/lib/layout/grid.js.map +1 -1
  52. package/lib/layout/index.d.ts +2 -1
  53. package/lib/layout/index.js +3 -1
  54. package/lib/layout/index.js.map +1 -1
  55. package/lib/layout/layout.js +2 -0
  56. package/lib/layout/layout.js.map +1 -1
  57. package/lib/layout/types.d.ts +61 -0
  58. package/lib/util/math.d.ts +21 -2
  59. package/lib/util/math.js +116 -6
  60. package/lib/util/math.js.map +1 -1
  61. package/package.json +4 -2
  62. package/src/layout/circular.ts +7 -6
  63. package/src/layout/concentric.ts +1 -1
  64. package/src/layout/constants.ts +1 -0
  65. package/src/layout/dagre.ts +1 -1
  66. package/src/layout/force2/ForceNBody.ts +128 -0
  67. package/src/layout/force2/index.ts +743 -0
  68. package/src/layout/gForce.ts +7 -6
  69. package/src/layout/gpu/gForce.ts +4 -3
  70. package/src/layout/grid.ts +1 -1
  71. package/src/layout/index.ts +2 -0
  72. package/src/layout/layout.ts +2 -0
  73. package/src/layout/types.ts +67 -0
  74. package/src/util/math.ts +122 -6
@@ -0,0 +1,743 @@
1
+ /**
2
+ * @fileOverview fruchterman layout
3
+ * @author shiwu.wyy@antfin.com
4
+ */
5
+
6
+ // @ts-nocheck
7
+ import {
8
+ OutNode,
9
+ Edge,
10
+ PointTuple,
11
+ IndexMap,
12
+ Point,
13
+ GForceLayoutOptions,
14
+ Degree,
15
+ NodeMap,
16
+ CentripetalOptions
17
+ } from "../types";
18
+ import { Base } from "../base";
19
+ import { isNumber, isFunction, isArray, getDegreeMap, isObject, getEdgeTerminal, getAvgNodePosition, getCoreNodeAndRelativeLeafNodes } from "../../util";
20
+ import { forceNBody } from "./ForceNBody";
21
+
22
+ type INode = OutNode & {
23
+ size: number | PointTuple;
24
+ };
25
+
26
+
27
+ const proccessToFunc = (
28
+ value: number | Function | undefined,
29
+ defaultV?: number
30
+ ): ((d: any) => number) => {
31
+ let func;
32
+ if (!value) {
33
+ func = (d: any): number => {
34
+ return defaultV || 1;
35
+ };
36
+ } else if (isNumber(value)) {
37
+ func = (d: any): number => {
38
+ return value;
39
+ };
40
+ } else {
41
+ func = value;
42
+ }
43
+ return func as any;
44
+ };
45
+
46
+ /**
47
+ * graphin 中的 force 布局
48
+ */
49
+ export class Force2Layout extends Base {
50
+ /** 布局中心 */
51
+ public center: PointTuple;
52
+
53
+ /** 停止迭代的最大迭代数 */
54
+ public maxIteration: number = 500;
55
+ /** 停止迭代的最大迭代数,兼容 graphin-force */
56
+ public maxIterations: number = 500;
57
+
58
+ /** 是否启动 worker */
59
+ public workerEnabled: boolean = false;
60
+
61
+ /** 弹簧引力系数 */
62
+ public edgeStrength: number | ((d?: any) => number) | undefined = 200;
63
+
64
+ /** 斥力系数 */
65
+ public nodeStrength: number | ((d?: any) => number) | undefined = 1000;
66
+
67
+ /** 库伦系数 */
68
+ public coulombDisScale: number = 0.005;
69
+
70
+ /** 阻尼系数 */
71
+ public damping: number = 0.9;
72
+
73
+ /** 最大速度 */
74
+ public maxSpeed: number = 1000;
75
+
76
+ /** 一次迭代的平均移动距离小于该值时停止迭代 */
77
+ public minMovement: number = 0.4;
78
+
79
+ /** 迭代中衰减 */
80
+ public interval: number = 0.02;
81
+
82
+ /** 斥力的一个系数 */
83
+ public factor: number = 1;
84
+
85
+ /** 每个节点质量的回调函数,若不指定,则默认使用度数作为节点质量 */
86
+ public getMass: ((d?: any) => number) | undefined;
87
+
88
+ /** 每个节点中心力的 x、y、强度的回调函数,若不指定,则没有额外中心力 */
89
+ public getCenter: ((d?: any, degree?: number) => number[]) | undefined;
90
+
91
+ /** 计算画布上下两侧对节点吸引力大小 */
92
+ public defSideCoe?: (node: Node) => number;
93
+
94
+ /** 理想边长 */
95
+ public linkDistance: number | ((edge?: any, source?: any, target?: any) => number) | undefined = 200;
96
+
97
+ /** 理想边长,兼容 graphin-force */
98
+ public defSpringLen: number | ((edge?: any, source?: any, target?: any) => number) | undefined
99
+
100
+ /** 重力大小 */
101
+ public gravity: number = 10;
102
+
103
+ /** 向心力 */
104
+ public centripetalOptions: CentripetalOptions;
105
+
106
+ /** 是否需要叶子节点聚类 */
107
+ public leafCluster: boolean;
108
+
109
+ /** 是否需要全部节点聚类 */
110
+ public clustering: boolean;
111
+
112
+ /** 节点聚类的映射字段 */
113
+ public nodeClusterBy: string;
114
+
115
+ /** 节点聚类作用力系数 */
116
+ public clusterNodeStrength: number | ((node: Node) => number) = 20;
117
+
118
+ /** 是否防止重叠 */
119
+ public preventOverlap: boolean = true;
120
+
121
+ /** 防止重叠时的节点大小,默认从节点数据中取 size */
122
+ public nodeSize: number | number[] | ((d?: any) => number) | undefined;
123
+
124
+ /** 防止重叠的力大小参数 */
125
+ public collideStrength: number = 1;
126
+
127
+ /** 防止重叠时的节点之间最小间距 */
128
+ public nodeSpacing: number | number[] | ((d?: any) => number) | undefined;
129
+
130
+ /** 阈值的使用条件,mean 代表平均移动距离小于 minMovement 时停止迭代,max 代表最大移动距离大时 minMovement 时停时迭代。默认为 mean */
131
+ public distanceThresholdMode: 'mean' | 'max' | 'min' = 'mean';
132
+
133
+ /** 每次迭代结束的回调函数 */
134
+ public tick: (() => void) | null = () => {};
135
+
136
+ /** 是否允许每次迭代结束调用回调函数 */
137
+ public enableTick: boolean;
138
+
139
+ public nodes: INode[] | null = [];
140
+
141
+ public edges: Edge[] | null = [];
142
+
143
+ public width: number = 300;
144
+
145
+ public height: number = 300;
146
+
147
+ public nodeMap: NodeMap = {};
148
+
149
+ public nodeIdxMap: IndexMap = {};
150
+
151
+ public canvasEl: HTMLCanvasElement;
152
+
153
+ public onLayoutEnd: () => void;
154
+
155
+ /** 是否使用 window.setInterval 运行迭代 */
156
+ public animate: Boolean;
157
+
158
+ /** 监控信息,不配置则不计算 */
159
+ public monitor: (params: { energy: number, nodes: INode[], edge: Edge[], iterations: number }) => void;
160
+
161
+ /** 存储节点度数 */
162
+ private degreesMap: { [id: string]: Degree };
163
+
164
+ /** 迭代中的标识 */
165
+ private timeInterval: number;
166
+
167
+ /** 与 minMovement 进行对比的判断停止迭代节点移动距离 */
168
+ private judgingDistance: number;
169
+
170
+ constructor(options?: GForceLayoutOptions) {
171
+ super();
172
+ this.judgingDistance = 0;
173
+ /** 默认的向心配置 */
174
+ this.centripetalOptions = {
175
+ leaf: 2,
176
+ single: 2,
177
+ others: 1,
178
+ // eslint-disable-next-line
179
+ center: (n: any) => {
180
+ return {
181
+ x: this.width / 2,
182
+ y: this.height / 2,
183
+ };
184
+ },
185
+ };
186
+
187
+ this.updateCfg(options);
188
+ }
189
+
190
+ public getCentripetalOptions() {
191
+ const { leafCluster, clustering, nodeClusterBy, nodes, nodeMap, clusterNodeStrength: propsClusterNodeStrength } = this;
192
+
193
+ const getClusterNodeStrength = (node: Node) =>
194
+ typeof propsClusterNodeStrength === 'function' ? propsClusterNodeStrength(node) : propsClusterNodeStrength;
195
+
196
+ let centripetalOptions = {};
197
+ let sameTypeLeafMap: any;
198
+ // 如果传入了需要叶子节点聚类
199
+ if (leafCluster) {
200
+ sameTypeLeafMap = this.getSameTypeLeafMap() || {};
201
+ const relativeNodesType = Array.from(new Set(nodes?.map(node => node[nodeClusterBy])))|| [];
202
+ centripetalOptions = {
203
+ single: 100,
204
+ leaf: (node, nodes, edges) => {
205
+ // 找出与它关联的边的起点或终点出发的所有一度节点中同类型的叶子节点
206
+ const { relativeLeafNodes, sameTypeLeafNodes } = sameTypeLeafMap[node.id] || {};
207
+ // 如果都是同一类型或者每种类型只有1个,则施加默认向心力
208
+ if (sameTypeLeafNodes?.length === relativeLeafNodes?.length || relativeNodesType?.length === 1) {
209
+ return 1;
210
+ }
211
+ return getClusterNodeStrength(node);
212
+ },
213
+ others: 1,
214
+ center: (node, nodes, edges) => {
215
+ const { degree } = node.data?.layout || {};
216
+ // 孤点默认给1个远离的中心点
217
+ if (!degree) {
218
+ return {
219
+ x: 100,
220
+ y: 100,
221
+ };
222
+ }
223
+ let centerNode;
224
+ if (degree === 1) {
225
+ // 如果为叶子节点
226
+ // 找出与它关联的边的起点出发的所有一度节点中同类型的叶子节点
227
+ const { sameTypeLeafNodes = [] } = sameTypeLeafMap[node.id] || {};
228
+ if (sameTypeLeafNodes.length === 1) {
229
+ // 如果同类型的叶子节点只有1个,中心节点置为undefined
230
+ centerNode = undefined;
231
+ } else if (sameTypeLeafNodes.length > 1) {
232
+ // 找出同类型节点平均位置节点的距离最近的节点作为中心节点
233
+ centerNode = getAvgNodePosition(sameTypeLeafNodes);
234
+ }
235
+ } else {
236
+ centerNode = undefined;
237
+ }
238
+ return {
239
+ x: centerNode?.x as number,
240
+ y: centerNode?.y as number,
241
+ };
242
+ },
243
+ };
244
+ }
245
+
246
+ // 如果传入了全局节点聚类
247
+ if (clustering) {
248
+ if (!sameTypeLeafMap) sameTypeLeafMap = this.getSameTypeLeafMap();
249
+ const clusters: string[] = Array.from(new Set(nodes.map((node, i) => {
250
+ return node[nodeClusterBy];
251
+ }))).filter(
252
+ item => item !== undefined,
253
+ );
254
+ const centerNodeInfo: { [key: string]: { x: number; y: number } } = {};
255
+ clusters.forEach(cluster => {
256
+ const sameTypeNodes = nodes.filter(item => item[nodeClusterBy] === cluster).map(node => nodeMap[node.id]);
257
+ // 找出同类型节点平均位置节点的距离最近的节点作为中心节点
258
+ centerNodeInfo[cluster] = getAvgNodePosition(sameTypeNodes);
259
+ });
260
+ centripetalOptions = {
261
+ single: node => getClusterNodeStrength(node),
262
+ leaf: node => getClusterNodeStrength(node),
263
+ others: node => getClusterNodeStrength(node),
264
+ center: (node, nodes, edges) => {
265
+ // 找出同类型节点平均位置节点的距离最近的节点作为中心节点
266
+ const centerNode = centerNodeInfo[node[nodeClusterBy]];
267
+ return {
268
+ x: centerNode?.x as number,
269
+ y: centerNode?.y as number,
270
+ };
271
+ },
272
+ };
273
+ }
274
+
275
+ this.centripetalOptions = {
276
+ ...this.centripetalOptions,
277
+ ...centripetalOptions,
278
+ };
279
+
280
+ const { leaf, single, others } = this.centripetalOptions;
281
+ if (leaf && typeof leaf !== 'function') this.centripetalOptions.leaf = () => leaf;
282
+ if (single && typeof single !== 'function') this.centripetalOptions.single = () => single;
283
+ if (others && typeof others !== 'function') this.centripetalOptions.others = () => others;
284
+ }
285
+
286
+ public updateCfg(cfg: any) {
287
+ if (cfg) Object.assign(this, cfg);
288
+ }
289
+
290
+ public getDefaultCfg() {
291
+ return {
292
+ maxIteration: 500,
293
+ gravity: 10,
294
+ enableTick: true,
295
+ animate: true,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * 执行布局
301
+ */
302
+ public execute() {
303
+ const self = this;
304
+ self.stop();
305
+ const { nodes, edges, defSpringLen } = self;
306
+
307
+ self.judgingDistance = 0;
308
+
309
+ if (!nodes || nodes.length === 0) {
310
+ self.onLayoutEnd([]);
311
+ return;
312
+ }
313
+
314
+ if (!self.width && typeof window !== "undefined") {
315
+ self.width = window.innerWidth;
316
+ }
317
+ if (!self.height && typeof window !== "undefined") {
318
+ self.height = window.innerHeight;
319
+ }
320
+ if (!self.center) {
321
+ self.center = [self.width / 2, self.height / 2];
322
+ }
323
+ const center = self.center;
324
+
325
+ if (nodes.length === 1) {
326
+ nodes[0].x = center[0];
327
+ nodes[0].y = center[1];
328
+ self.onLayoutEnd([{ ...node[0] }]);
329
+ return;
330
+ }
331
+ self.degreesMap = getDegreeMap(nodes, edges);
332
+ if (!self.getMass) {
333
+ self.getMass = (d) => {
334
+ let massWeight = 1;
335
+ if (isNumber(d.mass)) massWeight = d.mass;
336
+ const degree = self.degreesMap[d.id].all;
337
+ return (!degree || degree < 5) ? massWeight : degree * 5 * massWeight;
338
+ };
339
+ }
340
+
341
+
342
+ // node size function
343
+ const nodeSize = self.nodeSize;
344
+ let nodeSizeFunc;
345
+ if (self.preventOverlap) {
346
+ const nodeSpacing = self.nodeSpacing;
347
+ let nodeSpacingFunc: (d?: any) => number;
348
+ if (isNumber(nodeSpacing)) {
349
+ nodeSpacingFunc = () => nodeSpacing as number;
350
+ } else if (isFunction(nodeSpacing)) {
351
+ nodeSpacingFunc = nodeSpacing as (d?: any) => number;
352
+ } else {
353
+ nodeSpacingFunc = () => 0;
354
+ }
355
+ if (!nodeSize) {
356
+ nodeSizeFunc = (d: INode) => {
357
+ if (d.size) {
358
+ if (isArray(d.size)) {
359
+ return Math.max(d.size[0], d.size[1]) + nodeSpacingFunc(d);
360
+ } if(isObject(d.size)) {
361
+ return Math.max(d.size.width, d.size.height) + nodeSpacingFunc(d);
362
+ }
363
+ return (d.size as number) + nodeSpacingFunc(d);
364
+ }
365
+ return 10 + nodeSpacingFunc(d);
366
+ };
367
+ } else if (isArray(nodeSize)) {
368
+ nodeSizeFunc = (d: INode) => {
369
+ return Math.max(nodeSize[0], nodeSize[1]) + nodeSpacingFunc(d);
370
+ };
371
+ } else {
372
+ nodeSizeFunc = (d: INode) => (nodeSize as number) + nodeSpacingFunc(d);
373
+ }
374
+ }
375
+ self.nodeSize = nodeSizeFunc;
376
+
377
+ self.linkDistance = proccessToFunc(self.linkDistance, 1);
378
+ self.nodeStrength = proccessToFunc(self.nodeStrength, 1);
379
+ self.edgeStrength = proccessToFunc(self.edgeStrength, 1);
380
+
381
+ const nodeMap: NodeMap = {};
382
+ const nodeIdxMap: IndexMap = {};
383
+ nodes.forEach((node, i) => {
384
+ if (!isNumber(node.x)) node.x = Math.random() * self.width;
385
+ if (!isNumber(node.y)) node.y = Math.random() * self.height;
386
+ const degree = self.degreesMap[node.id];
387
+ nodeMap[node.id] = {
388
+ ...node,
389
+ data: {
390
+ ...node.data,
391
+ size: self.nodeSize(node) || 30,
392
+ layout: {
393
+ inDegree: degree.in,
394
+ outDegree: degree.out,
395
+ degree: degree.all,
396
+ tDegree: degree.in,
397
+ sDegree: degree.out,
398
+ force: {
399
+ mass: self.getMass(node),
400
+ nodeStrength: self.nodeStrength(node)
401
+ }
402
+ }
403
+ }
404
+ };
405
+ nodeIdxMap[node.id] = i;
406
+ });
407
+ self.nodeMap = nodeMap;
408
+ self.nodeIdxMap = nodeIdxMap;
409
+
410
+
411
+ self.edgeInfos = [];
412
+ edges?.forEach(edge => {
413
+ const sourceNode = nodeMap[edge.source];
414
+ const targetNode = nodeMap[edge.target];
415
+ if (!sourceNode || !targetNode) {
416
+ elf.edgeInfos.push({});
417
+ } else {
418
+ self.edgeInfos.push({
419
+ edgeStrength: self.edgeStrength(edge),
420
+ linkDistance: defSpringLen ? defSpringLen(
421
+ {
422
+ ...edge,
423
+ source: sourceNode,
424
+ target: targetNode
425
+ },
426
+ sourceNode,
427
+ targetNode
428
+ ) : self.linkDistance(edge, sourceNode, targetNode) || 1 + ((nodeSize(sourceNode) + nodeSize(sourceNode)) || 0) / 2
429
+ })
430
+ }
431
+ })
432
+
433
+ this.getCentripetalOptions();
434
+
435
+ self.onLayoutEnd = self.onLayoutEnd || (() => {});
436
+
437
+ self.run();
438
+ }
439
+
440
+ public run() {
441
+ const self = this;
442
+ const { maxIteration, maxIterations, nodes, workerEnabled, minMovement, animate, minMovement, nodeMap } = self;
443
+
444
+ if (!nodes) return;
445
+
446
+ const velArray: number[] = [];
447
+ nodes.forEach((_, i) => {
448
+ velArray[2 * i] = 0;
449
+ velArray[2 * i + 1] = 0;
450
+ });
451
+
452
+ const maxIter = maxIterations || maxIteration;
453
+ const silence = !animate;
454
+ if (workerEnabled || silence) {
455
+ let usedIter = 0;
456
+ for (let i = 0; (self.judgingDistance > minMovement || i < 1) && i < maxIter; i++) {
457
+ usedIter = i;
458
+ self.runOneStep(i, velArray);
459
+ }
460
+ self.onLayoutEnd(Object.values(nodeMap));
461
+ } else {
462
+ if (typeof window === "undefined") return;
463
+ let iter = 0;
464
+ // interval for render the result after each iteration
465
+ this.timeInterval = window.setInterval(() => {
466
+ if (!nodes) return;
467
+ self.runOneStep(iter, velArray);
468
+ iter++;
469
+ if (iter >= maxIter || self.judgingDistance < minMovement) {
470
+ self.onLayoutEnd(Object.values(nodeMap));
471
+ window.clearInterval(self.timeInterval);
472
+ }
473
+ }, 0);
474
+ }
475
+ }
476
+
477
+ private runOneStep(iter: number, velArray: number[]) {
478
+ const self = this;
479
+ const { nodes, edges, nodeMap, monitor } = self;
480
+ const accArray: number[] = [];
481
+ if (!nodes?.length) return;
482
+ self.calRepulsive(accArray);
483
+ if (edges) self.calAttractive(accArray);
484
+ self.calGravity(accArray);
485
+ const stepInterval = self.interval; // Math.max(0.02, self.interval - iter * 0.002);
486
+ self.updateVelocity(accArray, velArray, stepInterval);
487
+ self.updatePosition(velArray, stepInterval);
488
+ self.tick?.();
489
+
490
+ /** 如果需要监控信息,则提供给用户 */
491
+ if (monitor) {
492
+ const energy = this.calTotalEnergy(accArray);
493
+ monitor({ energy, nodes, edges, iterations: iter });
494
+ }
495
+ }
496
+
497
+ private calTotalEnergy(accArray: number[]) {
498
+ const { nodes, nodeMap } = this;
499
+ if (!nodes?.length) return 0;
500
+ let energy = 0.0;
501
+
502
+ nodes.forEach((node, i) => {
503
+ const vx = accArray[2 * i];
504
+ const vy = accArray[2 * i + 1];
505
+ const speed2 = vx * vx + vy * vy;
506
+ const { mass = 1} = nodeMap[node.id].data.layout.force;
507
+ energy += mass * speed2 * 0.5; // p = 1/2*(mv^2)
508
+ });
509
+
510
+ return energy;
511
+ };
512
+
513
+ // coulombs law
514
+ public calRepulsive(accArray: number[]) {
515
+ const self = this;
516
+ const { nodes, nodeMap, factor, coulombDisScale } = self;
517
+ const nodeSize = self.nodeSize as Function;
518
+ forceNBody(nodes, nodeMap, factor, coulombDisScale * coulombDisScale, accArray);
519
+ }
520
+
521
+ // hooks law
522
+ public calAttractive(accArray: number[]) {
523
+ const self = this;
524
+ const { edges, nodeMap, nodeIdxMap, edgeInfos } = self;
525
+ const nodeSize = self.nodeSize as Function;
526
+ edges.forEach((edge, i) => {
527
+ const source = getEdgeTerminal(edge, 'source');
528
+ const target = getEdgeTerminal(edge, 'target');
529
+ const sourceNode = nodeMap[source];
530
+ const targetNode = nodeMap[target];
531
+ if (!sourceNode || !targetNode) return;
532
+ let vecX = targetNode.x - sourceNode.x;
533
+ let vecY = targetNode.y - sourceNode.y;
534
+ if (!vecX && !vecY) {
535
+ vecX = Math.random() * 0.01;
536
+ vecY = Math.random() * 0.01;
537
+ }
538
+ const vecLength = Math.sqrt(vecX * vecX + vecY * vecY);
539
+ const direX = vecX / vecLength;
540
+ const direY = vecY / vecLength;
541
+ // @ts-ignore
542
+ const { linkDistance = 200, edgeStrength = 200 } = edgeInfos[i] || {};
543
+ const diff = linkDistance - vecLength;
544
+ const param = diff * edgeStrength;
545
+ const massSource = sourceNode.data.layout.force.mass || 1;
546
+ const massTarget = targetNode.data.layout.force.mass || 1;
547
+ // 质量占比越大,对另一端影响程度越大
548
+ const sourceMassRatio = 1 / massSource;
549
+ const targetMassRatio = 1 / massTarget;
550
+ const disX = direX * param;
551
+ const disY = direY * param;
552
+ const sourceIdx = 2 * nodeIdxMap[source];
553
+ const targetIdx = 2 * nodeIdxMap[target];
554
+ accArray[sourceIdx] -= disX * sourceMassRatio;
555
+ accArray[sourceIdx + 1] -= disY * sourceMassRatio;
556
+ accArray[targetIdx] += disX * targetMassRatio;
557
+ accArray[targetIdx + 1] += disY * targetMassRatio;
558
+ });
559
+ }
560
+
561
+ // attract to center
562
+ public calGravity(accArray: number[]) {
563
+ const self = this;
564
+ const { nodes, edges = [], nodeMap, width, height, center, gravity: defaultGravity, degreesMap, centripetalOptions } = self;
565
+ if (!nodes) return;
566
+ const nodeLength = nodes.length;
567
+ for (let i = 0; i < nodeLength; i++) {
568
+ const idx = 2 * i;
569
+ const node = nodeMap[nodes[i].id];
570
+ const { mass = 1 } = node.data.layout.force;
571
+ let vecX = 0;
572
+ let vecY = 0;
573
+ let gravity = defaultGravity;
574
+
575
+ const { in: inDegree, out: outDegree, all: degree } = degreesMap[node.id];
576
+ const forceCenter = self.getCenter?.(node, degree);
577
+ if (forceCenter) {
578
+ const [centerX, centerY, strength] = forceCenter;
579
+ vecX = node.x - centerX;
580
+ vecY = node.y - centerY;
581
+ gravity = strength;
582
+ } else {
583
+ vecX = node.x - center[0];
584
+ vecY = node.y - center[1];
585
+ }
586
+
587
+ if (gravity) {
588
+ accArray[idx] -= gravity * vecX / mass;
589
+ accArray[idx + 1] -= gravity * vecY / mass;
590
+ }
591
+
592
+ if (centripetalOptions) {
593
+ const { leaf, single, others, center: centriCenter } = centripetalOptions;
594
+ const { x: centriX, y: centriY, centerStrength } = centriCenter?.(node, nodes, edges, width, height) || { x: 0, y: 0, centerStrength: 0};
595
+ if (!isNumber(centriX) || !isNumber(centriY)) continue;
596
+ const vx = (node.x - centriX) / mass;
597
+ const vy = (node.y - centriY) / mass;
598
+ if (centerStrength) {
599
+ accArray[idx] -= centerStrength * vx;
600
+ accArray[idx + 1] -= centerStrength * vy;
601
+ }
602
+
603
+ // 孤点
604
+ if (degree === 0) {
605
+ const singleStrength = single(node);
606
+ if (!singleStrength) continue;
607
+ accArray[idx] -= singleStrength * vx;
608
+ accArray[idx + 1] -= singleStrength * vy;
609
+ continue;
610
+ }
611
+
612
+ // 没有出度或没有入度,都认为是叶子节点
613
+ if (inDegree === 0 || outDegree === 0) {
614
+ const leafStrength = leaf(node, nodes, edges);
615
+ if (!leafStrength) continue;
616
+ accArray[idx] -= leafStrength * vx;
617
+ accArray[idx + 1] -= leafStrength * vy;
618
+ continue;
619
+ }
620
+
621
+ /** others */
622
+ const othersStrength = others(node)
623
+ if (!othersStrength) continue;
624
+ accArray[idx] -= othersStrength * vx;
625
+ accArray[idx + 1] -= othersStrength * vy;
626
+ }
627
+ }
628
+ }
629
+
630
+ // TODO: 待 graphin 修改正确
631
+ // public attractToSide(accArray: number[]) {
632
+ // const { defSideCoe, height, nodes } = this;
633
+ // if (!defSideCoe || typeof defSideCoe !== 'function' || !nodes?.length) return;
634
+ // nodes.forEach((node, i) => {
635
+ // const sideCoe = defSideCoe!(node);
636
+ // if (sideCoe === 0) return;
637
+ // const targetY = sideCoe > 0 ? 0 : height;
638
+ // const strength = Math.abs(sideCoe);
639
+ // accArray[2 * i + 1] -= strength * (targetY - node.y);
640
+ // });
641
+ // };
642
+
643
+ public updateVelocity(
644
+ accArray: number[],
645
+ velArray: number[],
646
+ stepInterval: number
647
+ ) {
648
+ const self = this;
649
+ const { nodes, damping, maxSpeed } = self;
650
+ if (!nodes?.length) return;
651
+ nodes.forEach((_, i) => {
652
+ let vx = (velArray[2 * i] + accArray[2 * i] * stepInterval) * damping || 0.01;
653
+ let vy = (velArray[2 * i + 1] + accArray[2 * i + 1] * stepInterval) * damping || 0.01;
654
+ const vLength = Math.sqrt(vx * vx + vy * vy);
655
+ if (vLength > maxSpeed) {
656
+ const param2 = maxSpeed / vLength;
657
+ vx = param2 * vx;
658
+ vy = param2 * vy;
659
+ }
660
+ velArray[2 * i] = vx;
661
+ velArray[2 * i + 1] = vy;
662
+ });
663
+ }
664
+
665
+ public updatePosition(
666
+ velArray: number[],
667
+ stepInterval: number,
668
+ ) {
669
+ const self = this;
670
+ const { nodes, distanceThresholdMode, nodeMap } = self;
671
+ if (!nodes?.length) {
672
+ this.judgingDistance = 0;
673
+ return;
674
+ }
675
+ let sum = 0;
676
+ if (distanceThresholdMode === 'max') self.judgingDistance = -Infinity;
677
+ else if (distanceThresholdMode === 'min') self.judgingDistance = Infinity;
678
+
679
+ nodes.forEach((node: any, i) => {
680
+ const mappedNode = nodeMap[node.id];
681
+ if (isNumber(node.fx) && isNumber(node.fy)) {
682
+ node.x = node.fx;
683
+ node.y = node.fy;
684
+ mappedNode.x = node.x;
685
+ mappedNode.y = node.y;
686
+ return;
687
+ }
688
+ const distX = velArray[2 * i] * stepInterval;
689
+ const distY = velArray[2 * i + 1] * stepInterval;
690
+ node.x += distX;
691
+ node.y += distY;
692
+ mappedNode.x = node.x;
693
+ mappedNode.y = node.y;
694
+
695
+ const distanceMagnitude = Math.sqrt(distX * distX + distY * distY);
696
+ switch (distanceThresholdMode) {
697
+ case 'max':
698
+ if (self.judgingDistance < distanceMagnitude) self.judgingDistance = distanceMagnitude;
699
+ break;
700
+ case 'min':
701
+ if (self.judgingDistance > distanceMagnitude) self.judgingDistance = distanceMagnitude;
702
+ break;
703
+ default:
704
+ sum = sum + distanceMagnitude;
705
+ break;
706
+ }
707
+ });
708
+ if (!distanceThresholdMode || distanceThresholdMode === 'mean') self.judgingDistance = sum / nodes.length;
709
+ }
710
+
711
+ public stop() {
712
+ if (this.timeInterval && typeof window !== "undefined") {
713
+ window.clearInterval(this.timeInterval);
714
+ }
715
+ }
716
+
717
+ public destroy() {
718
+ const self = this;
719
+ self.stop();
720
+ self.tick = null;
721
+ self.nodes = null;
722
+ self.edges = null;
723
+ self.destroyed = true;
724
+ }
725
+
726
+ public getType() {
727
+ return "force2";
728
+ }
729
+
730
+ private getSameTypeLeafMap() {
731
+ const { nodeClusterBy, nodes, edges, nodeMap, degreesMap } = this;
732
+ if (!nodes?.length) return;
733
+ // eslint-disable-next-line
734
+ const sameTypeLeafMap: { [nodeId: string]: any } = {};
735
+ nodes.forEach((node, i) => {
736
+ const degree = degreesMap[node.id].all
737
+ if (degree === 1) {
738
+ sameTypeLeafMap[node.id] = getCoreNodeAndRelativeLeafNodes('leaf', node, edges, nodeClusterBy, degreesMap, nodeMap);
739
+ }
740
+ });
741
+ return sameTypeLeafMap;
742
+ };
743
+ }