@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,95 @@
|
|
|
1
|
+
import Grid from './grid';
|
|
2
|
+
import { INode, IEdgeInfo } from './type';
|
|
3
|
+
|
|
4
|
+
export default function layout(data: {
|
|
5
|
+
nodes: INode[],
|
|
6
|
+
edges: IEdgeInfo[],
|
|
7
|
+
}, options: any) {
|
|
8
|
+
if (!data.nodes || data.nodes.length === 0) return data;
|
|
9
|
+
const width = options.width;
|
|
10
|
+
const height = options.height;
|
|
11
|
+
const nodeMinGap = options.nodeMinGap;
|
|
12
|
+
|
|
13
|
+
// 2. 网格布局
|
|
14
|
+
let CELL_W = 10000;
|
|
15
|
+
let CELL_H = 10000;
|
|
16
|
+
data.nodes.forEach((node) => {
|
|
17
|
+
const nodeWidth = node.size[0] || 50;
|
|
18
|
+
const nodeHeight = node.size[1] || 50;
|
|
19
|
+
|
|
20
|
+
CELL_W = Math.min(nodeWidth, CELL_W);
|
|
21
|
+
CELL_H = Math.min(nodeHeight, CELL_H);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const grid = new Grid();
|
|
25
|
+
grid.init(width, height, {
|
|
26
|
+
CELL_H,
|
|
27
|
+
CELL_W,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
data.nodes.forEach((d) => {
|
|
31
|
+
const gridpoint = grid.occupyNearest(d);
|
|
32
|
+
if (gridpoint) {
|
|
33
|
+
gridpoint.node = {
|
|
34
|
+
id: d.id,
|
|
35
|
+
size: d.size,
|
|
36
|
+
};
|
|
37
|
+
d.x = gridpoint.x;
|
|
38
|
+
d.y = gridpoint.y;
|
|
39
|
+
d.dx = gridpoint.dx;
|
|
40
|
+
d.dy = gridpoint.dy;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 加入节点size
|
|
45
|
+
for (let i = 0; i < data.nodes.length; i++) {
|
|
46
|
+
// 节点宽度大于网格宽度,则往当前网格的右边插入列
|
|
47
|
+
const node = data.nodes[i];
|
|
48
|
+
const result = grid.findGridByNodeId(node.id);
|
|
49
|
+
if (!result) throw new Error("can not find node cell");
|
|
50
|
+
|
|
51
|
+
const { column, row } = result;
|
|
52
|
+
if ((node.size[0] + nodeMinGap) > CELL_W) {
|
|
53
|
+
const addGridSize = Math.ceil((node.size[0] +nodeMinGap) / CELL_W) - 1;
|
|
54
|
+
let realAdd = addGridSize;
|
|
55
|
+
// 优化,假设同一列,不同行存在两个size为2的节点,遍历到第一个节点的时候,会往右插入两列,遍历到第二个节点,又往右插入。就会导致多余的网格
|
|
56
|
+
for(let j=0; j< addGridSize; j++) {
|
|
57
|
+
const hasColumn = grid.additionColumn.indexOf(column + j + 1) > -1;
|
|
58
|
+
if (hasColumn && !grid.cells[column + j + 1][row].node) {
|
|
59
|
+
realAdd --;
|
|
60
|
+
} else {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
grid.insertColumn(column, realAdd);
|
|
65
|
+
}
|
|
66
|
+
// 节点高度大于网格宽度,则往当前网格的下边插入行
|
|
67
|
+
if ((node.size[1] +nodeMinGap) > CELL_H) {
|
|
68
|
+
const addGridSize = Math.ceil((node.size[1]+nodeMinGap) / CELL_H) - 1;
|
|
69
|
+
let realAdd = addGridSize;
|
|
70
|
+
for(let j=0; j< addGridSize; j++) {
|
|
71
|
+
const hasColumn = grid.additionRow.indexOf(row + j + 1) > -1;
|
|
72
|
+
if (hasColumn && !grid.cells[column][row + j + 1].node) {
|
|
73
|
+
realAdd --;
|
|
74
|
+
} else {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
grid.insertRow(row, realAdd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 同步节点坐标
|
|
83
|
+
for(let i = 0; i < grid.columnNum; i++) {
|
|
84
|
+
for(let j = 0; j < grid.rowNum; j++) {
|
|
85
|
+
const cell = grid.cells[i][j];
|
|
86
|
+
if (cell.node) {
|
|
87
|
+
const node = data.nodes.find((node) => node.id === cell?.node?.id);
|
|
88
|
+
if (node) {
|
|
89
|
+
node.x = cell.x + node.size[0] / 2;
|
|
90
|
+
node.y = cell.y + node.size[1] / 2;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ICell, INode } from './type';
|
|
2
|
+
|
|
3
|
+
export default class Grid {
|
|
4
|
+
public cells: ICell[][] = [];
|
|
5
|
+
public columnNum:number = 0;
|
|
6
|
+
public rowNum: number = 0;
|
|
7
|
+
|
|
8
|
+
public additionColumn: number[] = [];
|
|
9
|
+
public additionRow: number[] = [];
|
|
10
|
+
private static MIN_DIST = 50;
|
|
11
|
+
private static DEFAULT_CELL_W = 80;
|
|
12
|
+
private static DEFAULT_CELL_H = 80;
|
|
13
|
+
private CELL_W: number;
|
|
14
|
+
private CELL_H: number;
|
|
15
|
+
|
|
16
|
+
public init(width: number, height: number, gridSize: {
|
|
17
|
+
CELL_W: number,
|
|
18
|
+
CELL_H: number,
|
|
19
|
+
}) {
|
|
20
|
+
this.cells = [];
|
|
21
|
+
this.CELL_W = gridSize.CELL_W || Grid.DEFAULT_CELL_W;
|
|
22
|
+
this.CELL_H = gridSize.CELL_H || Grid.DEFAULT_CELL_H;
|
|
23
|
+
this.columnNum = Math.ceil(width / this.CELL_W);
|
|
24
|
+
this.rowNum = Math.ceil(height / this.CELL_H);
|
|
25
|
+
Grid.MIN_DIST = Math.pow(width, 2) + Math.pow(height, 2);
|
|
26
|
+
|
|
27
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
28
|
+
const tmp = [];
|
|
29
|
+
for(let j = 0; j < this.rowNum; j++) {
|
|
30
|
+
const cell = {
|
|
31
|
+
dx: i,
|
|
32
|
+
dy: j,
|
|
33
|
+
x : i * this.CELL_W,
|
|
34
|
+
y : j * this.CELL_H,
|
|
35
|
+
occupied : false
|
|
36
|
+
};
|
|
37
|
+
tmp.push(cell);
|
|
38
|
+
}
|
|
39
|
+
this.cells.push(tmp);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public findGridByNodeId(nodeId: string){
|
|
44
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
45
|
+
for(let j = 0; j < this.rowNum; j++) {
|
|
46
|
+
if(this.cells[i][j].node) {
|
|
47
|
+
if (this.cells[i][j]?.node?.id === nodeId) {
|
|
48
|
+
return {column: i, row: j};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public sqdist(a: any, b: any) {
|
|
57
|
+
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public occupyNearest(p: INode) {
|
|
61
|
+
let minDist = Grid.MIN_DIST;
|
|
62
|
+
let d;
|
|
63
|
+
let candidate = null;
|
|
64
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
65
|
+
for(let j = 0; j < this.rowNum; j++) {
|
|
66
|
+
if(!this.cells[i][j].occupied && ( d = this.sqdist(p, this.cells[i][j])) < minDist) {
|
|
67
|
+
minDist = d;
|
|
68
|
+
candidate = this.cells[i][j];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if(candidate) {
|
|
73
|
+
candidate.occupied = true;
|
|
74
|
+
}
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public insertColumn(columnIndex: number, length: number) {
|
|
79
|
+
if (length <= 0) return ;
|
|
80
|
+
// 插入空列
|
|
81
|
+
for (let i = 0; i < length; i++) {
|
|
82
|
+
this.cells[i + this.columnNum] = [];
|
|
83
|
+
for(let j = 0; j < this.rowNum; j++) {
|
|
84
|
+
this.cells[i + this.columnNum][j] = {
|
|
85
|
+
dx: i,
|
|
86
|
+
dy: j,
|
|
87
|
+
x : i * this.CELL_W,
|
|
88
|
+
y : j * this.CELL_H,
|
|
89
|
+
occupied : false,
|
|
90
|
+
node: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 交换数据
|
|
95
|
+
for(let i = (this.columnNum - 1); i > columnIndex; i--) {
|
|
96
|
+
for (let j = 0; j < this.rowNum; j++) {
|
|
97
|
+
this.cells[i + length][j] = {
|
|
98
|
+
...this.cells[i][j],
|
|
99
|
+
x: (i+length) * this.CELL_W,
|
|
100
|
+
y: j * this.CELL_H,
|
|
101
|
+
};
|
|
102
|
+
this.cells[i][j] = {
|
|
103
|
+
x : i * this.CELL_W,
|
|
104
|
+
y : j * this.CELL_H,
|
|
105
|
+
occupied : true,
|
|
106
|
+
node: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 已有行列的处理
|
|
111
|
+
for (let j = 0; j < this.additionColumn.length; j++) {
|
|
112
|
+
if (this.additionColumn[j] >= columnIndex) {
|
|
113
|
+
this.additionColumn[j] += length;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 记录新增的行列
|
|
117
|
+
for (let i = 0; i < length; i++) {
|
|
118
|
+
this.additionColumn.push(columnIndex + i + 1);
|
|
119
|
+
}
|
|
120
|
+
this.columnNum += length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public insertRow(rowIndex: number, length: number) {
|
|
124
|
+
if (length <= 0) return ;
|
|
125
|
+
// 插入空行
|
|
126
|
+
for (let j = 0; j < length; j++) {
|
|
127
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
128
|
+
this.cells[i][j + this.rowNum] = {
|
|
129
|
+
dx: i,
|
|
130
|
+
dy: j,
|
|
131
|
+
x : i * this.CELL_W,
|
|
132
|
+
y : j * this.CELL_H,
|
|
133
|
+
occupied : false,
|
|
134
|
+
node: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 交换数据
|
|
140
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
141
|
+
for (let j = (this.rowNum - 1); j > rowIndex; j--) {
|
|
142
|
+
this.cells[i][j+length] = {
|
|
143
|
+
...this.cells[i][j],
|
|
144
|
+
dx: i,
|
|
145
|
+
dy: j + length,
|
|
146
|
+
x: i * this.CELL_W,
|
|
147
|
+
y: (j+length) * this.CELL_H,
|
|
148
|
+
};
|
|
149
|
+
this.cells[i][j] = {
|
|
150
|
+
dx: i,
|
|
151
|
+
dy: j,
|
|
152
|
+
x : i * this.CELL_W,
|
|
153
|
+
y : j *this.CELL_H,
|
|
154
|
+
occupied : false,
|
|
155
|
+
node: null,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 已有行列的处理
|
|
162
|
+
for (let j = 0; j < this.additionRow.length; j++) {
|
|
163
|
+
if (this.additionRow[j] >= rowIndex) {
|
|
164
|
+
this.additionRow[j] += length;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 记录新增的行列
|
|
168
|
+
for (let i = 0; i < length; i++) {
|
|
169
|
+
this.additionRow.push(rowIndex + i + 1);
|
|
170
|
+
}
|
|
171
|
+
this.rowNum += length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public getNodes() {
|
|
175
|
+
const nodes = [];
|
|
176
|
+
for(let i = 0; i < this.columnNum; i++) {
|
|
177
|
+
for(let j = 0; j < this.rowNum; j++) {
|
|
178
|
+
if(this.cells[i][j].node) {
|
|
179
|
+
nodes.push(this.cells[i][j]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return nodes;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview Force Layout Grid Align layout
|
|
3
|
+
* @author wenyanqi
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Base } from "../base";
|
|
7
|
+
import layout from './core';
|
|
8
|
+
import { INode } from './type';
|
|
9
|
+
|
|
10
|
+
export interface ERLayoutOptions {
|
|
11
|
+
type: "er";
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
nodeMinGap?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ERLayout extends Base {
|
|
18
|
+
|
|
19
|
+
public width: number = 300;
|
|
20
|
+
public height: number = 300;
|
|
21
|
+
public nodeMinGap: number = 50;
|
|
22
|
+
|
|
23
|
+
/** 迭代结束的回调函数 */
|
|
24
|
+
public onLayoutEnd: () => void = () => { };
|
|
25
|
+
|
|
26
|
+
constructor(options?: any) {
|
|
27
|
+
super();
|
|
28
|
+
if (options) {
|
|
29
|
+
this.updateCfg(options);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public getDefaultCfg() {
|
|
34
|
+
return {
|
|
35
|
+
width: 300,
|
|
36
|
+
height: 300,
|
|
37
|
+
nodeMinGap: 50,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 执行布局
|
|
43
|
+
*/
|
|
44
|
+
public execute() {
|
|
45
|
+
const self = this;
|
|
46
|
+
const nodes = self.nodes;
|
|
47
|
+
const edges = self.edges;
|
|
48
|
+
// 节点初始化,size初始化
|
|
49
|
+
nodes?.forEach((node: INode) => {
|
|
50
|
+
if (!node.size) {
|
|
51
|
+
node.size = [50, 50];
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return layout({
|
|
55
|
+
nodes, edges,
|
|
56
|
+
}, {
|
|
57
|
+
width: this.width,
|
|
58
|
+
height: this.height,
|
|
59
|
+
nodeMinGap: this.nodeMinGap,
|
|
60
|
+
}).then(() => {
|
|
61
|
+
if (self.onLayoutEnd) self.onLayoutEnd();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getType() {
|
|
66
|
+
return "er";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { IEdge, IMysqlNode } from './type';
|
|
2
|
+
|
|
3
|
+
const graphWidth = 1200;
|
|
4
|
+
const graphHeight = 800;
|
|
5
|
+
const OVERLAP_QUOT = 10000000;
|
|
6
|
+
const MIN_DIST = 10;
|
|
7
|
+
const M_PI = 3.14159265358979323846;
|
|
8
|
+
const M_PI_2 = 1.57079632679489661923;
|
|
9
|
+
const PI_38 = M_PI * 0.375;
|
|
10
|
+
const PI_58 = M_PI * 0.625;
|
|
11
|
+
const nodeEdgeMap = new Map();
|
|
12
|
+
const CELL_W = 10;
|
|
13
|
+
const CELL_H = 10;
|
|
14
|
+
let T = 0.8;
|
|
15
|
+
const T_MIN = 0.1;
|
|
16
|
+
const R = 0.5;
|
|
17
|
+
|
|
18
|
+
function distanceToNode(node1: IMysqlNode, node2: IMysqlNode, isHoriz: boolean) {
|
|
19
|
+
const x11 = node1.x - node1.size[0] / 2;
|
|
20
|
+
const y11 = node1.y - node1.size[1] / 2;
|
|
21
|
+
const x12 = node1.x + node1.size[0] / 2;
|
|
22
|
+
const y12 = node1.y + node1.size[1] / 2;
|
|
23
|
+
const x21 = node2.x - node2.size[0] / 2;
|
|
24
|
+
const y21 = node2.y - node2.size[1] / 2;
|
|
25
|
+
const x22 = node2.x + node2.size[0] / 2;
|
|
26
|
+
const y22 = node2.y + node2.size[1] / 2;
|
|
27
|
+
|
|
28
|
+
const cx1 = node1.x;
|
|
29
|
+
const cy1 = node1.y;
|
|
30
|
+
const cx2 = node2.x;
|
|
31
|
+
const cy2 = node2.y;
|
|
32
|
+
const dcx = cx2 - cx1;
|
|
33
|
+
// 两个节点间的方位角
|
|
34
|
+
const qr = Math.atan2(dcx, (cy2 - cy1));
|
|
35
|
+
let dx = 0;
|
|
36
|
+
let dy = 0;
|
|
37
|
+
let l1 = 0;
|
|
38
|
+
let l2 = 0;
|
|
39
|
+
if (qr > M_PI_2) {
|
|
40
|
+
dy = y11 - y22;
|
|
41
|
+
dx = x21 - x12;
|
|
42
|
+
l1 = parseFloat(dy ? (dy / Math.cos(qr)).toFixed(2) : (dx).toFixed(2));
|
|
43
|
+
l2 = parseFloat(dx ? (dx / Math.sin(qr)).toFixed(2) : (dy).toFixed(2));
|
|
44
|
+
} else if (0.0 < qr && qr <= M_PI_2) {
|
|
45
|
+
dy = y21 - y12;
|
|
46
|
+
dx = x21 - x12;
|
|
47
|
+
if (dy > dx) {
|
|
48
|
+
l1 = l2 = parseFloat(dy ? (dy / Math.cos(qr)).toFixed(2) : (dx).toFixed(2));
|
|
49
|
+
} else {
|
|
50
|
+
l1 = l2 = parseFloat(dx ? (dx / Math.sin(qr)).toFixed(2) : (dy).toFixed(2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} else if (qr < -M_PI_2) {
|
|
54
|
+
dy = y11 - y22;
|
|
55
|
+
dx = -(x22 - x11);
|
|
56
|
+
if (dy > dx) {
|
|
57
|
+
l1 = l2 = parseFloat(dy ? (dy / Math.cos(qr)).toFixed(2) : (dx).toFixed(2));
|
|
58
|
+
} else {
|
|
59
|
+
l1 = l2 = parseFloat(dx ? (dx / Math.sin(qr)).toFixed(2) : (dy).toFixed(2));
|
|
60
|
+
}
|
|
61
|
+
}else {
|
|
62
|
+
dy = y21 - y12;
|
|
63
|
+
if (Math.abs(dcx) > (x12 - x11) / 2) {
|
|
64
|
+
dx = x11 - x22;
|
|
65
|
+
} else {
|
|
66
|
+
dx = dcx;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (dy > dx) {
|
|
70
|
+
l1 = l2 = parseFloat(dy ? (dy / Math.cos(qr)).toFixed(2) : (dx).toFixed(2));
|
|
71
|
+
} else {
|
|
72
|
+
l1 = l2 = parseFloat((dx && qr !== 0.0) ? (dx / Math.sin(qr)).toFixed(2) : (dy).toFixed(2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
const aqr = parseFloat(qr.toFixed(2));
|
|
77
|
+
// 判断是否水平,角度
|
|
78
|
+
let newHoriz = isHoriz;
|
|
79
|
+
if (isHoriz) {
|
|
80
|
+
newHoriz = PI_38 < aqr && aqr < PI_58;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
distance: Math.abs(l1 < l2 ? l1 : l2),
|
|
84
|
+
isHoriz: newHoriz,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function calcNodePair(nodeA: IMysqlNode, nodeB: IMysqlNode) {
|
|
89
|
+
// 确定两个节点间是否存在连线
|
|
90
|
+
const edges = nodeEdgeMap.get(nodeA.id) || [];
|
|
91
|
+
const isLinked = edges.find((edge: IEdge) => {
|
|
92
|
+
return edge.source === nodeB.id || edge.target === nodeB.id;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const areaA = nodeA.size[0] * nodeA.size[1];
|
|
96
|
+
const areaB = nodeB.size[0] * nodeB.size[1];
|
|
97
|
+
const node1 = areaA > areaB ? nodeB : nodeA;
|
|
98
|
+
const node2 = areaA > areaB ? nodeA : nodeB;
|
|
99
|
+
|
|
100
|
+
const x11 = node1.x - node1.size[0] / 2;
|
|
101
|
+
const y11 = node1.y - node1.size[1] / 2;
|
|
102
|
+
const x12 = node1.x + node1.size[0] / 2;
|
|
103
|
+
const y12 = node1.y + node1.size[1] / 2;
|
|
104
|
+
const x21 = node2.x - node2.size[0] / 2;
|
|
105
|
+
const y21 = node2.y - node2.size[1] / 2;
|
|
106
|
+
const x22 = node2.x + node2.size[0] / 2;
|
|
107
|
+
const y22 = node2.y + node2.size[1] / 2;
|
|
108
|
+
|
|
109
|
+
const cx1 = node1.x;
|
|
110
|
+
const cy1 = node1.y;
|
|
111
|
+
const cx2 = node2.x;
|
|
112
|
+
const cy2 = node2.y;
|
|
113
|
+
|
|
114
|
+
// Detect if nodes overlap 检查节点之间是否存在覆盖问题
|
|
115
|
+
const isoverlap = ((x12 >= x21) && (x22 >= x11) && (y12 >= y21) && (y22 >= y11));
|
|
116
|
+
let e = 0;
|
|
117
|
+
let distance = 0;
|
|
118
|
+
|
|
119
|
+
if (isoverlap) {
|
|
120
|
+
|
|
121
|
+
distance = Math.sqrt(Math.pow((cx2 - cx1), 2) + Math.pow((cy2 - cy1), 2));
|
|
122
|
+
|
|
123
|
+
// calc area of overlap 计算重复区域的坐标和面积
|
|
124
|
+
const sx1 = x11 > x21 ? x11 : x21;
|
|
125
|
+
const sy1 = y11 > y21 ? y11 : y21;
|
|
126
|
+
const sx2 = x12 < x22 ? x12 : x22;
|
|
127
|
+
const sy2 = y12 < y22 ? y12 : y22;
|
|
128
|
+
const dsx = sx2 - sx1;
|
|
129
|
+
const dsy = sy2 - sy1;
|
|
130
|
+
|
|
131
|
+
const sov = dsx * dsy;
|
|
132
|
+
|
|
133
|
+
if (distance === 0.0) {
|
|
134
|
+
distance = 0.0000001;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
e = MIN_DIST * 1 / distance * 100 + sov;
|
|
138
|
+
e *= OVERLAP_QUOT;
|
|
139
|
+
} else {
|
|
140
|
+
let isHoriz = false;
|
|
141
|
+
const res = distanceToNode(node1, node2, isHoriz);
|
|
142
|
+
distance = res.distance;
|
|
143
|
+
isHoriz = res.isHoriz;
|
|
144
|
+
|
|
145
|
+
if (distance <= MIN_DIST) {
|
|
146
|
+
if (distance !== 0) {
|
|
147
|
+
if (isLinked) {
|
|
148
|
+
e += MIN_DIST + OVERLAP_QUOT * 1 / distance;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
e += MIN_DIST + OVERLAP_QUOT * MIN_DIST / distance;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
e += OVERLAP_QUOT;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
e += distance;
|
|
158
|
+
if (isLinked) {
|
|
159
|
+
e += distance * distance;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return e;
|
|
165
|
+
}
|
|
166
|
+
function calcEnergy(nodes: any) {
|
|
167
|
+
let energy = 0;
|
|
168
|
+
for(let i = 0; i < nodes.length; i++) {
|
|
169
|
+
const node = nodes[i];
|
|
170
|
+
if ((node.x < 0) || (node.y < 0) || (node.x > graphWidth) || (node.y > graphHeight)) {
|
|
171
|
+
energy += 1000000000000;
|
|
172
|
+
}
|
|
173
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
174
|
+
energy += calcNodePair(node, nodes[j]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return energy;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isCorrectPosition(node: IMysqlNode, newPosition: {
|
|
182
|
+
x: number, y: number
|
|
183
|
+
}, nodes: IMysqlNode[], edges: IEdge[]) {
|
|
184
|
+
const nodeIdxMap = new Map<string, IMysqlNode>();
|
|
185
|
+
nodes.forEach((o, i) => {
|
|
186
|
+
nodeIdxMap.set(o.id, o);
|
|
187
|
+
});
|
|
188
|
+
const relateEdges = edges.filter((edge) => edge.source === node.id || edge.target === node.id) || [];
|
|
189
|
+
const relateNodes: IMysqlNode[] = [];
|
|
190
|
+
relateEdges.forEach((edge) => {
|
|
191
|
+
const otherNodeId = edge.source === node.id ? edge.target : edge.source;
|
|
192
|
+
const otherNode = nodeIdxMap.get(otherNodeId);
|
|
193
|
+
if (otherNode) {
|
|
194
|
+
relateNodes.push(otherNode);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let flag = true;
|
|
199
|
+
for(let i = 0; i < relateNodes.length; i++) {
|
|
200
|
+
const item = relateNodes[i];
|
|
201
|
+
// 判断条件调整,节点的坐标不需要完全一致。可以根据节点间的夹角来判断
|
|
202
|
+
const delta = Math.atan((node.y - item.y) / (item.x - node.y)) * 180;
|
|
203
|
+
const newDelta = Math.atan((newPosition.y - item.y) / (item.x - newPosition.y)) * 180;
|
|
204
|
+
const isHor = delta < 30 || delta > 150;
|
|
205
|
+
const newIsHor = newDelta < 30 || newDelta > 150;
|
|
206
|
+
const isVer = delta > 70 && delta < 110;
|
|
207
|
+
const newIsVer = newDelta > 70 && newDelta < 110;
|
|
208
|
+
// 定义四个相似角度区间,0-15度,75-90度,90到105度,165到180度。
|
|
209
|
+
if (isHor && !newIsHor || ((delta * newDelta) < 0)) {
|
|
210
|
+
flag = false;
|
|
211
|
+
break;
|
|
212
|
+
} else if (isVer && !newIsVer || ((delta * newDelta) < 0)) {
|
|
213
|
+
flag = false;
|
|
214
|
+
break;
|
|
215
|
+
} else if ((item.x - node.x) * (item.x - newPosition.x) < 0) {
|
|
216
|
+
flag = false;
|
|
217
|
+
break;
|
|
218
|
+
}else if ((item.y - node.y) * (item.y - newPosition.y) < 0) {
|
|
219
|
+
flag = false;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return flag;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function shuffle(nodes: IMysqlNode[], edges: IEdge[]) {
|
|
227
|
+
let foundSmallerEnergy = false;
|
|
228
|
+
// 多次测试发现step为1时的效果最佳。
|
|
229
|
+
const step = 1;
|
|
230
|
+
const wstep = CELL_W * step;
|
|
231
|
+
const hstep = CELL_H * step;
|
|
232
|
+
const wsteps = [ wstep, -wstep, 0, 0, ];
|
|
233
|
+
const hsteps = [ 0, 0, hstep, -hstep, ];
|
|
234
|
+
for (let i = 0; i < nodes.length; ++i) {
|
|
235
|
+
const node = nodes[i];
|
|
236
|
+
let nodeEnergy = calcNodeEnergy(node, nodes);
|
|
237
|
+
for (let ns = 0; ns < wsteps.length ; ns++) {
|
|
238
|
+
// 判断新位置与其他连线节点的位置关系是否违规
|
|
239
|
+
const flag = isCorrectPosition(node, { x: node.x + wsteps[ns], y: node.y + hsteps[ns] }, nodes, edges);
|
|
240
|
+
if (flag) {
|
|
241
|
+
// 节点朝上下左右四个方向移动,找到能量最小的那个位置
|
|
242
|
+
node.x += wsteps[ns];
|
|
243
|
+
node.y += hsteps[ns];
|
|
244
|
+
|
|
245
|
+
// 计算移动后节点的能量
|
|
246
|
+
const energy = calcNodeEnergy(node, nodes);
|
|
247
|
+
const rdm = Math.random();
|
|
248
|
+
|
|
249
|
+
if (energy < nodeEnergy) {
|
|
250
|
+
nodeEnergy = energy;
|
|
251
|
+
foundSmallerEnergy = true;
|
|
252
|
+
|
|
253
|
+
} else if (rdm < T && rdm > T_MIN) {
|
|
254
|
+
nodeEnergy = energy;
|
|
255
|
+
foundSmallerEnergy = true;
|
|
256
|
+
|
|
257
|
+
} else {
|
|
258
|
+
// 回归原位
|
|
259
|
+
node.x -= wsteps[ns];
|
|
260
|
+
node.y -= hsteps[ns];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
if (T > T_MIN) {
|
|
267
|
+
T *= R;
|
|
268
|
+
}
|
|
269
|
+
// 重新计算图整体的能量
|
|
270
|
+
if (foundSmallerEnergy) {
|
|
271
|
+
return calcEnergy(nodes);
|
|
272
|
+
}
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 计算节点的能量,
|
|
277
|
+
function calcNodeEnergy(node: IMysqlNode, nodes: IMysqlNode[]) {
|
|
278
|
+
let e = 0.0;
|
|
279
|
+
if ((node.x < 0) || (node.y < 0) ||
|
|
280
|
+
(node.x + node.size[0] + 20 > graphWidth) ||
|
|
281
|
+
(node.y + node.size[1] + 20 > graphHeight)
|
|
282
|
+
) {
|
|
283
|
+
e += 1000000000000.0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < nodes.length; ++i) {
|
|
287
|
+
if (node.id !== nodes[i].id) {
|
|
288
|
+
e += calcNodePair(node, nodes[i]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return e;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function layout(nodes: IMysqlNode[], edges: IEdge[]) {
|
|
295
|
+
if (nodes.length === 0) {
|
|
296
|
+
return { nodes, edges };
|
|
297
|
+
}
|
|
298
|
+
nodes.forEach((node: any) => {
|
|
299
|
+
const relateEdge = edges.filter((edge) => edge.source === node.id || edge.target === node.id);
|
|
300
|
+
nodeEdgeMap.set(node, relateEdge);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 1. 初始化
|
|
304
|
+
// 将node按照连接数进行排序
|
|
305
|
+
nodes.sort((node1: IMysqlNode, node2: IMysqlNode) => {
|
|
306
|
+
return nodeEdgeMap.get(node1.id)?.length - nodeEdgeMap.get(node2.id)?.length;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// 2. 计算图能量
|
|
310
|
+
let minEnergy = calcEnergy(nodes);
|
|
311
|
+
let deSameCount = 20; // de=0 count
|
|
312
|
+
let de = 1; // energy delta
|
|
313
|
+
let prevEnergy = 0;
|
|
314
|
+
// 定义总的迭代次数。超过就停掉,防止死循环
|
|
315
|
+
const MAX_COUNT = 50;
|
|
316
|
+
let count = 0;
|
|
317
|
+
while (deSameCount > 0) {
|
|
318
|
+
count ++;
|
|
319
|
+
if (count >= MAX_COUNT) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
const ea = shuffle(nodes, edges);
|
|
323
|
+
if (ea !== 0) {
|
|
324
|
+
prevEnergy = ea;
|
|
325
|
+
}
|
|
326
|
+
de = prevEnergy - minEnergy;
|
|
327
|
+
minEnergy = prevEnergy;
|
|
328
|
+
if (de === 0) {
|
|
329
|
+
--deSameCount;
|
|
330
|
+
} else {
|
|
331
|
+
deSameCount = 20;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
nodes.forEach((node: IMysqlNode) => {
|
|
335
|
+
node.x = node.x - node.size[0] / 2;
|
|
336
|
+
node.y = node.y - node.size[1] / 2;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
nodes,
|
|
341
|
+
edges,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default layout;
|