@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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview grid layout
|
|
3
|
+
* @author shiwu.wyy@antfin.com
|
|
4
|
+
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isString, getDegree, isNaN, getFuncByUnknownType } from "../util";
|
|
8
|
+
import { Base } from "./base";
|
|
9
|
+
import {
|
|
10
|
+
OutNode,
|
|
11
|
+
Edge,
|
|
12
|
+
PointTuple,
|
|
13
|
+
Size,
|
|
14
|
+
IndexMap,
|
|
15
|
+
GridLayoutOptions
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
type INode = OutNode & {
|
|
19
|
+
degree: number;
|
|
20
|
+
size: number | PointTuple | Size;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 网格布局
|
|
25
|
+
*/
|
|
26
|
+
export class GridLayout extends Base {
|
|
27
|
+
/** 布局起始点 */
|
|
28
|
+
public begin: PointTuple = [0, 0];
|
|
29
|
+
|
|
30
|
+
/** prevents node overlap, may overflow boundingBox if not enough space */
|
|
31
|
+
public preventOverlap: boolean = true;
|
|
32
|
+
|
|
33
|
+
/** extra spacing around nodes when preventOverlap: true */
|
|
34
|
+
public preventOverlapPadding: number = 10;
|
|
35
|
+
|
|
36
|
+
/** uses all available space on false, uses minimal space on true */
|
|
37
|
+
public condense: boolean = false;
|
|
38
|
+
|
|
39
|
+
/** force num of rows in the grid */
|
|
40
|
+
public rows: number | undefined;
|
|
41
|
+
|
|
42
|
+
/** force num of columns in the grid */
|
|
43
|
+
public cols: number | undefined;
|
|
44
|
+
|
|
45
|
+
/** the spacing between two nodes */
|
|
46
|
+
public nodeSpacing: ((d?: unknown) => number) | number | undefined;
|
|
47
|
+
|
|
48
|
+
/** returns { row, col } for element */
|
|
49
|
+
public position:
|
|
50
|
+
| ((node: INode) => { row?: number; col?: number })
|
|
51
|
+
| undefined;
|
|
52
|
+
|
|
53
|
+
/** a sorting function to order the nodes; e.g. function(a, b){ return a.datapublic ('weight') - b.data('weight') } */
|
|
54
|
+
public sortBy: string = "degree";
|
|
55
|
+
|
|
56
|
+
public nodeSize: number | number[] | { width: number, height: number } | undefined;
|
|
57
|
+
|
|
58
|
+
public nodes: INode[] = [];
|
|
59
|
+
|
|
60
|
+
public edges: Edge[] = [];
|
|
61
|
+
|
|
62
|
+
public width: number = 300;
|
|
63
|
+
|
|
64
|
+
public height: number = 300;
|
|
65
|
+
|
|
66
|
+
private cells: number | undefined;
|
|
67
|
+
|
|
68
|
+
private row: number = 0;
|
|
69
|
+
|
|
70
|
+
private col: number = 0;
|
|
71
|
+
|
|
72
|
+
private splits: number | undefined;
|
|
73
|
+
|
|
74
|
+
private columns: number | undefined;
|
|
75
|
+
|
|
76
|
+
private cellWidth: number = 0;
|
|
77
|
+
|
|
78
|
+
private cellHeight: number = 0;
|
|
79
|
+
|
|
80
|
+
private cellUsed: {
|
|
81
|
+
[key: string]: boolean;
|
|
82
|
+
} = {};
|
|
83
|
+
|
|
84
|
+
private id2manPos: {
|
|
85
|
+
[key: string]: {
|
|
86
|
+
row: number;
|
|
87
|
+
col: number;
|
|
88
|
+
};
|
|
89
|
+
} = {};
|
|
90
|
+
|
|
91
|
+
/** 迭代结束的回调函数 */
|
|
92
|
+
public onLayoutEnd: () => void = () => {};
|
|
93
|
+
|
|
94
|
+
constructor(options?: GridLayoutOptions) {
|
|
95
|
+
super();
|
|
96
|
+
this.updateCfg(options);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public getDefaultCfg() {
|
|
100
|
+
return {
|
|
101
|
+
begin: [0, 0],
|
|
102
|
+
preventOverlap: true,
|
|
103
|
+
preventOverlapPadding: 10,
|
|
104
|
+
condense: false,
|
|
105
|
+
rows: undefined,
|
|
106
|
+
cols: undefined,
|
|
107
|
+
position: undefined,
|
|
108
|
+
sortBy: "degree",
|
|
109
|
+
nodeSize: 30
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 执行布局
|
|
115
|
+
*/
|
|
116
|
+
public execute() {
|
|
117
|
+
const self = this;
|
|
118
|
+
const { nodes, edges, begin } = self;
|
|
119
|
+
const n = nodes.length;
|
|
120
|
+
if (n === 0) {
|
|
121
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
122
|
+
return {
|
|
123
|
+
nodes,
|
|
124
|
+
edges
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (n === 1) {
|
|
128
|
+
nodes[0].x = begin[0];
|
|
129
|
+
nodes[0].y = begin[1];
|
|
130
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
131
|
+
return {
|
|
132
|
+
nodes,
|
|
133
|
+
edges,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let { sortBy, width, height } = self;
|
|
138
|
+
const { condense, preventOverlapPadding, preventOverlap, nodeSpacing: paramNodeSpacing, nodeSize: paramNodeSize } = self;
|
|
139
|
+
|
|
140
|
+
const layoutNodes: INode[] = [];
|
|
141
|
+
nodes.forEach((node) => {
|
|
142
|
+
layoutNodes.push(node);
|
|
143
|
+
});
|
|
144
|
+
const nodeIdxMap: IndexMap = {};
|
|
145
|
+
layoutNodes.forEach((node, i) => {
|
|
146
|
+
nodeIdxMap[node.id] = i;
|
|
147
|
+
});
|
|
148
|
+
if (
|
|
149
|
+
sortBy === "degree" ||
|
|
150
|
+
!isString(sortBy) ||
|
|
151
|
+
(layoutNodes[0] as any)[sortBy] === undefined
|
|
152
|
+
) {
|
|
153
|
+
sortBy = "degree";
|
|
154
|
+
if (isNaN(nodes[0].degree)) {
|
|
155
|
+
const values = getDegree(layoutNodes.length, nodeIdxMap, edges);
|
|
156
|
+
layoutNodes.forEach((node, i) => {
|
|
157
|
+
node.degree = values[i];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// sort nodes by value
|
|
162
|
+
layoutNodes.sort(
|
|
163
|
+
(n1, n2) => (n2 as any)[sortBy] - (n1 as any)[sortBy]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (!width && typeof window !== "undefined") {
|
|
167
|
+
width = window.innerWidth;
|
|
168
|
+
}
|
|
169
|
+
if (!height && typeof window !== "undefined") {
|
|
170
|
+
height = window.innerHeight;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const oRows = self.rows;
|
|
174
|
+
const oCols = self.cols != null ? self.cols : self.columns;
|
|
175
|
+
self.cells = n;
|
|
176
|
+
|
|
177
|
+
// if rows or columns were set in self, use those values
|
|
178
|
+
if (oRows != null && oCols != null) {
|
|
179
|
+
self.rows = oRows;
|
|
180
|
+
self.cols = oCols;
|
|
181
|
+
} else if (oRows != null && oCols == null) {
|
|
182
|
+
self.rows = oRows;
|
|
183
|
+
self.cols = Math.ceil(self.cells / self.rows);
|
|
184
|
+
} else if (oRows == null && oCols != null) {
|
|
185
|
+
self.cols = oCols;
|
|
186
|
+
self.rows = Math.ceil(self.cells / self.cols);
|
|
187
|
+
} else {
|
|
188
|
+
// otherwise use the automatic values and adjust accordingly // otherwise use the automatic values and adjust accordingly
|
|
189
|
+
// width/height * splits^2 = cells where splits is number of times to split width
|
|
190
|
+
self.splits = Math.sqrt((self.cells * self.height) / self.width);
|
|
191
|
+
self.rows = Math.round(self.splits);
|
|
192
|
+
self.cols = Math.round((self.width / self.height) * self.splits);
|
|
193
|
+
}
|
|
194
|
+
self.rows = Math.max(self.rows, 1);
|
|
195
|
+
self.cols = Math.max(self.cols, 1);
|
|
196
|
+
if (self.cols * self.rows > self.cells) {
|
|
197
|
+
// otherwise use the automatic values and adjust accordingly
|
|
198
|
+
// if rounding was up, see if we can reduce rows or columns
|
|
199
|
+
const sm = self.small() as number;
|
|
200
|
+
const lg = self.large() as number;
|
|
201
|
+
|
|
202
|
+
// reducing the small side takes away the most cells, so try it first
|
|
203
|
+
if ((sm - 1) * lg >= self.cells) {
|
|
204
|
+
self.small(sm - 1);
|
|
205
|
+
} else if ((lg - 1) * sm >= self.cells) {
|
|
206
|
+
self.large(lg - 1);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// if rounding was too low, add rows or columns
|
|
210
|
+
while (self.cols * self.rows < self.cells) {
|
|
211
|
+
const sm = self.small() as number;
|
|
212
|
+
const lg = self.large() as number;
|
|
213
|
+
|
|
214
|
+
// try to add to larger side first (adds less in multiplication)
|
|
215
|
+
if ((lg + 1) * sm >= self.cells) {
|
|
216
|
+
self.large(lg + 1);
|
|
217
|
+
} else {
|
|
218
|
+
self.small(sm + 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
self.cellWidth = width / self.cols;
|
|
224
|
+
self.cellHeight = height / self.rows;
|
|
225
|
+
|
|
226
|
+
if (condense) {
|
|
227
|
+
self.cellWidth = 0;
|
|
228
|
+
self.cellHeight = 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if (preventOverlap || paramNodeSpacing) {
|
|
233
|
+
const nodeSpacing: Function = getFuncByUnknownType(10, paramNodeSpacing);
|
|
234
|
+
const nodeSize: Function = getFuncByUnknownType(30, paramNodeSize, false);
|
|
235
|
+
layoutNodes.forEach((node) => {
|
|
236
|
+
if (!node.x || !node.y) {
|
|
237
|
+
// for bb
|
|
238
|
+
node.x = 0;
|
|
239
|
+
node.y = 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const [nodew = 30, nodeh = 30] = nodeSize(node);
|
|
243
|
+
|
|
244
|
+
const p = nodeSpacing !== undefined ? nodeSpacing(node) : preventOverlapPadding;
|
|
245
|
+
|
|
246
|
+
const w = nodew + p;
|
|
247
|
+
const h = nodeh + p;
|
|
248
|
+
|
|
249
|
+
self.cellWidth = Math.max(self.cellWidth, w);
|
|
250
|
+
self.cellHeight = Math.max(self.cellHeight, h);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
self.cellUsed = {}; // e.g. 'c-0-2' => true
|
|
255
|
+
|
|
256
|
+
// to keep track of current cell position
|
|
257
|
+
self.row = 0;
|
|
258
|
+
self.col = 0;
|
|
259
|
+
|
|
260
|
+
// get a cache of all the manual positions
|
|
261
|
+
self.id2manPos = {};
|
|
262
|
+
for (let i = 0; i < layoutNodes.length; i++) {
|
|
263
|
+
const node = layoutNodes[i];
|
|
264
|
+
let rcPos;
|
|
265
|
+
if (self.position) {
|
|
266
|
+
rcPos = self.position(node);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
|
|
270
|
+
// must have at least row or col def'd
|
|
271
|
+
const pos = {
|
|
272
|
+
row: rcPos.row,
|
|
273
|
+
col: rcPos.col
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (pos.col === undefined) {
|
|
277
|
+
// find unused col
|
|
278
|
+
pos.col = 0;
|
|
279
|
+
|
|
280
|
+
while (self.used(pos.row, pos.col)) {
|
|
281
|
+
pos.col++;
|
|
282
|
+
}
|
|
283
|
+
} else if (pos.row === undefined) {
|
|
284
|
+
// find unused row
|
|
285
|
+
pos.row = 0;
|
|
286
|
+
|
|
287
|
+
while (self.used(pos.row, pos.col)) {
|
|
288
|
+
pos.row++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
self.id2manPos[node.id] = pos as { row: number; col: number };
|
|
293
|
+
self.use(pos.row, pos.col);
|
|
294
|
+
}
|
|
295
|
+
self.getPos(node);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
edges,
|
|
302
|
+
nodes: layoutNodes
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private small(val?: number): number | undefined {
|
|
307
|
+
const self = this;
|
|
308
|
+
let res: number | undefined;
|
|
309
|
+
const rows = self.rows || 5;
|
|
310
|
+
const cols = self.cols || 5;
|
|
311
|
+
if (val == null) {
|
|
312
|
+
res = Math.min(rows, cols);
|
|
313
|
+
} else {
|
|
314
|
+
const min = Math.min(rows, cols);
|
|
315
|
+
if (min === self.rows) {
|
|
316
|
+
self.rows = val;
|
|
317
|
+
} else {
|
|
318
|
+
self.cols = val;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return res;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private large(val?: number): number | undefined {
|
|
325
|
+
const self = this;
|
|
326
|
+
let res: number | undefined;
|
|
327
|
+
const rows = self.rows || 5;
|
|
328
|
+
const cols = self.cols || 5;
|
|
329
|
+
if (val == null) {
|
|
330
|
+
res = Math.max(rows, cols);
|
|
331
|
+
} else {
|
|
332
|
+
const max = Math.max(rows, cols);
|
|
333
|
+
if (max === self.rows) {
|
|
334
|
+
self.rows = val;
|
|
335
|
+
} else {
|
|
336
|
+
self.cols = val;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return res;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private used(row: number | undefined, col: number | undefined) {
|
|
343
|
+
const self = this;
|
|
344
|
+
return self.cellUsed[`c-${row}-${col}`] || false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private use(row: number | undefined, col: number | undefined) {
|
|
348
|
+
const self = this;
|
|
349
|
+
self.cellUsed[`c-${row}-${col}`] = true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private moveToNextCell() {
|
|
353
|
+
const self = this;
|
|
354
|
+
const cols = self.cols || 5;
|
|
355
|
+
self.col++;
|
|
356
|
+
if (self.col >= cols) {
|
|
357
|
+
self.col = 0;
|
|
358
|
+
self.row++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private getPos(node: INode) {
|
|
363
|
+
const self = this;
|
|
364
|
+
const { begin, cellWidth, cellHeight } = self;
|
|
365
|
+
let x: number;
|
|
366
|
+
let y: number;
|
|
367
|
+
|
|
368
|
+
// see if we have a manual position set
|
|
369
|
+
const rcPos = self.id2manPos[node.id];
|
|
370
|
+
if (rcPos) {
|
|
371
|
+
x = rcPos.col * cellWidth + cellWidth / 2 + begin[0];
|
|
372
|
+
y = rcPos.row * cellHeight + cellHeight / 2 + begin[1];
|
|
373
|
+
} else {
|
|
374
|
+
// otherwise set automatically
|
|
375
|
+
|
|
376
|
+
while (self.used(self.row, self.col)) {
|
|
377
|
+
self.moveToNextCell();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
x = self.col * cellWidth + cellWidth / 2 + begin[0];
|
|
381
|
+
y = self.row * cellHeight + cellHeight / 2 + begin[1];
|
|
382
|
+
self.use(self.row, self.col);
|
|
383
|
+
|
|
384
|
+
self.moveToNextCell();
|
|
385
|
+
}
|
|
386
|
+
node.x = x;
|
|
387
|
+
node.y = y;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public getType() {
|
|
391
|
+
return "grid";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GridLayout } from "./grid";
|
|
2
|
+
import { RandomLayout } from "./random";
|
|
3
|
+
import { GForceLayout } from "./gForce";
|
|
4
|
+
import { ForceLayout } from "./force";
|
|
5
|
+
import { CircularLayout } from "./circular";
|
|
6
|
+
import { DagreLayout } from "./dagre";
|
|
7
|
+
import { DagreCompoundLayout } from "./dagreCompound";
|
|
8
|
+
import { RadialLayout } from "./radial";
|
|
9
|
+
import { ConcentricLayout } from "./concentric";
|
|
10
|
+
import { MDSLayout } from "./mds";
|
|
11
|
+
import { FruchtermanLayout } from "./fruchterman";
|
|
12
|
+
import { FruchtermanGPULayout } from "./gpu/fruchterman";
|
|
13
|
+
import { GForceGPULayout } from "./gpu/gForce";
|
|
14
|
+
import { ComboForceLayout } from "./comboForce";
|
|
15
|
+
import { ComboCombinedLayout } from "./comboCombined";
|
|
16
|
+
import { ForceAtlas2Layout } from "./forceAtlas2";
|
|
17
|
+
import { ERLayout } from './er';
|
|
18
|
+
|
|
19
|
+
import { Layout, Layouts } from "./layout";
|
|
20
|
+
|
|
21
|
+
export { Layout, Layouts };
|
|
22
|
+
|
|
23
|
+
// layout
|
|
24
|
+
export {
|
|
25
|
+
GridLayout,
|
|
26
|
+
RandomLayout,
|
|
27
|
+
GForceLayout,
|
|
28
|
+
ForceLayout,
|
|
29
|
+
CircularLayout,
|
|
30
|
+
DagreLayout,
|
|
31
|
+
DagreCompoundLayout,
|
|
32
|
+
RadialLayout,
|
|
33
|
+
ConcentricLayout,
|
|
34
|
+
MDSLayout,
|
|
35
|
+
FruchtermanLayout,
|
|
36
|
+
FruchtermanGPULayout,
|
|
37
|
+
GForceGPULayout,
|
|
38
|
+
ComboForceLayout,
|
|
39
|
+
ComboCombinedLayout,
|
|
40
|
+
ForceAtlas2Layout,
|
|
41
|
+
ERLayout
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// types file
|
|
45
|
+
export * from "./types";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Base } from "./base";
|
|
2
|
+
import { Model, ILayout } from "./types";
|
|
3
|
+
import { getLayoutByName } from "../registy";
|
|
4
|
+
import { GridLayout } from "./grid";
|
|
5
|
+
import { RandomLayout } from "./random";
|
|
6
|
+
import { GForceLayout } from "./gForce";
|
|
7
|
+
import { ForceLayout } from "./force";
|
|
8
|
+
import { CircularLayout } from "./circular";
|
|
9
|
+
import { DagreLayout } from "./dagre";
|
|
10
|
+
import { RadialLayout } from "./radial";
|
|
11
|
+
import { ConcentricLayout } from "./concentric";
|
|
12
|
+
import { MDSLayout } from "./mds";
|
|
13
|
+
import { FruchtermanLayout } from "./fruchterman";
|
|
14
|
+
import { FruchtermanGPULayout } from "./gpu/fruchterman";
|
|
15
|
+
import { GForceGPULayout } from "./gpu/gForce";
|
|
16
|
+
import { ComboForceLayout } from "./comboForce";
|
|
17
|
+
import { ComboCombinedLayout } from "./comboCombined";
|
|
18
|
+
import { ForceAtlas2Layout } from "./forceAtlas2";
|
|
19
|
+
import { ERLayout } from "./er";
|
|
20
|
+
import { DagreCompoundLayout } from "./dagreCompound";
|
|
21
|
+
export class Layout {
|
|
22
|
+
public readonly layoutInstance: Base;
|
|
23
|
+
|
|
24
|
+
constructor(options: ILayout.LayoutOptions) {
|
|
25
|
+
const layoutClass = getLayoutByName(options.type as string);
|
|
26
|
+
this.layoutInstance = new layoutClass(options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
layout(data: Model) {
|
|
30
|
+
return this.layoutInstance.layout(data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updateCfg(cfg: ILayout.LayoutOptions) {
|
|
34
|
+
this.layoutInstance.updateCfg(cfg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
init(data: Model) {
|
|
38
|
+
this.layoutInstance.init(data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
execute() {
|
|
42
|
+
this.layoutInstance.execute();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDefaultCfg() {
|
|
46
|
+
return this.layoutInstance.getDefaultCfg();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
destroy() {
|
|
50
|
+
return this.layoutInstance.destroy();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// FIXME
|
|
55
|
+
// FOR G6
|
|
56
|
+
// tslint:disable-next-line
|
|
57
|
+
export const Layouts: { [key: string]: any } = {
|
|
58
|
+
force: ForceLayout,
|
|
59
|
+
fruchterman: FruchtermanLayout,
|
|
60
|
+
forceAtlas2: ForceAtlas2Layout,
|
|
61
|
+
gForce: GForceLayout,
|
|
62
|
+
dagre: DagreLayout,
|
|
63
|
+
dagreCompound: DagreCompoundLayout,
|
|
64
|
+
circular: CircularLayout,
|
|
65
|
+
radial: RadialLayout,
|
|
66
|
+
concentric: ConcentricLayout,
|
|
67
|
+
grid: GridLayout,
|
|
68
|
+
mds: MDSLayout,
|
|
69
|
+
comboForce: ComboForceLayout,
|
|
70
|
+
comboCombined: ComboCombinedLayout,
|
|
71
|
+
random: RandomLayout,
|
|
72
|
+
'gForce-gpu': GForceGPULayout,
|
|
73
|
+
'fruchterman-gpu': FruchtermanGPULayout,
|
|
74
|
+
er: ERLayout,
|
|
75
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview MDS layout
|
|
3
|
+
* @author shiwu.wyy@antfin.com
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Matrix as MLMatrix, SingularValueDecomposition } from "ml-matrix";
|
|
7
|
+
import { PointTuple, OutNode, Edge, Matrix, MDSLayoutOptions } from "./types";
|
|
8
|
+
import { floydWarshall, getAdjMatrix, scaleMatrix } from "../util";
|
|
9
|
+
import { Base } from "./base";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* mds 布局
|
|
13
|
+
*/
|
|
14
|
+
export class MDSLayout extends Base {
|
|
15
|
+
/** 布局中心 */
|
|
16
|
+
public center: PointTuple = [0, 0];
|
|
17
|
+
|
|
18
|
+
/** 边长度 */
|
|
19
|
+
public linkDistance: number = 50;
|
|
20
|
+
|
|
21
|
+
private scaledDistances: Matrix[];
|
|
22
|
+
|
|
23
|
+
public nodes: OutNode[] = [];
|
|
24
|
+
|
|
25
|
+
public edges: Edge[] = [];
|
|
26
|
+
|
|
27
|
+
/** 迭代结束的回调函数 */
|
|
28
|
+
public onLayoutEnd: () => void = () => {};
|
|
29
|
+
|
|
30
|
+
constructor(options?: MDSLayoutOptions) {
|
|
31
|
+
super();
|
|
32
|
+
this.updateCfg(options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public getDefaultCfg() {
|
|
36
|
+
return {
|
|
37
|
+
center: [0, 0],
|
|
38
|
+
linkDistance: 50
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 执行布局
|
|
44
|
+
*/
|
|
45
|
+
public execute() {
|
|
46
|
+
const self = this;
|
|
47
|
+
const { nodes, edges = [] } = self;
|
|
48
|
+
const center = self.center;
|
|
49
|
+
if (!nodes || nodes.length === 0) {
|
|
50
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (nodes.length === 1) {
|
|
54
|
+
nodes[0].x = center[0];
|
|
55
|
+
nodes[0].y = center[1];
|
|
56
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const linkDistance = self.linkDistance;
|
|
60
|
+
// the graph-theoretic distance (shortest path distance) matrix
|
|
61
|
+
const adjMatrix = getAdjMatrix({ nodes, edges }, false);
|
|
62
|
+
const distances = floydWarshall(adjMatrix);
|
|
63
|
+
self.handleInfinity(distances);
|
|
64
|
+
|
|
65
|
+
// scale the ideal edge length acoording to linkDistance
|
|
66
|
+
const scaledD = scaleMatrix(distances, linkDistance);
|
|
67
|
+
self.scaledDistances = scaledD;
|
|
68
|
+
|
|
69
|
+
// get positions by MDS
|
|
70
|
+
const positions = self.runMDS();
|
|
71
|
+
self.positions = positions;
|
|
72
|
+
positions.forEach((p: number[], i: number) => {
|
|
73
|
+
nodes[i].x = p[0] + center[0];
|
|
74
|
+
nodes[i].y = p[1] + center[1];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
nodes,
|
|
81
|
+
edges
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* mds 算法
|
|
87
|
+
* @return {array} positions 计算后的节点位置数组
|
|
88
|
+
*/
|
|
89
|
+
public runMDS(): PointTuple[] {
|
|
90
|
+
const self = this;
|
|
91
|
+
const dimension = 2;
|
|
92
|
+
const distances = self.scaledDistances;
|
|
93
|
+
|
|
94
|
+
// square distances
|
|
95
|
+
const M = MLMatrix.mul(MLMatrix.pow(distances, 2), -0.5);
|
|
96
|
+
|
|
97
|
+
// double centre the rows/columns
|
|
98
|
+
const rowMeans = M.mean("row");
|
|
99
|
+
const colMeans = M.mean("column");
|
|
100
|
+
const totalMean = M.mean();
|
|
101
|
+
M.add(totalMean)
|
|
102
|
+
.subRowVector(rowMeans)
|
|
103
|
+
.subColumnVector(colMeans);
|
|
104
|
+
|
|
105
|
+
// take the SVD of the double centred matrix, and return the
|
|
106
|
+
// points from it
|
|
107
|
+
const ret = new SingularValueDecomposition(M);
|
|
108
|
+
const eigenValues = MLMatrix.sqrt(ret.diagonalMatrix).diagonal();
|
|
109
|
+
return ret.leftSingularVectors.toJSON().map((row: number[]) => {
|
|
110
|
+
return MLMatrix.mul([row], [eigenValues])
|
|
111
|
+
.toJSON()[0]
|
|
112
|
+
.splice(0, dimension) as PointTuple;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public handleInfinity(distances: Matrix[]) {
|
|
117
|
+
let maxDistance = -999999;
|
|
118
|
+
distances.forEach((row) => {
|
|
119
|
+
row.forEach((value) => {
|
|
120
|
+
if (value === Infinity) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (maxDistance < value) {
|
|
124
|
+
maxDistance = value;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
distances.forEach((row, i) => {
|
|
129
|
+
row.forEach((value, j) => {
|
|
130
|
+
if (value === Infinity) {
|
|
131
|
+
distances[i][j] = maxDistance;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public getType() {
|
|
138
|
+
return "mds";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './radial';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PointTuple, Matrix } from '../types';
|
|
2
|
+
import { Matrix as MLMatrix, SingularValueDecomposition } from 'ml-matrix';
|
|
3
|
+
|
|
4
|
+
export default class MDS {
|
|
5
|
+
/** distance matrix */
|
|
6
|
+
public distances: Matrix[];
|
|
7
|
+
|
|
8
|
+
/** dimensions */
|
|
9
|
+
public dimension: number;
|
|
10
|
+
|
|
11
|
+
/** link distance */
|
|
12
|
+
public linkDistance: number;
|
|
13
|
+
|
|
14
|
+
constructor(params: { distances: Matrix[]; dimension?: number; linkDistance: number }) {
|
|
15
|
+
this.distances = params.distances;
|
|
16
|
+
this.dimension = params.dimension || 2;
|
|
17
|
+
this.linkDistance = params.linkDistance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public layout(): PointTuple[] {
|
|
21
|
+
const self = this;
|
|
22
|
+
const { dimension, distances, linkDistance } = self;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// square distances
|
|
26
|
+
const M = MLMatrix.mul(MLMatrix.pow(distances, 2), -0.5);
|
|
27
|
+
|
|
28
|
+
// double centre the rows/columns
|
|
29
|
+
const rowMeans = M.mean('row');
|
|
30
|
+
const colMeans = M.mean('column');
|
|
31
|
+
const totalMean = M.mean();
|
|
32
|
+
M.add(totalMean).subRowVector(rowMeans).subColumnVector(colMeans);
|
|
33
|
+
|
|
34
|
+
// take the SVD of the double centred matrix, and return the
|
|
35
|
+
// points from it
|
|
36
|
+
const ret = new SingularValueDecomposition(M);
|
|
37
|
+
const eigenValues = MLMatrix.sqrt(ret.diagonalMatrix).diagonal();
|
|
38
|
+
return ret.leftSingularVectors.toJSON().map((row: number[]) => {
|
|
39
|
+
return MLMatrix.mul([row], [eigenValues]).toJSON()[0].splice(0, dimension) as PointTuple;
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
const res: PointTuple[] = [];
|
|
43
|
+
for (let i = 0; i < distances.length; i++) {
|
|
44
|
+
const x = Math.random() * linkDistance;
|
|
45
|
+
const y = Math.random() * linkDistance;
|
|
46
|
+
res.push([x, y]);
|
|
47
|
+
}
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|