Skip to content

Commit eecf188

Browse files
committed
New feature - VueUiDag - Port dagrejs into vue-data-ui
1 parent 6975d0d commit eecf188

29 files changed

+3900
-0
lines changed

src/DAG/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
All files in the DAG directory are ported from the superb @dagrejs/dagre library, and adapted to function inside vue-data-ui's environment.

src/DAG/acyclic.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import greedyFAS from "./greedy-fas.js";
2+
import { uniqueId } from "./util.js";
3+
4+
export function run(graph) {
5+
const feedbackArcSet = graph.graph().acyclicer === "greedy"
6+
? greedyFAS(graph, createWeightFunction(graph))
7+
: depthFirstSearchFeedbackArcSet(graph);
8+
9+
feedbackArcSet.forEach(edgeObject => {
10+
const label = graph.edge(edgeObject);
11+
graph.removeEdge(edgeObject);
12+
13+
label.forwardName = edgeObject.name;
14+
label.reversed = true;
15+
16+
graph.setEdge(edgeObject.w, edgeObject.v, label, uniqueId("rev"));
17+
});
18+
19+
function createWeightFunction(innerGraph) {
20+
return edgeObject => {
21+
return innerGraph.edge(edgeObject).weight;
22+
};
23+
}
24+
}
25+
26+
export function undo(graph) {
27+
graph.edges().forEach(edgeObject => {
28+
const label = graph.edge(edgeObject);
29+
if (label.reversed) {
30+
graph.removeEdge(edgeObject);
31+
32+
const forwardName = label.forwardName;
33+
delete label.reversed;
34+
delete label.forwardName;
35+
36+
graph.setEdge(edgeObject.w, edgeObject.v, label, forwardName);
37+
}
38+
});
39+
}
40+
41+
function depthFirstSearchFeedbackArcSet(graph) {
42+
const feedbackArcSet = [];
43+
const stack = {};
44+
const visited = {};
45+
46+
function depthFirstSearch(nodeId) {
47+
if (Object.hasOwn(visited, nodeId)) {
48+
return;
49+
}
50+
51+
visited[nodeId] = true;
52+
stack[nodeId] = true;
53+
54+
graph.outEdges(nodeId).forEach(edgeObject => {
55+
if (Object.hasOwn(stack, edgeObject.w)) {
56+
feedbackArcSet.push(edgeObject);
57+
} else {
58+
depthFirstSearch(edgeObject.w);
59+
}
60+
});
61+
62+
delete stack[nodeId];
63+
}
64+
65+
graph.nodes().forEach(depthFirstSearch);
66+
67+
return feedbackArcSet;
68+
}
69+
70+
export default {
71+
run,
72+
undo,
73+
};

src/DAG/add-border-segments.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { addDummyNode } from "./util.js";
2+
3+
export default function addBorderSegments(graph) {
4+
function depthFirstSearch(nodeId) {
5+
const children = graph.children(nodeId);
6+
const node = graph.node(nodeId);
7+
8+
if (children.length) {
9+
children.forEach(depthFirstSearch);
10+
}
11+
12+
if (Object.hasOwn(node, "minRank")) {
13+
node.borderLeft = [];
14+
node.borderRight = [];
15+
16+
for (let rank = node.minRank, maximumRank = node.maxRank + 1; rank < maximumRank; ++rank) {
17+
addBorderNode(graph, "borderLeft", "_bl", nodeId, node, rank);
18+
addBorderNode(graph, "borderRight", "_br", nodeId, node, rank);
19+
}
20+
}
21+
}
22+
23+
graph.children().forEach(depthFirstSearch);
24+
}
25+
26+
function addBorderNode(graph, propertyName, prefix, subgraphId, subgraphNode, rank) {
27+
const label = {
28+
width: 0,
29+
height: 0,
30+
rank,
31+
borderType: propertyName,
32+
};
33+
34+
const previousNodeId = subgraphNode[propertyName][rank - 1];
35+
const currentNodeId = addDummyNode(graph, "border", label, prefix);
36+
37+
subgraphNode[propertyName][rank] = currentNodeId;
38+
graph.setParent(currentNodeId, subgraphId);
39+
40+
if (previousNodeId) {
41+
graph.setEdge(previousNodeId, currentNodeId, { weight: 1 });
42+
}
43+
}

