@antv/layout 0.2.2 → 0.2.5
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/dist/layout.min.js +1 -1
- package/dist/layout.min.js.map +1 -1
- package/es/layout/comboCombined.js +6 -21
- package/es/layout/comboCombined.js.map +1 -1
- package/es/layout/grid.js +2 -0
- package/es/layout/grid.js.map +1 -1
- package/es/layout/types.d.ts +7 -3
- package/lib/layout/comboCombined.js +6 -21
- package/lib/layout/comboCombined.js.map +1 -1
- package/lib/layout/grid.js +2 -0
- package/lib/layout/grid.js.map +1 -1
- package/lib/layout/types.d.ts +7 -3
- package/package.json +2 -1
- package/src/index.ts +7 -0
- package/src/layout/base.ts +54 -0
- package/src/layout/circular.ts +369 -0
- package/src/layout/comboCombined.ts +391 -0
- package/src/layout/comboForce.ts +873 -0
- package/src/layout/concentric.ts +289 -0
- package/src/layout/constants.ts +21 -0
- package/src/layout/dagre/graph.ts +104 -0
- package/src/layout/dagre/index.ts +31 -0
- package/src/layout/dagre/src/acyclic.ts +58 -0
- package/src/layout/dagre/src/add-border-segments.ts +47 -0
- package/src/layout/dagre/src/coordinate-system.ts +77 -0
- package/src/layout/dagre/src/data/list.ts +60 -0
- package/src/layout/dagre/src/debug.ts +30 -0
- package/src/layout/dagre/src/greedy-fas.ts +144 -0
- package/src/layout/dagre/src/layout.ts +580 -0
- package/src/layout/dagre/src/nesting-graph.ts +143 -0
- package/src/layout/dagre/src/normalize.ts +96 -0
- package/src/layout/dagre/src/order/add-subgraph-constraints.ts +29 -0
- package/src/layout/dagre/src/order/barycenter.ts +26 -0
- package/src/layout/dagre/src/order/build-layer-graph.ts +82 -0
- package/src/layout/dagre/src/order/cross-count.ts +77 -0
- package/src/layout/dagre/src/order/index.ts +105 -0
- package/src/layout/dagre/src/order/init-data-order.ts +27 -0
- package/src/layout/dagre/src/order/init-order.ts +56 -0
- package/src/layout/dagre/src/order/resolve-conflicts.ts +152 -0
- package/src/layout/dagre/src/order/sort-subgraph.ts +105 -0
- package/src/layout/dagre/src/order/sort.ts +76 -0
- package/src/layout/dagre/src/parent-dummy-chains.ts +102 -0
- package/src/layout/dagre/src/position/bk.ts +494 -0
- package/src/layout/dagre/src/position/index.ts +82 -0
- package/src/layout/dagre/src/rank/feasible-tree.ts +165 -0
- package/src/layout/dagre/src/rank/index.ts +54 -0
- package/src/layout/dagre/src/rank/network-simplex.ts +225 -0
- package/src/layout/dagre/src/rank/util.ts +157 -0
- package/src/layout/dagre/src/util.ts +308 -0
- package/src/layout/dagre.ts +423 -0
- package/src/layout/dagreCompound.ts +518 -0
- package/src/layout/er/core.ts +117 -0
- package/src/layout/er/forceGrid.ts +95 -0
- package/src/layout/er/grid.ts +185 -0
- package/src/layout/er/index.ts +68 -0
- package/src/layout/er/mysqlWorkbench.ts +345 -0
- package/src/layout/er/type.ts +39 -0
- package/src/layout/force/force-in-a-box.ts +400 -0
- package/src/layout/force/force.ts +391 -0
- package/src/layout/force/index.ts +1 -0
- package/src/layout/forceAtlas2/body.ts +115 -0
- package/src/layout/forceAtlas2/index.ts +556 -0
- package/src/layout/forceAtlas2/quad.ts +115 -0
- package/src/layout/forceAtlas2/quadTree.ts +107 -0
- package/src/layout/fruchterman.ts +361 -0
- package/src/layout/gForce.ts +487 -0
- package/src/layout/gpu/fruchterman.ts +314 -0
- package/src/layout/gpu/fruchtermanShader.ts +204 -0
- package/src/layout/gpu/gForce.ts +406 -0
- package/src/layout/gpu/gForceShader.ts +221 -0
- package/src/layout/grid.ts +393 -0
- package/src/layout/index.ts +45 -0
- package/src/layout/layout.ts +75 -0
- package/src/layout/mds.ts +140 -0
- package/src/layout/radial/index.ts +1 -0
- package/src/layout/radial/mds.ts +51 -0
- package/src/layout/radial/radial.ts +500 -0
- package/src/layout/radial/radialNonoverlapForce.ts +189 -0
- package/src/layout/random.ts +75 -0
- package/src/layout/types.ts +425 -0
- package/src/registy/index.ts +43 -0
- package/src/util/array.ts +1 -0
- package/src/util/function.ts +64 -0
- package/src/util/gpu.ts +254 -0
- package/src/util/index.ts +6 -0
- package/src/util/math.ts +158 -0
- package/src/util/number.ts +8 -0
- package/src/util/object.ts +28 -0
- package/src/util/string.ts +18 -0
- package/CHANGELOG.md +0 -88
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import Body from './body';
|
|
2
|
+
import Quad from './quad';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileOverview quadTree
|
|
6
|
+
* @author shiwu.wyy@antfin.com
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default class QuadTree {
|
|
10
|
+
public body: Body | null;
|
|
11
|
+
public quad: Quad | null;
|
|
12
|
+
public theta: number;
|
|
13
|
+
public NW: QuadTree | null;
|
|
14
|
+
public NE: QuadTree | null;
|
|
15
|
+
public SW: QuadTree | null;
|
|
16
|
+
public SE: QuadTree | null;
|
|
17
|
+
|
|
18
|
+
// each quadtree represents a quadrant and an aggregate body
|
|
19
|
+
// that represents all bodies inside the quadrant
|
|
20
|
+
constructor(param: Quad | null) {
|
|
21
|
+
/**
|
|
22
|
+
* (aggregated) body in this quad
|
|
23
|
+
* @type {object}
|
|
24
|
+
*/
|
|
25
|
+
this.body = null;
|
|
26
|
+
/**
|
|
27
|
+
* tree representing the northwest quadrant
|
|
28
|
+
* @type {object}
|
|
29
|
+
*/
|
|
30
|
+
this.quad = null;
|
|
31
|
+
this.NW = null;
|
|
32
|
+
this.NE = null;
|
|
33
|
+
this.SW = null;
|
|
34
|
+
this.SE = null;
|
|
35
|
+
/**
|
|
36
|
+
* threshold
|
|
37
|
+
* @type {number}
|
|
38
|
+
*/
|
|
39
|
+
this.theta = 0.5;
|
|
40
|
+
if (param != null) this.quad = param;
|
|
41
|
+
}
|
|
42
|
+
// insert a body(node) into the tree
|
|
43
|
+
insert(bo: Body) {
|
|
44
|
+
// if this node does not contain a body, put the new body bo here
|
|
45
|
+
if (this.body == null) {
|
|
46
|
+
this.body = bo;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// internal node
|
|
50
|
+
if (!this._isExternal()) {
|
|
51
|
+
// update mass info
|
|
52
|
+
this.body = this.body.add(bo);
|
|
53
|
+
// insert body into quadrant
|
|
54
|
+
this._putBody(bo);
|
|
55
|
+
} else { // external node
|
|
56
|
+
// divide this region into four children
|
|
57
|
+
if (this.quad) {
|
|
58
|
+
this.NW = new QuadTree(this.quad.NW());
|
|
59
|
+
this.NE = new QuadTree(this.quad.NE());
|
|
60
|
+
this.SW = new QuadTree(this.quad.SW());
|
|
61
|
+
this.SE = new QuadTree(this.quad.SE());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// insert this body and bo
|
|
65
|
+
this._putBody(this.body);
|
|
66
|
+
this._putBody(bo);
|
|
67
|
+
// update the mass info
|
|
68
|
+
this.body = this.body.add(bo);
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// inserts bo into a quad
|
|
73
|
+
// tslint:disable-next-line
|
|
74
|
+
_putBody(bo: Body) {
|
|
75
|
+
if (!this.quad) return;
|
|
76
|
+
if (bo.in(this.quad.NW()) && this.NW) this.NW.insert(bo);
|
|
77
|
+
else if (bo.in(this.quad.NE()) && this.NE) this.NE.insert(bo);
|
|
78
|
+
else if (bo.in(this.quad.SW()) && this.SW )this.SW.insert(bo);
|
|
79
|
+
else if (bo.in(this.quad.SE()) && this.SE) this.SE.insert(bo);
|
|
80
|
+
}
|
|
81
|
+
// tslint:disable-next-line
|
|
82
|
+
_isExternal() {
|
|
83
|
+
// four children are null
|
|
84
|
+
return (this.NW == null && this.NE == null && this.SW == null && this.SE == null);
|
|
85
|
+
}
|
|
86
|
+
// update the forces
|
|
87
|
+
updateForce(bo: Body) {
|
|
88
|
+
if (this.body == null || bo === this.body) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// if the current node is external
|
|
92
|
+
if (this._isExternal()) bo.addForce(this.body);
|
|
93
|
+
// internal nodes
|
|
94
|
+
else {
|
|
95
|
+
const s = this.quad ? this.quad.getLength() : 0;
|
|
96
|
+
const d = this.body.distanceTo(bo);
|
|
97
|
+
// b is far enough
|
|
98
|
+
if ((s / d) < this.theta) bo.addForce(this.body);
|
|
99
|
+
else {
|
|
100
|
+
this.NW && this.NW.updateForce(bo);
|
|
101
|
+
this.NE && this.NE.updateForce(bo);
|
|
102
|
+
this.SW && this.SW.updateForce(bo);
|
|
103
|
+
this.SE && this.SE.updateForce(bo);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview fruchterman layout
|
|
3
|
+
* @author shiwu.wyy@antfin.com
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
OutNode,
|
|
8
|
+
Edge,
|
|
9
|
+
PointTuple,
|
|
10
|
+
IndexMap,
|
|
11
|
+
Point,
|
|
12
|
+
FruchtermanLayoutOptions
|
|
13
|
+
} from "./types";
|
|
14
|
+
import { Base } from "./base";
|
|
15
|
+
import { getEdgeTerminal, isNumber } from "../util";
|
|
16
|
+
|
|
17
|
+
type NodeMap = {
|
|
18
|
+
[key: string]: INode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type INode = OutNode & {
|
|
22
|
+
cluster: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SPEED_DIVISOR = 800;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* fruchterman 布局
|
|
29
|
+
*/
|
|
30
|
+
export class FruchtermanLayout extends Base {
|
|
31
|
+
/** 布局中心 */
|
|
32
|
+
public center: PointTuple;
|
|
33
|
+
|
|
34
|
+
/** 停止迭代的最大迭代数 */
|
|
35
|
+
public maxIteration: number = 1000;
|
|
36
|
+
|
|
37
|
+
/** 是否启动 worker */
|
|
38
|
+
public workerEnabled: boolean = false;
|
|
39
|
+
|
|
40
|
+
/** 重力大小,影响图的紧凑程度 */
|
|
41
|
+
public gravity: number = 10;
|
|
42
|
+
|
|
43
|
+
/** 速度 */
|
|
44
|
+
public speed: number = 5;
|
|
45
|
+
|
|
46
|
+
/** 是否产生聚类力 */
|
|
47
|
+
public clustering: boolean = false;
|
|
48
|
+
|
|
49
|
+
/** 聚类力大小 */
|
|
50
|
+
public clusterGravity: number = 10;
|
|
51
|
+
|
|
52
|
+
public nodes: INode[] | null = [];
|
|
53
|
+
|
|
54
|
+
public edges: Edge[] | null = [];
|
|
55
|
+
|
|
56
|
+
public width: number = 300;
|
|
57
|
+
|
|
58
|
+
public height: number = 300;
|
|
59
|
+
|
|
60
|
+
public nodeMap: NodeMap = {};
|
|
61
|
+
|
|
62
|
+
public nodeIdxMap: IndexMap = {};
|
|
63
|
+
|
|
64
|
+
/** 迭代结束的回调函数 */
|
|
65
|
+
public onLayoutEnd: () => void = () => {};
|
|
66
|
+
|
|
67
|
+
/** 每次迭代结束的回调函数 */
|
|
68
|
+
public tick: (() => void) | null = () => {};
|
|
69
|
+
|
|
70
|
+
/** 是否使用 window.setInterval 运行迭代 */
|
|
71
|
+
public animate: boolean = true;
|
|
72
|
+
|
|
73
|
+
/** 迭代中的标识 */
|
|
74
|
+
private timeInterval: number;
|
|
75
|
+
|
|
76
|
+
constructor(options?: FruchtermanLayoutOptions) {
|
|
77
|
+
super();
|
|
78
|
+
this.updateCfg(options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public getDefaultCfg() {
|
|
82
|
+
return {
|
|
83
|
+
maxIteration: 1000,
|
|
84
|
+
gravity: 10,
|
|
85
|
+
speed: 1,
|
|
86
|
+
clustering: false,
|
|
87
|
+
clusterGravity: 10,
|
|
88
|
+
animate: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 执行布局
|
|
94
|
+
*/
|
|
95
|
+
public execute() {
|
|
96
|
+
const self = this;
|
|
97
|
+
const nodes = self.nodes;
|
|
98
|
+
|
|
99
|
+
if (self.timeInterval !== undefined && typeof window !== "undefined") {
|
|
100
|
+
window.clearInterval(self.timeInterval);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!nodes || nodes.length === 0) {
|
|
104
|
+
self.onLayoutEnd?.();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!self.width && typeof window !== "undefined") {
|
|
109
|
+
self.width = window.innerWidth;
|
|
110
|
+
}
|
|
111
|
+
if (!self.height && typeof window !== "undefined") {
|
|
112
|
+
self.height = window.innerHeight;
|
|
113
|
+
}
|
|
114
|
+
if (!self.center) {
|
|
115
|
+
self.center = [self.width / 2, self.height / 2];
|
|
116
|
+
}
|
|
117
|
+
const center = self.center;
|
|
118
|
+
|
|
119
|
+
if (nodes.length === 1) {
|
|
120
|
+
nodes[0].x = center[0];
|
|
121
|
+
nodes[0].y = center[1];
|
|
122
|
+
self.onLayoutEnd?.();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const nodeMap: NodeMap = {};
|
|
126
|
+
const nodeIdxMap: IndexMap = {};
|
|
127
|
+
nodes.forEach((node, i) => {
|
|
128
|
+
if (!isNumber(node.x)) node.x = Math.random() * this.width;
|
|
129
|
+
if (!isNumber(node.y)) node.y = Math.random() * this.height;
|
|
130
|
+
nodeMap[node.id] = node;
|
|
131
|
+
nodeIdxMap[node.id] = i;
|
|
132
|
+
});
|
|
133
|
+
self.nodeMap = nodeMap;
|
|
134
|
+
self.nodeIdxMap = nodeIdxMap;
|
|
135
|
+
// layout
|
|
136
|
+
return self.run();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public run() {
|
|
140
|
+
const self = this;
|
|
141
|
+
const nodes = self.nodes;
|
|
142
|
+
if (!nodes) return;
|
|
143
|
+
const { edges, maxIteration, workerEnabled, clustering, animate } = self;
|
|
144
|
+
const clusterMap: {
|
|
145
|
+
[key: string]: {
|
|
146
|
+
name: string | number;
|
|
147
|
+
cx: number;
|
|
148
|
+
cy: number;
|
|
149
|
+
count: number;
|
|
150
|
+
};
|
|
151
|
+
} = {};
|
|
152
|
+
if (clustering) {
|
|
153
|
+
nodes.forEach((n) => {
|
|
154
|
+
if (clusterMap[n.cluster] === undefined) {
|
|
155
|
+
clusterMap[n.cluster] = {
|
|
156
|
+
name: n.cluster,
|
|
157
|
+
cx: 0,
|
|
158
|
+
cy: 0,
|
|
159
|
+
count: 0
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (workerEnabled || !animate) {
|
|
165
|
+
for (let i = 0; i < maxIteration; i++) {
|
|
166
|
+
self.runOneStep(clusterMap);
|
|
167
|
+
}
|
|
168
|
+
self.onLayoutEnd?.();
|
|
169
|
+
} else {
|
|
170
|
+
if (typeof window === "undefined") return;
|
|
171
|
+
let iter = 0;
|
|
172
|
+
// interval for render the result after each iteration
|
|
173
|
+
this.timeInterval = window.setInterval(() => {
|
|
174
|
+
self.runOneStep(clusterMap);
|
|
175
|
+
iter++;
|
|
176
|
+
if (iter >= maxIteration) {
|
|
177
|
+
self.onLayoutEnd?.();
|
|
178
|
+
window.clearInterval(self.timeInterval);
|
|
179
|
+
}
|
|
180
|
+
}, 0);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
nodes,
|
|
184
|
+
edges
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private runOneStep(clusterMap: any) {
|
|
189
|
+
const self = this;
|
|
190
|
+
const nodes = self.nodes;
|
|
191
|
+
if (!nodes) return;
|
|
192
|
+
const { edges, center, gravity, speed, clustering } = self;
|
|
193
|
+
const area = self.height * self.width;
|
|
194
|
+
const maxDisplace = Math.sqrt(area) / 10;
|
|
195
|
+
const k2 = area / (nodes.length + 1);
|
|
196
|
+
const k = Math.sqrt(k2);
|
|
197
|
+
const displacements: Point[] = [];
|
|
198
|
+
nodes.forEach((_, j) => {
|
|
199
|
+
displacements[j] = { x: 0, y: 0 };
|
|
200
|
+
});
|
|
201
|
+
self.applyCalculate(nodes, edges, displacements, k, k2);
|
|
202
|
+
|
|
203
|
+
// gravity for clusters
|
|
204
|
+
if (clustering) {
|
|
205
|
+
// re-compute the clustering centers
|
|
206
|
+
for (const key in clusterMap) {
|
|
207
|
+
clusterMap[key].cx = 0;
|
|
208
|
+
clusterMap[key].cy = 0;
|
|
209
|
+
clusterMap[key].count = 0;
|
|
210
|
+
}
|
|
211
|
+
nodes.forEach((n) => {
|
|
212
|
+
const c = clusterMap[n.cluster];
|
|
213
|
+
if (isNumber(n.x)) {
|
|
214
|
+
c.cx += n.x;
|
|
215
|
+
}
|
|
216
|
+
if (isNumber(n.y)) {
|
|
217
|
+
c.cy += n.y;
|
|
218
|
+
}
|
|
219
|
+
c.count++;
|
|
220
|
+
});
|
|
221
|
+
for (const key in clusterMap) {
|
|
222
|
+
clusterMap[key].cx /= clusterMap[key].count;
|
|
223
|
+
clusterMap[key].cy /= clusterMap[key].count;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// compute the cluster gravity forces
|
|
227
|
+
const clusterGravity = self.clusterGravity || gravity;
|
|
228
|
+
nodes.forEach((n, j) => {
|
|
229
|
+
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
|
230
|
+
const c = clusterMap[n.cluster];
|
|
231
|
+
const distLength = Math.sqrt(
|
|
232
|
+
(n.x - c.cx) * (n.x - c.cx) + (n.y - c.cy) * (n.y - c.cy)
|
|
233
|
+
);
|
|
234
|
+
const gravityForce = k * clusterGravity;
|
|
235
|
+
displacements[j].x -= (gravityForce * (n.x - c.cx)) / distLength;
|
|
236
|
+
displacements[j].y -= (gravityForce * (n.y - c.cy)) / distLength;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// gravity
|
|
241
|
+
nodes.forEach((n, j) => {
|
|
242
|
+
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
|
243
|
+
const gravityForce = 0.01 * k * gravity;
|
|
244
|
+
displacements[j].x -= gravityForce * (n.x - center[0]);
|
|
245
|
+
displacements[j].y -= gravityForce * (n.y - center[1]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// move
|
|
249
|
+
nodes.forEach((n: any, j) => {
|
|
250
|
+
if (isNumber(n.fx) && isNumber(n.fy)) {
|
|
251
|
+
n.x = n.fx;
|
|
252
|
+
n.y = n.fy;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
|
256
|
+
const distLength = Math.sqrt(
|
|
257
|
+
displacements[j].x * displacements[j].x +
|
|
258
|
+
displacements[j].y * displacements[j].y
|
|
259
|
+
);
|
|
260
|
+
if (distLength > 0) {
|
|
261
|
+
// && !n.isFixed()
|
|
262
|
+
const limitedDist = Math.min(
|
|
263
|
+
maxDisplace * (speed / SPEED_DIVISOR),
|
|
264
|
+
distLength
|
|
265
|
+
);
|
|
266
|
+
n.x += (displacements[j].x / distLength) * limitedDist;
|
|
267
|
+
n.y += (displacements[j].y / distLength) * limitedDist;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
self.tick?.();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private applyCalculate(
|
|
275
|
+
nodes: INode[],
|
|
276
|
+
edges: Edge[] | null,
|
|
277
|
+
displacements: Point[],
|
|
278
|
+
k: number,
|
|
279
|
+
k2: number
|
|
280
|
+
) {
|
|
281
|
+
const self = this;
|
|
282
|
+
self.calRepulsive(nodes, displacements, k2);
|
|
283
|
+
if (edges) self.calAttractive(edges, displacements, k);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private calRepulsive(nodes: INode[], displacements: Point[], k2: number) {
|
|
287
|
+
nodes.forEach((v, i) => {
|
|
288
|
+
displacements[i] = { x: 0, y: 0 };
|
|
289
|
+
nodes.forEach((u, j) => {
|
|
290
|
+
if (i === j) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
!isNumber(v.x) ||
|
|
295
|
+
!isNumber(u.x) ||
|
|
296
|
+
!isNumber(v.y) ||
|
|
297
|
+
!isNumber(u.y)
|
|
298
|
+
) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let vecX = v.x - u.x;
|
|
302
|
+
let vecY = v.y - u.y;
|
|
303
|
+
let vecLengthSqr = vecX * vecX + vecY * vecY;
|
|
304
|
+
if (vecLengthSqr === 0) {
|
|
305
|
+
vecLengthSqr = 1;
|
|
306
|
+
const sign = i > j ? 1 : -1;
|
|
307
|
+
vecX = 0.01 * sign;
|
|
308
|
+
vecY = 0.01 * sign;
|
|
309
|
+
}
|
|
310
|
+
const common = k2 / vecLengthSqr;
|
|
311
|
+
displacements[i].x += vecX * common;
|
|
312
|
+
displacements[i].y += vecY * common;
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private calAttractive(edges: Edge[], displacements: Point[], k: number) {
|
|
318
|
+
edges.forEach((e) => {
|
|
319
|
+
const source = getEdgeTerminal(e, 'source');
|
|
320
|
+
const target = getEdgeTerminal(e, 'target');
|
|
321
|
+
if (!source || !target) return;
|
|
322
|
+
const uIndex = this.nodeIdxMap[source];
|
|
323
|
+
const vIndex = this.nodeIdxMap[target];
|
|
324
|
+
if (uIndex === vIndex) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const u = this.nodeMap[source];
|
|
328
|
+
const v = this.nodeMap[target];
|
|
329
|
+
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const vecX = v.x - u.x;
|
|
333
|
+
const vecY = v.y - u.y;
|
|
334
|
+
const vecLength = Math.sqrt(vecX * vecX + vecY * vecY);
|
|
335
|
+
const common = (vecLength * vecLength) / k;
|
|
336
|
+
displacements[vIndex].x -= (vecX / vecLength) * common;
|
|
337
|
+
displacements[vIndex].y -= (vecY / vecLength) * common;
|
|
338
|
+
displacements[uIndex].x += (vecX / vecLength) * common;
|
|
339
|
+
displacements[uIndex].y += (vecY / vecLength) * common;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public stop() {
|
|
344
|
+
if (this.timeInterval && typeof window !== "undefined") {
|
|
345
|
+
window.clearInterval(this.timeInterval);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public destroy() {
|
|
350
|
+
const self = this;
|
|
351
|
+
self.stop();
|
|
352
|
+
self.tick = null;
|
|
353
|
+
self.nodes = null;
|
|
354
|
+
self.edges = null;
|
|
355
|
+
self.destroyed = true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public getType() {
|
|
359
|
+
return "fruchterman";
|
|
360
|
+
}
|
|
361
|
+
}
|