@flowgram-vue/free-auto-layout-plugin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +2713 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +377 -0
- package/dist/index.js +2705 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/create-auto-layout-plugin.ts +23 -0
- package/src/dagre-layout/acyclic.ts +129 -0
- package/src/dagre-layout/graph.ts +71 -0
- package/src/dagre-layout/index.ts +9 -0
- package/src/dagre-layout/layout.ts +151 -0
- package/src/dagre-layout/order.ts +261 -0
- package/src/dagre-layout/rank/feasible-tree.ts +102 -0
- package/src/dagre-layout/rank/index.ts +9 -0
- package/src/dagre-layout/rank/longest-path.ts +79 -0
- package/src/dagre-layout/rank/network-simplex.ts +235 -0
- package/src/dagre-layout/rank/normalize-ranks.ts +26 -0
- package/src/dagre-layout/type.ts +43 -0
- package/src/dagre-lib/acyclic.js +72 -0
- package/src/dagre-lib/add-border-segments.js +41 -0
- package/src/dagre-lib/coordinate-system.js +77 -0
- package/src/dagre-lib/data/list.js +63 -0
- package/src/dagre-lib/debug.js +34 -0
- package/src/dagre-lib/greedy-fas.js +134 -0
- package/src/dagre-lib/index.js +75 -0
- package/src/dagre-lib/layout.js +449 -0
- package/src/dagre-lib/nesting-graph.js +133 -0
- package/src/dagre-lib/normalize.js +98 -0
- package/src/dagre-lib/order/add-subgraph-constraints.js +57 -0
- package/src/dagre-lib/order/barycenter.js +34 -0
- package/src/dagre-lib/order/build-layer-graph.js +80 -0
- package/src/dagre-lib/order/cross-count.js +78 -0
- package/src/dagre-lib/order/index.js +86 -0
- package/src/dagre-lib/order/init-order.js +43 -0
- package/src/dagre-lib/order/resolve-conflicts.js +128 -0
- package/src/dagre-lib/order/sort-subgraph.js +79 -0
- package/src/dagre-lib/order/sort.js +62 -0
- package/src/dagre-lib/parent-dummy-chains.js +90 -0
- package/src/dagre-lib/position/bk.js +431 -0
- package/src/dagre-lib/position/index.js +37 -0
- package/src/dagre-lib/rank/feasible-tree.js +104 -0
- package/src/dagre-lib/rank/index.js +61 -0
- package/src/dagre-lib/rank/network-simplex.js +243 -0
- package/src/dagre-lib/rank/util.js +70 -0
- package/src/dagre-lib/util.js +365 -0
- package/src/dagre-lib/version.js +6 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +10 -0
- package/src/layout/constant.ts +26 -0
- package/src/layout/dagre.ts +245 -0
- package/src/layout/index.ts +9 -0
- package/src/layout/layout.ts +41 -0
- package/src/layout/position.ts +72 -0
- package/src/layout/store.ts +262 -0
- package/src/layout/type.ts +162 -0
- package/src/services.ts +174 -0
- package/src/type.ts +10 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutGraph } from './graph';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DFS去环算法
|
|
10
|
+
* @param graph 布局图实例
|
|
11
|
+
* @returns 反馈弧集(需要反转的边的ID数组)
|
|
12
|
+
*/
|
|
13
|
+
const dfsFAS = (graph: LayoutGraph): string[] => {
|
|
14
|
+
const visited: { [key: string]: boolean } = {};
|
|
15
|
+
const stack: { [key: string]: boolean } = {};
|
|
16
|
+
const fas: string[] = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DFS遍历
|
|
20
|
+
* @param nodeId 当前节点ID
|
|
21
|
+
*/
|
|
22
|
+
const dfs = (nodeId: string): void => {
|
|
23
|
+
visited[nodeId] = true;
|
|
24
|
+
stack[nodeId] = true;
|
|
25
|
+
|
|
26
|
+
const outEdges = graph.edges.filter((edge) => edge.from === nodeId);
|
|
27
|
+
outEdges.forEach((edge) => {
|
|
28
|
+
if (!visited[edge.to]) {
|
|
29
|
+
dfs(edge.to);
|
|
30
|
+
} else if (stack[edge.to]) {
|
|
31
|
+
// 发现环,将该边添加到反馈弧集
|
|
32
|
+
fas.push(edge.id);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
stack[nodeId] = false;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 对每个未访问的节点进行DFS
|
|
40
|
+
graph.nodes.forEach((node) => {
|
|
41
|
+
if (!visited[node.id]) {
|
|
42
|
+
dfs(node.id);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return fas;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 贪心去环算法
|
|
51
|
+
* @param graph 布局图实例
|
|
52
|
+
* @returns 反馈弧集(需要反转的边的ID数组)
|
|
53
|
+
*/
|
|
54
|
+
const greedyFAS = (graph: LayoutGraph): string[] => {
|
|
55
|
+
const fas: string[] = [];
|
|
56
|
+
const nodeOrder: string[] = [];
|
|
57
|
+
|
|
58
|
+
// 计算节点的入度和出度
|
|
59
|
+
const inDegree: { [key: string]: number } = {};
|
|
60
|
+
const outDegree: { [key: string]: number } = {};
|
|
61
|
+
|
|
62
|
+
graph.nodes.forEach((node) => {
|
|
63
|
+
inDegree[node.id] = 0;
|
|
64
|
+
outDegree[node.id] = 0;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
graph.edges.forEach((edge) => {
|
|
68
|
+
inDegree[edge.to]++;
|
|
69
|
+
outDegree[edge.from]++;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 贪心选择节点
|
|
73
|
+
while (nodeOrder.length < graph.nodes.length) {
|
|
74
|
+
let maxDiff = -Infinity;
|
|
75
|
+
let bestNode: string | null = null;
|
|
76
|
+
|
|
77
|
+
graph.nodes.forEach((node) => {
|
|
78
|
+
if (!nodeOrder.includes(node.id)) {
|
|
79
|
+
const diff = outDegree[node.id] - inDegree[node.id];
|
|
80
|
+
if (diff > maxDiff) {
|
|
81
|
+
maxDiff = diff;
|
|
82
|
+
bestNode = node.id;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (bestNode) {
|
|
88
|
+
nodeOrder.push(bestNode);
|
|
89
|
+
// 更新相邻节点的入度和出度
|
|
90
|
+
graph.edges.forEach((edge) => {
|
|
91
|
+
if (edge.from === bestNode) {
|
|
92
|
+
inDegree[edge.to]--;
|
|
93
|
+
}
|
|
94
|
+
if (edge.to === bestNode) {
|
|
95
|
+
outDegree[edge.from]--;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 根据节点顺序确定需要反转的边
|
|
102
|
+
graph.edges.forEach((edge) => {
|
|
103
|
+
if (nodeOrder.indexOf(edge.from) > nodeOrder.indexOf(edge.to)) {
|
|
104
|
+
fas.push(edge.id);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return fas;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 去环
|
|
113
|
+
*/
|
|
114
|
+
export const acyclic = (graph: LayoutGraph, acyclicer: 'dfs' | 'greedy' = 'dfs'): LayoutGraph => {
|
|
115
|
+
// 使用DFS或贪心算法获取反馈弧集
|
|
116
|
+
const fas = acyclicer === 'dfs' ? dfsFAS(graph) : greedyFAS(graph);
|
|
117
|
+
|
|
118
|
+
// 反转反馈弧集中的边
|
|
119
|
+
fas.forEach((edgeId) => {
|
|
120
|
+
const edge = graph.edges.find((e) => e.id === edgeId);
|
|
121
|
+
if (edge) {
|
|
122
|
+
const { from, to } = edge;
|
|
123
|
+
graph.removeEdge(edgeId);
|
|
124
|
+
graph.addLayoutEdge({ id: edgeId, from: to, to: from });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return graph;
|
|
129
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram-vue/free-layout-core';
|
|
7
|
+
import { TransformData } from '@flowgram-vue/core';
|
|
8
|
+
|
|
9
|
+
import type { ILayoutGraph, LayoutEdge, LayoutNode } from './type';
|
|
10
|
+
|
|
11
|
+
export class LayoutGraph implements ILayoutGraph {
|
|
12
|
+
public readonly store: {
|
|
13
|
+
nodes: Map<string, LayoutNode>;
|
|
14
|
+
edges: Map<string, LayoutEdge>;
|
|
15
|
+
} = {
|
|
16
|
+
nodes: new Map(),
|
|
17
|
+
edges: new Map(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
public get nodes(): LayoutNode[] {
|
|
21
|
+
return Array.from(this.store.nodes.values());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public get edges(): LayoutEdge[] {
|
|
25
|
+
return Array.from(this.store.edges.values());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public getNode(id: string): LayoutNode | undefined {
|
|
29
|
+
return this.store.nodes.get(id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public hasNode(id: string): boolean {
|
|
33
|
+
return this.store.nodes.has(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public addNode(nodeEntity: WorkflowNodeEntity): LayoutNode {
|
|
37
|
+
const transform = nodeEntity.getData(TransformData);
|
|
38
|
+
const layoutNode: LayoutNode = {
|
|
39
|
+
id: nodeEntity.id,
|
|
40
|
+
node: nodeEntity,
|
|
41
|
+
rank: -1,
|
|
42
|
+
order: -1,
|
|
43
|
+
position: { x: transform.position.x, y: transform.position.y },
|
|
44
|
+
size: { width: transform.bounds.width, height: transform.bounds.height },
|
|
45
|
+
};
|
|
46
|
+
this.store.nodes.set(layoutNode.id, layoutNode);
|
|
47
|
+
return layoutNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public addLayoutNode(layoutNode: LayoutNode): void {
|
|
51
|
+
this.store.nodes.set(layoutNode.id, layoutNode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public addEdge(edgeEntity: WorkflowLineEntity): LayoutEdge {
|
|
55
|
+
const layoutEdge: LayoutEdge = {
|
|
56
|
+
id: edgeEntity.id,
|
|
57
|
+
from: edgeEntity.from!.id,
|
|
58
|
+
to: edgeEntity.to!.id,
|
|
59
|
+
};
|
|
60
|
+
this.store.edges.set(layoutEdge.id, layoutEdge);
|
|
61
|
+
return layoutEdge;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public addLayoutEdge(layoutEdge: LayoutEdge): void {
|
|
65
|
+
this.store.edges.set(layoutEdge.id, layoutEdge);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public removeEdge(id: string): void {
|
|
69
|
+
this.store.edges.delete(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
WorkflowLineEntity,
|
|
8
|
+
WorkflowNodeEntity,
|
|
9
|
+
WorkflowNodeLinesData,
|
|
10
|
+
} from '@flowgram-vue/free-layout-core';
|
|
11
|
+
import { TransformData } from '@flowgram-vue/core';
|
|
12
|
+
|
|
13
|
+
import { LayoutNode } from './type';
|
|
14
|
+
import { LayoutGraph } from './graph';
|
|
15
|
+
import { acyclic, feasibleTree, longestPath, networkSimplex, normalizeRanks, order } from './';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 布局算法
|
|
19
|
+
* 参考 dagre.js 的实现 https://github.com/dagrejs/dagre
|
|
20
|
+
*/
|
|
21
|
+
export namespace DagreLayout {
|
|
22
|
+
const getNextEdges = (node: WorkflowNodeEntity): WorkflowLineEntity[] => {
|
|
23
|
+
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
|
24
|
+
return linesData.outputLines.filter((line) => line.from && line.to);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getPrevEdges = (node: WorkflowNodeEntity): WorkflowLineEntity[] => {
|
|
28
|
+
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
|
29
|
+
return linesData.inputLines.filter((line) => line.from && line.to);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 添加节点 */
|
|
33
|
+
const createData = (params: {
|
|
34
|
+
node: WorkflowNodeEntity;
|
|
35
|
+
depth: number;
|
|
36
|
+
graph: LayoutGraph;
|
|
37
|
+
}): LayoutGraph => {
|
|
38
|
+
const { node, depth, graph } = params;
|
|
39
|
+
if (graph.hasNode(node.id)) {
|
|
40
|
+
return graph;
|
|
41
|
+
}
|
|
42
|
+
graph.addNode(node);
|
|
43
|
+
const prevEdges = getPrevEdges(node);
|
|
44
|
+
const nextEdges = getNextEdges(node);
|
|
45
|
+
prevEdges.forEach((prevEdge) => {
|
|
46
|
+
graph.addEdge(prevEdge);
|
|
47
|
+
createData({ node: prevEdge.from!, depth: depth - 1, graph });
|
|
48
|
+
});
|
|
49
|
+
nextEdges.forEach((nextEdge) => {
|
|
50
|
+
graph.addEdge(nextEdge);
|
|
51
|
+
createData({ node: nextEdge.to!, depth: depth + 1, graph });
|
|
52
|
+
});
|
|
53
|
+
return graph;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 定义一些常量
|
|
57
|
+
const NODE_SPACING = 100; // 同层级节点之间的垂直间距
|
|
58
|
+
const RANK_SPACING = 100; // 层级之间的水平间距
|
|
59
|
+
|
|
60
|
+
/** 计算图中所有节点的坐标 */
|
|
61
|
+
const calcCoordinates = (graph: LayoutGraph): LayoutGraph => {
|
|
62
|
+
// 按rank对节点进行分组
|
|
63
|
+
const rankGroups = groupNodesByRank(graph.nodes);
|
|
64
|
+
|
|
65
|
+
// 计算每个rank的最大高度
|
|
66
|
+
const rankHeights = calculateRankHeights(rankGroups);
|
|
67
|
+
|
|
68
|
+
// 计算每个节点的坐标
|
|
69
|
+
let currentX = 0;
|
|
70
|
+
rankGroups.forEach((nodesInRank, rank) => {
|
|
71
|
+
const rankHeight = rankHeights[rank];
|
|
72
|
+
|
|
73
|
+
nodesInRank.forEach((node) => {
|
|
74
|
+
// 计算X坐标
|
|
75
|
+
node.position.x = currentX + node.size.width / 2;
|
|
76
|
+
|
|
77
|
+
// 计算Y坐标
|
|
78
|
+
const totalHeightOfRank = nodesInRank.reduce((sum, n) => sum + n.size.height, 0);
|
|
79
|
+
const totalSpacing = (nodesInRank.length - 1) * NODE_SPACING;
|
|
80
|
+
const startY = (rankHeight - totalHeightOfRank - totalSpacing) / 2;
|
|
81
|
+
|
|
82
|
+
let currentY = startY;
|
|
83
|
+
for (let i = 0; i < node.order; i++) {
|
|
84
|
+
currentY += nodesInRank[i].size.height + NODE_SPACING;
|
|
85
|
+
}
|
|
86
|
+
node.position.y = currentY + node.size.height / 2;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 更新X坐标为下一个rank的起始位置
|
|
90
|
+
currentX += rankHeight + RANK_SPACING;
|
|
91
|
+
});
|
|
92
|
+
return graph;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** 按rank对节点进行分组 */
|
|
96
|
+
const groupNodesByRank = (nodes: LayoutNode[]): LayoutNode[][] => {
|
|
97
|
+
const groups: LayoutNode[][] = [];
|
|
98
|
+
nodes.forEach((node) => {
|
|
99
|
+
if (!groups[node.rank]) {
|
|
100
|
+
groups[node.rank] = [];
|
|
101
|
+
}
|
|
102
|
+
groups[node.rank].push(node);
|
|
103
|
+
});
|
|
104
|
+
return groups;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** 计算每个rank的最大高度 */
|
|
108
|
+
const calculateRankHeights = (rankGroups: LayoutNode[][]): number[] =>
|
|
109
|
+
rankGroups.map((nodesInRank) => Math.max(...nodesInRank.map((node) => node.size.width)));
|
|
110
|
+
|
|
111
|
+
const positioning = (graph: LayoutGraph): LayoutGraph => {
|
|
112
|
+
graph.nodes.forEach((node) => {
|
|
113
|
+
const transform = node.node.getData(TransformData);
|
|
114
|
+
transform.update({
|
|
115
|
+
position: node.position,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return graph;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const rank = (
|
|
122
|
+
graph: LayoutGraph,
|
|
123
|
+
ranker: 'longest-path' | 'network-simplex' | 'tight-tree' = 'network-simplex'
|
|
124
|
+
): LayoutGraph => {
|
|
125
|
+
if (ranker === 'longest-path') {
|
|
126
|
+
longestPath(graph);
|
|
127
|
+
} else if (ranker === 'network-simplex') {
|
|
128
|
+
networkSimplex(graph);
|
|
129
|
+
} else if (ranker === 'tight-tree') {
|
|
130
|
+
feasibleTree(graph);
|
|
131
|
+
}
|
|
132
|
+
return graph;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const applyLayout = (graph: LayoutGraph): void => {
|
|
136
|
+
acyclic(graph); // 去环
|
|
137
|
+
rank(graph); // 分层
|
|
138
|
+
normalizeRanks(graph); // 归一化 rank 值
|
|
139
|
+
order(graph); // 重心法对同层级节点进行排序
|
|
140
|
+
calcCoordinates(graph); // 分配坐标
|
|
141
|
+
positioning(graph); // 应用布局
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** 创建布局图 */
|
|
145
|
+
export const createGraph = (node: WorkflowNodeEntity): LayoutGraph => {
|
|
146
|
+
const graph = new LayoutGraph();
|
|
147
|
+
createData({ node, depth: 0, graph });
|
|
148
|
+
applyLayout(graph);
|
|
149
|
+
return graph;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutGraph } from './graph';
|
|
7
|
+
|
|
8
|
+
// 辅助函数:获取图中的最大rank
|
|
9
|
+
const getMaxRank = (graph: LayoutGraph): number =>
|
|
10
|
+
Math.max(...graph.nodes.map((node) => node.rank));
|
|
11
|
+
|
|
12
|
+
// 辅助函数:根据rank构建层级图
|
|
13
|
+
const buildLayerGraph = (
|
|
14
|
+
graph: LayoutGraph,
|
|
15
|
+
rank: number,
|
|
16
|
+
edgeType: 'inEdges' | 'outEdges'
|
|
17
|
+
): LayoutGraph => {
|
|
18
|
+
const layerGraph = new LayoutGraph();
|
|
19
|
+
|
|
20
|
+
graph.nodes
|
|
21
|
+
.filter((node) => node.rank === rank)
|
|
22
|
+
.forEach((node) => {
|
|
23
|
+
layerGraph.addLayoutNode(node);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
graph.edges.forEach((edge) => {
|
|
27
|
+
const sourceNode = graph.getNode(edge.from);
|
|
28
|
+
const targetNode = graph.getNode(edge.to);
|
|
29
|
+
if (!sourceNode || !targetNode) return;
|
|
30
|
+
|
|
31
|
+
if (edgeType === 'inEdges' && targetNode.rank === rank) {
|
|
32
|
+
layerGraph.addLayoutEdge(edge);
|
|
33
|
+
} else if (edgeType === 'outEdges' && sourceNode.rank === rank) {
|
|
34
|
+
layerGraph.addLayoutEdge(edge);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return layerGraph;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 辅助函数:初始化order
|
|
42
|
+
const initOrder = (graph: LayoutGraph): { [key: number]: string[] } => {
|
|
43
|
+
const layering: { [key: number]: string[] } = {};
|
|
44
|
+
graph.nodes.forEach((node) => {
|
|
45
|
+
if (!layering[node.rank]) {
|
|
46
|
+
layering[node.rank] = [];
|
|
47
|
+
}
|
|
48
|
+
layering[node.rank].push(node.id);
|
|
49
|
+
});
|
|
50
|
+
return layering;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 辅助函数:分配order
|
|
54
|
+
const assignOrder = (graph: LayoutGraph, layering: { [key: number]: string[] }): void => {
|
|
55
|
+
Object.entries(layering).forEach(([rank, layer]) => {
|
|
56
|
+
layer.forEach((nodeId, index) => {
|
|
57
|
+
const node = graph.getNode(nodeId);
|
|
58
|
+
if (node) {
|
|
59
|
+
node.order = index;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 辅助函数:计算交叉数
|
|
66
|
+
const crossCount = (graph: LayoutGraph, layering: { [key: number]: string[] }): number => {
|
|
67
|
+
let cc = 0;
|
|
68
|
+
const layers = Object.values(layering);
|
|
69
|
+
|
|
70
|
+
for (let i = 1; i < layers.length; i++) {
|
|
71
|
+
const northLayer = layers[i - 1];
|
|
72
|
+
const southLayer = layers[i];
|
|
73
|
+
|
|
74
|
+
for (let j = 0; j < northLayer.length; j++) {
|
|
75
|
+
for (let k = j + 1; k < northLayer.length; k++) {
|
|
76
|
+
const v = graph.getNode(northLayer[j]);
|
|
77
|
+
const w = graph.getNode(northLayer[k]);
|
|
78
|
+
if (!v || !w) continue;
|
|
79
|
+
|
|
80
|
+
// 获取v和w的南向邻居
|
|
81
|
+
const vNeighbors = graph.edges
|
|
82
|
+
.filter((e) => e.from === v.id)
|
|
83
|
+
.map((e) => graph.getNode(e.to));
|
|
84
|
+
const wNeighbors = graph.edges
|
|
85
|
+
.filter((e) => e.from === w.id)
|
|
86
|
+
.map((e) => graph.getNode(e.to));
|
|
87
|
+
|
|
88
|
+
for (const vNeighbor of vNeighbors) {
|
|
89
|
+
for (const wNeighbor of wNeighbors) {
|
|
90
|
+
if (!vNeighbor || !wNeighbor) continue;
|
|
91
|
+
if (southLayer.indexOf(vNeighbor.id) > southLayer.indexOf(wNeighbor.id)) {
|
|
92
|
+
cc++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return cc;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 辅助函数:构建复合图
|
|
104
|
+
const buildCompoundGraph = (): LayoutGraph => new LayoutGraph();
|
|
105
|
+
|
|
106
|
+
// 辅助函数:添加子图约束
|
|
107
|
+
const addSubgraphConstraints = (layerGraph: LayoutGraph, cg: LayoutGraph, vs: string[]): void => {
|
|
108
|
+
const prev: { [key: string]: string } = {};
|
|
109
|
+
let root = layerGraph.nodes[0]?.id;
|
|
110
|
+
vs.forEach((v) => {
|
|
111
|
+
let prevV = prev[root];
|
|
112
|
+
if (prevV) {
|
|
113
|
+
cg.addLayoutEdge({ id: `${prevV}-${v}`, from: prevV, to: v, weight: 0 });
|
|
114
|
+
}
|
|
115
|
+
prev[root] = v;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 辅助函数:对子图进行排序
|
|
120
|
+
const sortSubgraph = (
|
|
121
|
+
layerGraph: LayoutGraph,
|
|
122
|
+
root: string,
|
|
123
|
+
cg: LayoutGraph,
|
|
124
|
+
biasRight: boolean
|
|
125
|
+
): { vs: string[] } => {
|
|
126
|
+
const vs: string[] = [];
|
|
127
|
+
const visited = new Set<string>();
|
|
128
|
+
const nodeData = new Map<string, { barycenter: number; weight: number }>();
|
|
129
|
+
|
|
130
|
+
const dfs = (v: string) => {
|
|
131
|
+
if (visited.has(v)) return;
|
|
132
|
+
visited.add(v);
|
|
133
|
+
|
|
134
|
+
let barycenter = 0;
|
|
135
|
+
let weight = 0;
|
|
136
|
+
|
|
137
|
+
const node = layerGraph.getNode(v);
|
|
138
|
+
if (node) {
|
|
139
|
+
const edges = biasRight
|
|
140
|
+
? layerGraph.edges.filter((e) => e.to === v)
|
|
141
|
+
: layerGraph.edges.filter((e) => e.from === v);
|
|
142
|
+
|
|
143
|
+
edges.forEach((edge) => {
|
|
144
|
+
const w = biasRight ? edge.from : edge.to;
|
|
145
|
+
const otherNode = layerGraph.getNode(w);
|
|
146
|
+
if (otherNode) {
|
|
147
|
+
const edgeWeight = edge.weight || 1;
|
|
148
|
+
weight += edgeWeight;
|
|
149
|
+
barycenter += (otherNode.order || 0) * edgeWeight;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (weight > 0) {
|
|
154
|
+
barycenter /= weight;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
nodeData.set(v, { barycenter, weight });
|
|
159
|
+
vs.push(v);
|
|
160
|
+
|
|
161
|
+
const neighbors = layerGraph.edges
|
|
162
|
+
.filter((e) => e.from === v || e.to === v)
|
|
163
|
+
.map((e) => (e.from === v ? e.to : e.from));
|
|
164
|
+
neighbors.sort((a, b) => {
|
|
165
|
+
const nodeA = layerGraph.getNode(a);
|
|
166
|
+
const nodeB = layerGraph.getNode(b);
|
|
167
|
+
return (nodeA?.order || 0) - (nodeB?.order || 0);
|
|
168
|
+
});
|
|
169
|
+
neighbors.forEach(dfs);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
dfs(root);
|
|
173
|
+
|
|
174
|
+
// 根据重心值和权重排序
|
|
175
|
+
vs.sort((a, b) => {
|
|
176
|
+
const aData = nodeData.get(a);
|
|
177
|
+
const bData = nodeData.get(b);
|
|
178
|
+
if (aData && bData) {
|
|
179
|
+
if (Math.abs(aData.barycenter - bData.barycenter) < 0.001) {
|
|
180
|
+
return bData.weight - aData.weight;
|
|
181
|
+
}
|
|
182
|
+
return aData.barycenter - bData.barycenter;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return { vs };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// 新增:局部搜索优化
|
|
191
|
+
const localSearch = (graph: LayoutGraph, layering: { [key: number]: string[] }): void => {
|
|
192
|
+
const ranks = Object.keys(layering).map(Number);
|
|
193
|
+
ranks.forEach((rank) => {
|
|
194
|
+
const layer = layering[rank];
|
|
195
|
+
for (let i = 0; i < layer.length - 1; i++) {
|
|
196
|
+
for (let j = i + 1; j < layer.length; j++) {
|
|
197
|
+
const currentCC = crossCount(graph, layering);
|
|
198
|
+
// 交换两个节点的位置
|
|
199
|
+
[layer[i], layer[j]] = [layer[j], layer[i]];
|
|
200
|
+
const newCC = crossCount(graph, layering);
|
|
201
|
+
// 如果交叉数增加,则恢复交换
|
|
202
|
+
if (newCC > currentCC) {
|
|
203
|
+
[layer[i], layer[j]] = [layer[j], layer[i]];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// 优化:sweepLayerGraphs 函数
|
|
211
|
+
const sweepLayerGraphs = (layerGraphs: LayoutGraph[], biasRight: boolean): void => {
|
|
212
|
+
const cg = buildCompoundGraph();
|
|
213
|
+
layerGraphs.forEach((lg) => {
|
|
214
|
+
const root = lg.nodes[0]?.id;
|
|
215
|
+
if (root) {
|
|
216
|
+
const sorted = sortSubgraph(lg, root, cg, biasRight);
|
|
217
|
+
sorted.vs.forEach((v, i) => {
|
|
218
|
+
const node = lg.getNode(v);
|
|
219
|
+
if (node) {
|
|
220
|
+
node.order = i;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
addSubgraphConstraints(lg, cg, sorted.vs);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// 更新主函数 order
|
|
229
|
+
export const order = (graph: LayoutGraph): LayoutGraph => {
|
|
230
|
+
const maxRank = getMaxRank(graph);
|
|
231
|
+
const downLayerGraphs = Array.from({ length: maxRank + 1 }, (_, i) =>
|
|
232
|
+
buildLayerGraph(graph, i, 'inEdges')
|
|
233
|
+
);
|
|
234
|
+
const upLayerGraphs = Array.from({ length: maxRank + 1 }, (_, i) =>
|
|
235
|
+
buildLayerGraph(graph, maxRank - i, 'outEdges')
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
let layering = initOrder(graph);
|
|
239
|
+
assignOrder(graph, layering);
|
|
240
|
+
|
|
241
|
+
let bestCC = Number.POSITIVE_INFINITY;
|
|
242
|
+
let bestLayering = layering;
|
|
243
|
+
|
|
244
|
+
// 增加迭代次数
|
|
245
|
+
for (let i = 0, lastBest = 0; lastBest < 8; ++i, ++lastBest) {
|
|
246
|
+
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2);
|
|
247
|
+
|
|
248
|
+
layering = initOrder(graph);
|
|
249
|
+
localSearch(graph, layering); // 应用局部搜索
|
|
250
|
+
const cc = crossCount(graph, layering);
|
|
251
|
+
if (cc < bestCC) {
|
|
252
|
+
lastBest = 0;
|
|
253
|
+
bestLayering = JSON.parse(JSON.stringify(layering));
|
|
254
|
+
bestCC = cc;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
assignOrder(graph, bestLayering);
|
|
259
|
+
|
|
260
|
+
return graph;
|
|
261
|
+
};
|