src/DAG/coordinate-system.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export function adjust(graph) {
2+
const rankDirection = graph.graph().rankdir.toLowerCase();
3+
4+
if (rankDirection === "lr" || rankDirection === "rl") {
5+
swapWidthAndHeight(graph);
6+
}
7+
}
8+
9+
export function undo(graph) {
10+
const rankDirection = graph.graph().rankdir.toLowerCase();
11+
12+
if (rankDirection === "bt" || rankDirection === "rl") {
13+
reverseY(graph);
14+
}
15+
16+
if (rankDirection === "lr" || rankDirection === "rl") {
17+
swapXY(graph);
18+
swapWidthAndHeight(graph);
19+
}
20+
}
21+
22+
function swapWidthAndHeight(graph) {
23+
graph.nodes().forEach(nodeId => {
24+
swapWidthAndHeightOnAttributes(graph.node(nodeId));
25+
});
26+
27+
graph.edges().forEach(edgeObject => {
28+
swapWidthAndHeightOnAttributes(graph.edge(edgeObject));
29+
});
30+
}
31+
32+
function swapWidthAndHeightOnAttributes(attributes) {
33+
const width = attributes.width;
34+
attributes.width = attributes.height;
35+
attributes.height = width;
36+
}
37+
38+
function reverseY(graph) {
39+
graph.nodes().forEach(nodeId => {
40+
reverseYOnAttributes(graph.node(nodeId));
41+
});
42+
43+
graph.edges().forEach(edgeObject => {
44+
const edge = graph.edge(edgeObject);
45+
46+
edge.points.forEach(reverseYOnAttributes);
47+
48+
if (Object.hasOwn(edge, "y")) {
49+
reverseYOnAttributes(edge);
50+
}
51+
});
52+
}
53+
54+
function reverseYOnAttributes(attributes) {
55+
attributes.y = -attributes.y;
56+
}
57+
58+
function swapXY(graph) {
59+
graph.nodes().forEach(nodeId => {
60+
swapXYOnAttributes(graph.node(nodeId));
61+
});
62+
63+
graph.edges().forEach(edgeObject => {
64+
const edge = graph.edge(edgeObject);
65+
66+
edge.points.forEach(swapXYOnAttributes);
67+
68+
if (Object.hasOwn(edge, "x")) {
69+
swapXYOnAttributes(edge);
70+
}
71+
});
72+
}
73+
74+
function swapXYOnAttributes(attributes) {
75+
const x = attributes.x;
76+
attributes.x = attributes.y;
77+
attributes.y = x;
78+
}
79+
80+
export default {
81+
adjust,
82+
undo,
83+
};

