Skip to content

Commit 5f380cc

Browse files
authored
Merge pull request #512 from MatsSchrader/dynamic-graph-support
Dynamic graph support
2 parents a3383cb + 33a5c83 commit 5f380cc

10 files changed

Lines changed: 226 additions & 36 deletions

File tree

lib/acyclic.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import type {Edge, EdgeLabel, GraphLabel, NodeLabel, WeightFunction} from "./typ
55

66
export {run, undo};
77

8-
function run(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>): void {
8+
function run(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>, oldGraph: Graph| null): void {
99
const fas = (graph.graph().acyclicer === "greedy"
1010
? greedyFAS(graph, weightFn(graph))
11-
: dfsFAS(graph));
11+
: dfsFAS(graph, oldGraph));
1212
fas.forEach(e => {
1313
const label = graph.edge(e)!;
1414
graph.removeEdge(e);
@@ -24,7 +24,7 @@ function run(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>): void {
2424
}
2525
}
2626

27-
function dfsFAS(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>): Edge[] {
27+
function dfsFAS(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>, oldGraph: Graph|null): Edge[] {
2828
const fas: Edge[] = [];
2929
const stack: { [key: string]: boolean } = {};
3030
const visited: { [key: string]: boolean } = {};
@@ -45,7 +45,31 @@ function dfsFAS(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>): Edge[] {
4545
delete stack[v];
4646
}
4747

48-
graph.nodes().forEach(dfs);
48+
function dfsDynamic(v: string) {
49+
if (Object.hasOwn(visited, v)) {
50+
return;
51+
}
52+
visited[v] = true;
53+
stack[v] = true;
54+
graph.outEdges(v)?.forEach(e => {
55+
if (Object.hasOwn(stack, e.w) ||
56+
(oldGraph!.node(v)?.rank > oldGraph!.node(e.w)?.rank) && isReachedFromStartWithoutEdge(graph, e.w, e)) {
57+
fas.push(e);
58+
} else {
59+
dfsDynamic(e.w);
60+
}
61+
});
62+
delete stack[v];
63+
}
64+
65+
let dfsFn = dfs;
66+
if (oldGraph && typeof oldGraph.node === 'function') {
67+
dfsFn = dfsDynamic;
68+
}
69+
70+
graph.sources().forEach(dfsFn);
71+
graph.nodes().forEach(dfsFn);
72+
4973
return fas;
5074
}
5175

@@ -62,3 +86,27 @@ function undo(graph: Graph<GraphLabel, NodeLabel, EdgeLabel>): void {
6286
}
6387
});
6488
}
89+
90+
function isReachedFromStartWithoutEdge(graph: Graph, node: string, ignoreEdge: Edge) {
91+
// reverse dfs from the given node to 'Start' without using edge e
92+
const localVisited = new Set<string>();
93+
94+
function reverseDfs(v: string): boolean {
95+
if (graph.sources().includes(v)) {
96+
return true;
97+
}
98+
localVisited.add(v);
99+
for (const e of graph.inEdges(v) ?? []) {
100+
// do not use ignored edge
101+
// do not visit the same node twice
102+
if (!(e.v === ignoreEdge.v && e.w === ignoreEdge.w)
103+
&& !localVisited.has(e.v)) {
104+
if (reverseDfs(e.v)) {
105+
return true;
106+
}
107+
}
108+
}
109+
return false;
110+
}
111+
return reverseDfs(node);
112+
}

lib/layout.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as coordinateSystem from "./coordinate-system";
1010
import order from "./order";
1111
import {position} from "./position";
1212
import {Graph} from "./graph-lib";
13-
import type {Edge, EdgeLabel, GraphLabel, LayoutOptions, NodeLabel, Point} from "./types";
13+
import type {Edge, EdgeLabel, GraphLabel, LayoutOptions, NodeCollection, NodeLabel, Point} from "./types";
1414

