feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
@@ -0,0 +1 @@
export function addSubgraphConstraints(g: any, cg: any, vs: any): void;
@@ -0,0 +1,53 @@
import * as _ from 'lodash-es';
export { addSubgraphConstraints };
function addSubgraphConstraints(g, cg, vs) {
var prev = {},
rootPrev;
_.forEach(vs, function (v) {
var child = g.parent(v),
parent,
prevChild;
while (child) {
parent = g.parent(child);
if (parent) {
prevChild = prev[parent];
prev[parent] = child;
} else {
prevChild = rootPrev;
rootPrev = child;
}
if (prevChild && prevChild !== child) {
cg.setEdge(prevChild, child);
return;
}
child = parent;
}
});
/*
function dfs(v) {
var children = v ? g.children(v) : g.children();
if (children.length) {
var min = Number.POSITIVE_INFINITY,
subgraphs = [];
_.each(children, function(child) {
var childMin = dfs(child);
if (g.children(child).length) {
subgraphs.push({ v: child, order: childMin });
}
min = Math.min(min, childMin);
});
_.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) {
cg.setEdge(prev.v, curr.v);
return curr;
});
return min;
}
return g.node(v).order;
}
dfs(undefined);
*/
}
@@ -0,0 +1,9 @@
export function barycenter(g: any, movable: any): ({
v: any;
barycenter?: undefined;
weight?: undefined;
} | {
v: any;
barycenter: number;
weight: any;
})[];
+31
View File
@@ -0,0 +1,31 @@
import * as _ from 'lodash-es';
export { barycenter };
function barycenter(g, movable) {
return _.map(movable, function (v) {
var inV = g.inEdges(v);
if (!inV.length) {
return { v: v };
} else {
var result = _.reduce(
inV,
function (acc, e) {
var edge = g.edge(e),
nodeU = g.node(e.v);
return {
sum: acc.sum + edge.weight * nodeU.order,
weight: acc.weight + edge.weight,
};
},
{ sum: 0, weight: 0 },
);
return {
v: v,
barycenter: result.sum / result.weight,
weight: result.weight,
};
}
});
}
@@ -0,0 +1,2 @@
export function buildLayerGraph(g: any, rank: any, relationship: any): Graph<any, any, any>;
import { Graph } from '../../graphlib/index.js';
@@ -0,0 +1,76 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
export { buildLayerGraph };
/*
* Constructs a graph that can be used to sort a layer of nodes. The graph will
* contain all base and subgraph nodes from the request layer in their original
* hierarchy and any edges that are incident on these nodes and are of the type
* requested by the "relationship" parameter.
*
* Nodes from the requested rank that do not have parents are assigned a root
* node in the output graph, which is set in the root graph attribute. This
* makes it easy to walk the hierarchy of movable nodes during ordering.
*
* Pre-conditions:
*
* 1. Input graph is a DAG
* 2. Base nodes in the input graph have a rank attribute
* 3. Subgraph nodes in the input graph has minRank and maxRank attributes
* 4. Edges have an assigned weight
*
* Post-conditions:
*
* 1. Output graph has all nodes in the movable rank with preserved
* hierarchy.
* 2. Root nodes in the movable layer are made children of the node
* indicated by the root attribute of the graph.
* 3. Non-movable nodes incident on movable nodes, selected by the
* relationship parameter, are included in the graph (without hierarchy).
* 4. Edges incident on movable nodes, selected by the relationship
* parameter, are added to the output graph.
* 5. The weights for copied edges are aggregated as need, since the output
* graph is not a multi-graph.
*/
function buildLayerGraph(g, rank, relationship) {
var root = createRootNode(g),
result = new Graph({ compound: true })
.setGraph({ root: root })
.setDefaultNodeLabel(function (v) {
return g.node(v);
});
_.forEach(g.nodes(), function (v) {
var node = g.node(v),
parent = g.parent(v);
if (node.rank === rank || (node.minRank <= rank && rank <= node.maxRank)) {
result.setNode(v);
result.setParent(v, parent || root);
// This assumes we have only short edges!
_.forEach(g[relationship](v), function (e) {
var u = e.v === v ? e.w : e.v,
edge = result.edge(u, v),
weight = !_.isUndefined(edge) ? edge.weight : 0;
result.setEdge(u, v, { weight: g.edge(e).weight + weight });
});
if (Object.prototype.hasOwnProperty.call(node, 'minRank')) {
result.setNode(v, {
borderLeft: node.borderLeft[rank],
borderRight: node.borderRight[rank],
});
}
}
});
return result;
}
function createRootNode(g) {
var v;
while (g.hasNode((v = _.uniqueId('_root'))));
return v;
}
@@ -0,0 +1 @@
export function crossCount(g: any, layering: any): number;
+79
View File
@@ -0,0 +1,79 @@
import * as _ from 'lodash-es';
export { crossCount };
/*
* A function that takes a layering (an array of layers, each with an array of
* ordererd nodes) and a graph and returns a weighted crossing count.
*
* Pre-conditions:
*
* 1. Input graph must be simple (not a multigraph), directed, and include
* only simple edges.
* 2. Edges in the input graph must have assigned weights.
*
* Post-conditions:
*
* 1. The graph and layering matrix are left unchanged.
*
* This algorithm is derived from Barth, et al., "Bilayer Cross Counting."
*/
function crossCount(g, layering) {
var cc = 0;
for (var i = 1; i < layering.length; ++i) {
cc += twoLayerCrossCount(g, layering[i - 1], layering[i]);
}
return cc;
}
function twoLayerCrossCount(g, northLayer, southLayer) {
// Sort all of the edges between the north and south layers by their position
// in the north layer and then the south. Map these edges to the position of
// their head in the south layer.
var southPos = _.zipObject(
southLayer,
_.map(southLayer, function (v, i) {
return i;
}),
);
var southEntries = _.flatten(
_.map(northLayer, function (v) {
return _.sortBy(
_.map(g.outEdges(v), function (e) {
return { pos: southPos[e.w], weight: g.edge(e).weight };
}),
'pos',
);
}),
);
// Build the accumulator tree
var firstIndex = 1;
while (firstIndex < southLayer.length) firstIndex <<= 1;
var treeSize = 2 * firstIndex - 1;
firstIndex -= 1;
var tree = _.map(new Array(treeSize), function () {
return 0;
});
// Calculate the weighted crossings
var cc = 0;
_.forEach(
// @ts-expect-error
southEntries.forEach(function (entry) {
var index = entry.pos + firstIndex;
tree[index] += entry.weight;
var weightSum = 0;
while (index > 0) {
if (index % 2) {
weightSum += tree[index + 1];
}
index = (index - 1) >> 1;
tree[index] += entry.weight;
}
cc += entry.weight * weightSum;
}),
);
return cc;
}
+1
View File
@@ -0,0 +1 @@
export function order(g: any): void;
+77
View File
@@ -0,0 +1,77 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
import * as util from '../util.js';
import { addSubgraphConstraints } from './add-subgraph-constraints.js';
import { buildLayerGraph } from './build-layer-graph.js';
import { crossCount } from './cross-count.js';
import { initOrder } from './init-order.js';
import { sortSubgraph } from './sort-subgraph.js';
export { order };
/*
* Applies heuristics to minimize edge crossings in the graph and sets the best
* order solution as an order attribute on each node.
*
* Pre-conditions:
*
* 1. Graph must be DAG
* 2. Graph nodes must be objects with a "rank" attribute
* 3. Graph edges must have the "weight" attribute
*
* Post-conditions:
*
* 1. Graph nodes will have an "order" attribute based on the results of the
* algorithm.
*/
function order(g) {
var maxRank = util.maxRank(g),
downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), 'inEdges'),
upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), 'outEdges');
var layering = initOrder(g);
assignOrder(g, layering);
var bestCC = Number.POSITIVE_INFINITY,
best;
for (var i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) {
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2);
layering = util.buildLayerMatrix(g);
var cc = crossCount(g, layering);
if (cc < bestCC) {
lastBest = 0;
best = _.cloneDeep(layering);
bestCC = cc;
}
}
assignOrder(g, best);
}
function buildLayerGraphs(g, ranks, relationship) {
return _.map(ranks, function (rank) {
return buildLayerGraph(g, rank, relationship);
});
}
function sweepLayerGraphs(layerGraphs, biasRight) {
var cg = new Graph();
_.forEach(layerGraphs, function (lg) {
var root = lg.graph().root;
var sorted = sortSubgraph(lg, root, cg, biasRight);
_.forEach(sorted.vs, function (v, i) {
lg.node(v).order = i;
});
addSubgraphConstraints(lg, cg, sorted.vs);
});
}
function assignOrder(g, layering) {
_.forEach(layering, function (layer) {
_.forEach(layer, function (v, i) {
g.node(v).order = i;
});
});
}
@@ -0,0 +1 @@
export function initOrder(g: any): any[][];
+42
View File
@@ -0,0 +1,42 @@
import * as _ from 'lodash-es';
/*
* Assigns an initial order value for each node by performing a DFS search
* starting from nodes in the first rank. Nodes are assigned an order in their
* rank as they are first visited.
*
* This approach comes from Gansner, et al., "A Technique for Drawing Directed
* Graphs."
*
* Returns a layering matrix with an array per layer and each layer sorted by
* the order of its nodes.
*/
export function initOrder(g) {
var visited = {};
var simpleNodes = _.filter(g.nodes(), function (v) {
return !g.children(v).length;
});
var maxRank = _.max(
_.map(simpleNodes, function (v) {
return g.node(v).rank;
}),
);
var layers = _.map(_.range(maxRank + 1), function () {
return [];
});
function dfs(v) {
if (_.has(visited, v)) return;
visited[v] = true;
var node = g.node(v);
layers[node.rank].push(v);
_.forEach(g.successors(v), dfs);
}
var orderedVs = _.sortBy(simpleNodes, function (v) {
return g.node(v).rank;
});
_.forEach(orderedVs, dfs);
return layers;
}
@@ -0,0 +1 @@
export function resolveConflicts(entries: any, cg: any): Pick<any, "weight" | "barycenter" | "vs" | "i">[];
@@ -0,0 +1,128 @@
import * as _ from 'lodash-es';
export { resolveConflicts };
/*
* Given a list of entries of the form {v, barycenter, weight} and a
* constraint graph this function will resolve any conflicts between the
* constraint graph and the barycenters for the entries. If the barycenters for
* an entry would violate a constraint in the constraint graph then we coalesce
* the nodes in the conflict into a new node that respects the contraint and
* aggregates barycenter and weight information.
*
* This implementation is based on the description in Forster, "A Fast and
* Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it
* differs in some specific details.
*
* Pre-conditions:
*
* 1. Each entry has the form {v, barycenter, weight}, or if the node has
* no barycenter, then {v}.
*
* Returns:
*
* A new list of entries of the form {vs, i, barycenter, weight}. The list
* `vs` may either be a singleton or it may be an aggregation of nodes
* ordered such that they do not violate constraints from the constraint
* graph. The property `i` is the lowest original index of any of the
* elements in `vs`.
*/
function resolveConflicts(entries, cg) {
var mappedEntries = {};
_.forEach(entries, function (entry, i) {
var tmp = (mappedEntries[entry.v] = {
indegree: 0,
in: [],
out: [],
vs: [entry.v],
i: i,
});
if (!_.isUndefined(entry.barycenter)) {
// @ts-expect-error
tmp.barycenter = entry.barycenter;
// @ts-expect-error
tmp.weight = entry.weight;
}
});
_.forEach(cg.edges(), function (e) {
var entryV = mappedEntries[e.v];
var entryW = mappedEntries[e.w];
if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) {
entryW.indegree++;
entryV.out.push(mappedEntries[e.w]);
}
});
var sourceSet = _.filter(mappedEntries, function (entry) {
// @ts-expect-error
return !entry.indegree;
});
return doResolveConflicts(sourceSet);
}
function doResolveConflicts(sourceSet) {
var entries = [];
function handleIn(vEntry) {
return function (uEntry) {
if (uEntry.merged) {
return;
}
if (
_.isUndefined(uEntry.barycenter) ||
_.isUndefined(vEntry.barycenter) ||
uEntry.barycenter >= vEntry.barycenter
) {
mergeEntries(vEntry, uEntry);
}
};
}
function handleOut(vEntry) {
return function (wEntry) {
wEntry['in'].push(vEntry);
if (--wEntry.indegree === 0) {
sourceSet.push(wEntry);
}
};
}
while (sourceSet.length) {
var entry = sourceSet.pop();
entries.push(entry);
_.forEach(entry['in'].reverse(), handleIn(entry));
_.forEach(entry.out, handleOut(entry));
}
return _.map(
_.filter(entries, function (entry) {
return !entry.merged;
}),
function (entry) {
return _.pick(entry, ['vs', 'i', 'barycenter', 'weight']);
},
);
}
function mergeEntries(target, source) {
var sum = 0;
var weight = 0;
if (target.weight) {
sum += target.barycenter * target.weight;
weight += target.weight;
}
if (source.weight) {
sum += source.barycenter * source.weight;
weight += source.weight;
}
target.vs = source.vs.concat(target.vs);
target.barycenter = sum / weight;
target.weight = weight;
target.i = Math.min(source.i, target.i);
source.merged = true;
}
@@ -0,0 +1,3 @@
export function sortSubgraph(g: any, v: any, cg: any, biasRight: any): {
vs: any[];
};
@@ -0,0 +1,78 @@
import * as _ from 'lodash-es';
import { barycenter } from './barycenter.js';
import { resolveConflicts } from './resolve-conflicts.js';
import { sort } from './sort.js';
export { sortSubgraph };
function sortSubgraph(g, v, cg, biasRight) {
var movable = g.children(v);
var node = g.node(v);
var bl = node ? node.borderLeft : undefined;
var br = node ? node.borderRight : undefined;
var subgraphs = {};
if (bl) {
movable = _.filter(movable, function (w) {
return w !== bl && w !== br;
});
}
var barycenters = barycenter(g, movable);
_.forEach(barycenters, function (entry) {
if (g.children(entry.v).length) {
var subgraphResult = sortSubgraph(g, entry.v, cg, biasRight);
subgraphs[entry.v] = subgraphResult;
if (Object.prototype.hasOwnProperty.call(subgraphResult, 'barycenter')) {
mergeBarycenters(entry, subgraphResult);
}
}
});
var entries = resolveConflicts(barycenters, cg);
expandSubgraphs(entries, subgraphs);
var result = sort(entries, biasRight);
if (bl) {
result.vs = _.flatten([bl, result.vs, br]);
if (g.predecessors(bl).length) {
var blPred = g.node(g.predecessors(bl)[0]),
brPred = g.node(g.predecessors(br)[0]);
if (!Object.prototype.hasOwnProperty.call(result, 'barycenter')) {
result.barycenter = 0;
result.weight = 0;
}
result.barycenter =
(result.barycenter * result.weight + blPred.order + brPred.order) / (result.weight + 2);
result.weight += 2;
}
}
return result;
}
function expandSubgraphs(entries, subgraphs) {
_.forEach(entries, function (entry) {
entry.vs = _.flatten(
entry.vs.map(function (v) {
if (subgraphs[v]) {
return subgraphs[v].vs;
}
return v;
}),
);
});
}
function mergeBarycenters(target, other) {
if (!_.isUndefined(target.barycenter)) {
target.barycenter =
(target.barycenter * target.weight + other.barycenter * other.weight) /
(target.weight + other.weight);
target.weight += other.weight;
} else {
target.barycenter = other.barycenter;
target.weight = other.weight;
}
}
+3
View File
@@ -0,0 +1,3 @@
export function sort(entries: any, biasRight: any): {
vs: any[];
};
+59
View File
@@ -0,0 +1,59 @@
import * as _ from 'lodash-es';
import * as util from '../util.js';
export { sort };
function sort(entries, biasRight) {
var parts = util.partition(entries, function (entry) {
return Object.prototype.hasOwnProperty.call(entry, 'barycenter');
});
var sortable = parts.lhs,
unsortable = _.sortBy(parts.rhs, function (entry) {
return -entry.i;
}),
vs = [],
sum = 0,
weight = 0,
vsIndex = 0;
sortable.sort(compareWithBias(!!biasRight));
vsIndex = consumeUnsortable(vs, unsortable, vsIndex);
_.forEach(sortable, function (entry) {
vsIndex += entry.vs.length;
vs.push(entry.vs);
sum += entry.barycenter * entry.weight;
weight += entry.weight;
vsIndex = consumeUnsortable(vs, unsortable, vsIndex);
});
var result = { vs: _.flatten(vs) };
if (weight) {
result.barycenter = sum / weight;
result.weight = weight;
}
return result;
}
function consumeUnsortable(vs, unsortable, index) {
var last;
while (unsortable.length && (last = _.last(unsortable)).i <= index) {
unsortable.pop();
vs.push(last.vs);
index++;
}
return index;
}
function compareWithBias(bias) {
return function (entryV, entryW) {
if (entryV.barycenter < entryW.barycenter) {
return -1;
} else if (entryV.barycenter > entryW.barycenter) {
return 1;
}
return !bias ? entryV.i - entryW.i : entryW.i - entryV.i;
};
}