src/DAG/dag-layout-entry.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import Graph from "./graph.js";
2+
import layout from "./layout.js";
3+
4+
/**
5+
*
6+
* @param {Object} params
7+
* @param {Array<{ id: string, width: number, height: number, data?: any }>} params.nodes
8+
* @param {Array<{ id?: string, from: string, to: string, minlen?: number, weight?: number, data?: any }>} params.edges
9+
* @param {Object} [params.graph]
10+
* @param {number} [params.graph.ranksep]
11+
* @param {number} [params.graph.nodesep]
12+
* @param {number} [params.graph.edgesep]
13+
* @param {number} [params.graph.marginx]
14+
* @param {number} [params.graph.marginy]
15+
* @param {"TB" | "BT" | "LR" | "RL"} [params.graph.rankdir]
16+
* @param {"network-simplex" | "tight-tree" | "longest-path" | "none"} [params.graph.ranker]
17+
* @param {Object} [params.layoutOptions]
18+
*
19+
* @returns {{
20+
* width: number,
21+
* height: number,
22+
* nodes: Record<string, { x: number, y: number, width: number, height: number, data?: any }>,
23+
* edges: Array<{
24+
* id: string,
25+
* from: string,
26+
* to: string,
27+
* points: Array<{ x: number, y: number }>,
28+
* label?: { x: number, y: number, width: number, height: number },
29+
* data?: any
30+
* }>
31+
* }}
32+
*/
33+
export function computeDagLayout({
34+
nodes,
35+
edges,
36+
graph = {},
37+
layoutOptions = {},
38+
}) {
39+
const dagGraph = new Graph({ multigraph: true, compound: true });
40+
41+
dagGraph.setGraph({
42+
nodesep: graph.nodesep ?? 50,
43+
edgesep: graph.edgesep ?? 20,
44+
ranksep: graph.ranksep ?? 50,
45+
marginx: graph.marginx ?? 20,
46+
marginy: graph.marginy ?? 20,
47+
rankdir: graph.rankdir ?? "TB",
48+
ranker: graph.ranker ?? "network-simplex",
49+
align: graph.align,
50+
acyclicer: graph.acyclicer,
51+
});
52+
53+
nodes.forEach(node => {
54+
dagGraph.setNode(node.id, {
55+
width: node.width,
56+
height: node.height,
57+
...("data" in node ? { data: node.data } : {}),
58+
});
59+
});
60+
61+
edges.forEach(edge => {
62+
const edgeId = edge.id ?? `${edge.from}->${edge.to}`;
63+
dagGraph.setEdge(
64+
{ v: edge.from, w: edge.to, name: edgeId },
65+
{
66+
minlen: edge.minlen ?? 1,
67+
weight: edge.weight ?? 1,
68+
width: 0,
69+
height: 0,
70+
labeloffset: 10,
71+
labelpos: "r",
72+
...("data" in edge ? { data: edge.data } : {}),
73+
},
74+
);
75+
});
76+
77+
layout(dagGraph, layoutOptions);
78+
79+
const graphLabel = dagGraph.graph();
80+
81+
const positionedNodes = {};
82+
dagGraph.nodes().forEach(nodeId => {
83+
const node = dagGraph.node(nodeId);
84+
positionedNodes[nodeId] = {
85+
x: node.x,
86+
y: node.y,
87+
width: node.width,
88+
height: node.height,
89+
...(node.data !== undefined ? { data: node.data } : {}),
90+
};
91+
});
92+
93+
const positionedEdges = dagGraph.edges().map(edgeObj => {
94+
const edgeLabel = dagGraph.edge(edgeObj);
95+
96+
const resultEdge = {
97+
id: edgeObj.name,
98+
from: edgeObj.v,
99+
to: edgeObj.w,
100+
points: edgeLabel.points ?? [],
101+
...(edgeLabel.data !== undefined ? { data: edgeLabel.data } : {}),
102+
};
103+
104+
if (
105+
Object.prototype.hasOwnProperty.call(edgeLabel, "x") &&
106+
Object.prototype.hasOwnProperty.call(edgeLabel, "y")
107+
) {
108+
resultEdge.label = {
109+
x: edgeLabel.x,
110+
y: edgeLabel.y,
111+
width: edgeLabel.width ?? 0,
112+
height: edgeLabel.height ?? 0,
113+
};
114+
}
115+
116+
return resultEdge;
117+
});
118+
119+
return {
120+
width: graphLabel.width,
121+
height: graphLabel.height,
122+
nodes: positionedNodes,
123+
edges: positionedEdges,
124+
};
125+
}
126+
127+
export default computeDagLayout;

src/DAG/data/list.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Simple doubly linked list implementation derived from Cormen, et al.,
3+
* "Introduction to Algorithms".
4+
*/
5+
6+
class List {
7+
constructor() {
8+
const sentinel = {};
9+
sentinel._next = sentinel._prev = sentinel;
10+
this._sentinel = sentinel;
11+
}
12+
13+
dequeue() {
14+
const sentinel = this._sentinel;
15+
const entry = sentinel._prev;
16+
if (entry !== sentinel) {
17+
unlink(entry);
18+
return entry;
19+
}
20+
}
21+
22+
enqueue(entry) {
23+
const sentinel = this._sentinel;
24+
25+
// If entry is already in a list, unlink it first
26+
if (entry._prev && entry._next) {
27+
unlink(entry);
28+
}
29+
30+
entry._next = sentinel._next;
31+
sentinel._next._prev = entry;
32+
33+
sentinel._next = entry;
34+
entry._prev = sentinel;
35+
}
36+
37+
toString() {
38+
const result = [];
39+
const sentinel = this._sentinel;
40+
let curr = sentinel._prev;
41+
42+
while (curr !== sentinel) {
43+
result.push(JSON.stringify(curr, filterOutLinks));
44+
curr = curr._prev;
45+
}
46+
47+
return "[" + result.join(", ") + "]";
48+
}
49+
}
50+
51+
function unlink(entry) {
52+
entry._prev._next = entry._next;
53+
entry._next._prev = entry._prev;
54+
55+
delete entry._next;
56+
delete entry._prev;
57+
}
58+
59+
function filterOutLinks(key, value) {
60+
if (key !== "_next" && key !== "_prev") {
61+
return value;
62+
}
63+
}
64+
65+
export default List;

0 commit comments

Comments
 (0)