1515
interface SelfEdge {
1616
e: Edge;
@@ -34,6 +34,8 @@ interface EdgeProxyNodeLabel extends Omit<NodeLabel, 'e'> {
3434
e: Edge;
3535
}
3636

37+
let _oldGraph: Graph<GraphLabel, NodeLabel, EdgeLabel> | null = null;
38+
let _rawOldNodes: NodeCollection = null;
3739

3840
export function layout(g: Graph<GraphLabel, NodeLabel, EdgeLabel>, opts: LayoutOptions = {}): Graph<GraphLabel, NodeLabel, EdgeLabel> {
3941
recursiveClusterLayout(g, util.notime, opts);
@@ -317,9 +319,13 @@ function runLayout(
317319
time: <T>(name: string, fn: () => T) => T,
318320
opts: LayoutOptions
319321
): void {
322+
if (opts?.useDynamic === false) {
323+
_oldGraph = null;
324+
_rawOldNodes = null;
325+
}
320326
time(" makeSpaceForEdgeLabels", () => makeSpaceForEdgeLabels(g));
321327
time(" removeSelfEdges", () => removeSelfEdges(g));
322-
time(" acyclic", () => acyclic.run(g));
328+
time(" acyclic", () => acyclic.run(g, _oldGraph));
323329
time(" nestingGraph.run", () => nestingGraph.run(g));
324330
time(" rank", () => rank(util.asNonCompoundGraph(g)));
325331
time(" injectEdgeLabelProxies", () => injectEdgeLabelProxies(g));
@@ -331,11 +337,12 @@ function runLayout(
331337
time(" normalize.run", () => normalize.run(g));
332338
time(" parentDummyChains", () => parentDummyChains(g));
333339
time(" addBorderSegments", () => addBorderSegments(g));
334-
time(" order", () => order(g, opts));
340+
time(" order", () => order(g, opts, _rawOldNodes));
335341
time(" insertSelfEdges", () => insertSelfEdges(g));
336342
time(" adjustCoordinateSystem", () => coordinateSystem.adjust(g));
337-
time(" position", () => position(g));
343+
time(" position", () => position(g, opts.corePath));
338344
time(" positionSelfEdges", () => positionSelfEdges(g));
345+
_rawOldNodes = JSON.parse(JSON.stringify(g._nodes));
339346
time(" removeBorderNodes", () => removeBorderNodes(g));
340347
time(" normalize.undo", () => normalize.undo(g));
341348
time(" fixupEdgeLabelCoords", () => fixupEdgeLabelCoords(g));
@@ -344,6 +351,7 @@ function runLayout(
344351
time(" assignNodeIntersects", () => assignNodeIntersects(g));
345352
time(" reversePoints", () => reversePointsForReversedEdges(g));
346353
time(" acyclic.undo", () => acyclic.undo(g));
354+
_oldGraph = g;
347355
}
348356

349357
/*

lib/order/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import buildLayerGraph from "./build-layer-graph";
77
import addSubgraphConstraints from "./add-subgraph-constraints";
88
import {Graph} from "../graph-lib";
99
import * as util from "../util";
10-
import type {Graph as GraphType, OrderConstraint} from '../types';
10+
import type {Graph as GraphType, NodeCollection, OrderConstraint} from '../types';
1111

1212
interface OrderOptions {
13-
customOrder?: (graph: GraphType, order: (g: GraphType, opts: OrderOptions) => void) => void;
13+
customOrder?: (graph: GraphType, order: (g: GraphType, opts: OrderOptions, oldNodes: NodeCollection) => void) => void;
1414
disableOptimalOrderHeuristic?: boolean;
1515
constraints?: OrderConstraint[];
1616
}
@@ -30,7 +30,7 @@ interface OrderOptions {
3030
* 1. Graph nodes will have an "order" attribute based on the results of the
3131
* algorithm.
3232
*/
33-
export default function order(graph: GraphType, opts: OrderOptions = {}): void {
33+
export default function order(graph: GraphType, opts: OrderOptions = {}, oldNodes: NodeCollection): void {
3434
if (typeof opts.customOrder === 'function') {
3535
opts.customOrder(graph, order);
3636
return;
@@ -40,7 +40,7 @@ export default function order(graph: GraphType, opts: OrderOptions = {}): void {
4040
const downLayerGraphs = buildLayerGraphs(graph, util.range(1, maxRank + 1), "inEdges");
4141
const upLayerGraphs = buildLayerGraphs(graph, util.range(maxRank - 1, -1, -1), "outEdges");
4242

43-
let layering = initOrder(graph);
43+
let layering = initOrder(graph, oldNodes);
4444
assignOrder(graph, layering);
4545

4646
if (opts.disableOptimalOrderHeuristic) {
@@ -52,7 +52,7 @@ export default function order(graph: GraphType, opts: OrderOptions = {}): void {
5252

5353
const constraints = opts.constraints || [];
5454
for (let i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) {
55-
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2, constraints);
55+
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2, constraints, oldNodes);
5656

5757
layering = util.buildLayerMatrix(graph);
5858
const cc = crossCount(graph, layering);
@@ -104,13 +104,17 @@ function buildLayerGraphs(graph: GraphType, ranks: number[], relationship: "inEd
104104
});
105105
}
106106

107-
function sweepLayerGraphs(layerGraphs: GraphType[], biasRight: boolean, constraints: OrderConstraint[]): void {
107+
function sweepLayerGraphs(layerGraphs: GraphType[], switchBias: boolean, constraints: OrderConstraint[], oldNodes: NodeCollection): void {
108+
let biasRight: boolean = true;
108109
const cg = new Graph() as GraphType;
109110
layerGraphs.forEach(function (lg) {
110111
constraints.forEach(con => cg.setEdge(con.left, con.right));
111112

112113
const root = (lg.graph() as { root: string }).root;
113-
const sorted = sortSubgraph(lg, root, cg, biasRight);
114+
const {result: sorted, usedBias} = sortSubgraph(lg, root, cg, oldNodes, biasRight);
115+
if (switchBias && usedBias) {
116+
biasRight = !biasRight;
117+
}
114118
sorted.vs.forEach((v, i) => lg.node(v).order = i);
115119
addSubgraphConstraints(lg, cg, sorted.vs);
116120
});

lib/order/init-order.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as util from "../util";
2-
import type {Graph} from '../types';
2+
import type {Graph, NodeCollection} from '../types';
33

44
/*
55
* Assigns an initial order value for each node by performing a DFS search
@@ -12,7 +12,7 @@ import type {Graph} from '../types';
1212
* Returns a layering matrix with an array per layer and each layer sorted by
1313
* the order of its nodes.
1414
*/
15-
export default function initOrder(graph: Graph): string[][] {
15+
export default function initOrder(graph: Graph, oldNodes: NodeCollection): string[][] {
1616
const visited: { [key: string]: boolean } = {};
1717
const simpleNodes = graph.nodes().filter(v => !graph.children(v).length);
1818
const simpleNodesRanks = simpleNodes.map(v => graph.node(v).rank);
@@ -26,12 +26,20 @@ export default function initOrder(graph: Graph): string[][] {
2626
layers[node.rank]!.push(v);
2727
const successors = graph.successors(v);
2828
if (successors) {
29-
successors.forEach(dfs);
29+
const sortedSuccessors = [...successors].sort((a: string, b: string) => compareOldOrder(a, b));
30+
sortedSuccessors.forEach(dfs);
3031
}
3132
}
3233

3334
const orderedVs = simpleNodes.sort((a, b) => graph.node(a).rank - graph.node(b).rank);
3435
orderedVs.forEach(dfs);
3536

37+
function compareOldOrder(a: string, b: string) {
38+
const nodeA = graph.node(a);
39+
const nodeB = graph.node(b);
40+
41+
return util.compareByOldOrder(oldNodes, nodeA, nodeB);
42+
}
43+
3644
return layers;
3745
}

lib/order/sort-subgraph.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import barycenter from "./barycenter";
2-
import resolveConflicts from "./resolve-conflicts";
2+
import resolveConflicts, {ResolvedEntry} from "./resolve-conflicts";
33
import sort from "./sort";
4-
import type {Graph} from '../types';
4+
import type {Graph, NodeCollection} from '../types';
55

66
interface SubgraphResult {
77
vs: string[];
@@ -15,7 +15,7 @@ interface BarycenterEntry {
1515
weight?: number;
1616
}
1717

18-
export default function sortSubgraph(graph: Graph, v: string, constraintGraph: Graph, biasRight?: boolean): SubgraphResult {
18+
export default function sortSubgraph(graph: Graph, v: string, constraintGraph: Graph, oldNodes: NodeCollection, biasRight?: boolean): {result: SubgraphResult, usedBias: boolean} {
1919
let movable = graph.children(v);
2020
const node = graph.node(v);
2121
const bl: string | undefined = node ? (node.borderLeft) : undefined;
@@ -29,7 +29,7 @@ export default function sortSubgraph(graph: Graph, v: string, constraintGraph: G
2929
const barycenters = barycenter(graph, movable);
3030
barycenters.forEach(entry => {
3131
if (graph.children(entry.v).length) {
32-
const subgraphResult = sortSubgraph(graph, entry.v, constraintGraph, biasRight);
32+
const {result: subgraphResult} = sortSubgraph(graph, entry.v, constraintGraph, oldNodes, biasRight);
3333
subgraphs[entry.v] = subgraphResult;
3434
if (Object.hasOwn(subgraphResult, "barycenter")) {
3535
mergeBarycenters(entry, subgraphResult);
@@ -40,7 +40,41 @@ export default function sortSubgraph(graph: Graph, v: string, constraintGraph: G
4040
const entries = resolveConflicts(barycenters, constraintGraph);
4141
expandSubgraphs(entries, subgraphs);
4242

43-
const result = sort(entries, biasRight);
43+
const reversedPairs: Record<string, ResolvedEntry> = {};
44+
let usedBias = false;
45+
for (let i = 0; i < entries.length; i++) {
46+
for (let j = i + 1; j < entries.length; j++) {
47+
if (!entries[i] || !entries[j] || !entries[i]?.barycenter || !entries[j]?.barycenter) {
48+
continue;
49+
}
50+
if (entries[i]?.barycenter === entries[j]!.barycenter) {
51+
const nameI = entries[i]?.vs[0] ?? "";
52+
const nameJ = entries[j]?.vs[0] ?? "";
53+
const nodeI = graph.node(nameI);
54+
const nodeJ = graph.node(nameJ);
55+
56+
if (nodeI.dummy === "edge" && nodeJ.dummy === "edge" &&
57+
nodeI.edgeObj?.v === nodeJ.edgeObj?.v &&
58+
nodeI.edgeObj?.w === nodeJ.edgeObj?.w) {
59+
if (nodeI.edgeLabel.reversed) {
60+
reversedPairs[nameJ] = entries[i]!;
61+
entries.splice(i, 1);
62+
i--;
63+
break;
64+
} else {
65+
reversedPairs[nameI] = entries[j]!;
66+
entries.splice(j, 1);
67+
j--;
68+
}
69+
70+
} else {
71+
usedBias = true;
72+
}
73+
}
74+
}
75+
}
76+
77+
const result = sort(entries, reversedPairs, oldNodes, graph, biasRight);
4478

4579
if (bl && br) {
4680
result.vs = [bl, result.vs, br].flat(1) as string[];
@@ -59,7 +93,7 @@ export default function sortSubgraph(graph: Graph, v: string, constraintGraph: G
5993
}
6094
}
6195

62-
return result;
96+
return {result: result, usedBias: usedBias};
6397
}
6498

6599
function expandSubgraphs(entries: { vs: string[] }[], subgraphs: { [key: string]: SubgraphResult }): void {

lib/order/sort.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as util from "../util";
2+
import type {Graph, NodeCollection} from '../types';
3+
import {ResolvedEntry} from "./resolve-conflicts";
24

35
interface SortEntry {
46
vs: string[];
@@ -13,7 +15,7 @@ interface SortResult {
1315
weight?: number;
1416
}
1517

16-
export default function sort(entries: SortEntry[], biasRight?: boolean): SortResult {
18+
export default function sort(entries: SortEntry[], reversedPairs: Record<string, ResolvedEntry>, oldNodes: NodeCollection, graph: Graph, biasRight?: boolean): SortResult {
1719
const parts = util.partition(entries, entry => {
1820
return Object.hasOwn(entry, "barycenter");
1921
});
@@ -24,7 +26,13 @@ export default function sort(entries: SortEntry[], biasRight?: boolean): SortRes
2426
let weight = 0;
2527
let vsIndex = 0;
2628

27-
sortable.sort(compareWithBias(!!biasRight));
29+
sortable.sort(compareWithOldOrder(graph, oldNodes, !!biasRight));
30+
31+
// re-inserts the links, that are already in sortable but reversed, next to its reverse counterpart
32+
for (const [key, value] of Object.entries(reversedPairs)) {
33+
const keyIndex = sortable.findIndex(entry => entry.vs[0] === key);
34+
sortable.splice(keyIndex + 1, 0, value);
35+
}
2836

2937
vsIndex = consumeUnsortable(vs, unsortable, vsIndex);
3038

@@ -54,14 +62,23 @@ function consumeUnsortable(vs: string[][], unsortable: SortEntry[], index: numbe
5462
return index;
5563
}
5664

57-
function compareWithBias(bias: boolean): (entryV: SortEntry, entryW: SortEntry) => number {
65+
function compareWithOldOrder(graph: Graph, oldNodes: NodeCollection, bias: boolean): (entryV: SortEntry, entryW: SortEntry) => number {
5866
return (entryV: SortEntry, entryW: SortEntry) => {
5967
if (entryV.barycenter! < entryW.barycenter!) {
6068
return -1;
6169
} else if (entryV.barycenter! > entryW.barycenter!) {
6270
return 1;
6371
}
6472

73+
if (typeof entryV.vs[0] === "string" || typeof entryW.vs[0] === "string") {
74+
const nodeV = graph.node(entryV.vs[0]!);
75+
const nodeW = graph.node(entryW.vs[0]!);
76+
const byOldOrder = util.compareByOldOrder(oldNodes, nodeV, nodeW);
77+
if (byOldOrder !== 0) {
78+
return byOldOrder;
79+
}
80+
}
81+
6582
return !bias ? entryV.i - entryW.i : entryW.i - entryV.i;
6683
};
6784
}

0 commit comments

Comments
 (0)