You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
8.4 KiB
281 lines
8.4 KiB
import { Graph } from '@antv/graphlib';
|
|
import { isNumber } from '@antv/util';
|
|
const safeSort = (valueA, valueB) => {
|
|
return Number(valueA) - Number(valueB);
|
|
};
|
|
/*
|
|
* Adds a dummy node to the graph and return v.
|
|
*/
|
|
export const addDummyNode = (g, type, data, name) => {
|
|
let v;
|
|
do {
|
|
v = `${name}${Math.random()}`;
|
|
} while (g.hasNode(v));
|
|
data.dummy = type;
|
|
g.addNode({
|
|
id: v,
|
|
data,
|
|
});
|
|
return v;
|
|
};
|
|
/*
|
|
* Returns a new graph with only simple edges. Handles aggregation of data
|
|
* associated with multi-edges.
|
|
*/
|
|
export const simplify = (g) => {
|
|
const simplified = new Graph();
|
|
g.getAllNodes().forEach((v) => {
|
|
simplified.addNode(Object.assign({}, v));
|
|
});
|
|
g.getAllEdges().forEach((e) => {
|
|
const edge = simplified
|
|
.getRelatedEdges(e.source, 'out')
|
|
.find((edge) => edge.target === e.target);
|
|
if (!edge) {
|
|
simplified.addEdge({
|
|
id: e.id,
|
|
source: e.source,
|
|
target: e.target,
|
|
data: {
|
|
weight: e.data.weight || 0,
|
|
minlen: e.data.minlen || 1,
|
|
},
|
|
});
|
|
}
|
|
else {
|
|
simplified.updateEdgeData(edge === null || edge === void 0 ? void 0 : edge.id, Object.assign(Object.assign({}, edge.data), { weight: edge.data.weight + e.data.weight || 0, minlen: Math.max(edge.data.minlen, e.data.minlen || 1) }));
|
|
}
|
|
});
|
|
return simplified;
|
|
};
|
|
export const asNonCompoundGraph = (g) => {
|
|
const simplified = new Graph();
|
|
g.getAllNodes().forEach((node) => {
|
|
if (!g.getChildren(node.id).length) {
|
|
simplified.addNode(Object.assign({}, node));
|
|
}
|
|
});
|
|
g.getAllEdges().forEach((edge) => {
|
|
simplified.addEdge(edge);
|
|
});
|
|
return simplified;
|
|
};
|
|
export const zipObject = (keys, values) => {
|
|
return keys === null || keys === void 0 ? void 0 : keys.reduce((obj, key, i) => {
|
|
obj[key] = values[i];
|
|
return obj;
|
|
}, {});
|
|
};
|
|
export const successorWeights = (g) => {
|
|
const weightsMap = {};
|
|
g.getAllNodes().forEach((node) => {
|
|
const sucs = {};
|
|
g.getRelatedEdges(node.id, 'out').forEach((e) => {
|
|
sucs[e.target] = (sucs[e.target] || 0) + (e.data.weight || 0);
|
|
});
|
|
weightsMap[node.id] = sucs;
|
|
});
|
|
return weightsMap;
|
|
};
|
|
export const predecessorWeights = (g) => {
|
|
const nodes = g.getAllNodes();
|
|
const weightMap = nodes.map((v) => {
|
|
const preds = {};
|
|
g.getRelatedEdges(v.id, 'in').forEach((e) => {
|
|
preds[e.source] = (preds[e.source] || 0) + e.data.weight;
|
|
});
|
|
return preds;
|
|
});
|
|
return zipObject(nodes.map((n) => n.id), weightMap);
|
|
};
|
|
/*
|
|
* Finds where a line starting at point ({x, y}) would intersect a rectangle
|
|
* ({x, y, width, height}) if it were pointing at the rectangle's center.
|
|
*/
|
|
export const intersectRect = (rect, point) => {
|
|
const x = Number(rect.x);
|
|
const y = Number(rect.y);
|
|
// Rectangle intersection algorithm from:
|
|
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
|
const dx = Number(point.x) - x;
|
|
const dy = Number(point.y) - y;
|
|
let w = Number(rect.width) / 2;
|
|
let h = Number(rect.height) / 2;
|
|
if (!dx && !dy) {
|
|
// completely overlapped directly, then return points its self
|
|
return { x: 0, y: 0 };
|
|
}
|
|
let sx;
|
|
let sy;
|
|
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
// Intersection is top or bottom of rect.
|
|
if (dy < 0) {
|
|
h = -h;
|
|
}
|
|
sx = (h * dx) / dy;
|
|
sy = h;
|
|
}
|
|
else {
|
|
// Intersection is left or right of rect.
|
|
if (dx < 0) {
|
|
w = -w;
|
|
}
|
|
sx = w;
|
|
sy = (w * dy) / dx;
|
|
}
|
|
return { x: x + sx, y: y + sy };
|
|
};
|
|
/*
|
|
* Given a DAG with each node assigned "rank" and "order" properties, this
|
|
* const will produce a matrix with the ids of each node.
|
|
*/
|
|
export const buildLayerMatrix = (g) => {
|
|
const layeringNodes = [];
|
|
const rankMax = maxRank(g) + 1;
|
|
for (let i = 0; i < rankMax; i++) {
|
|
layeringNodes.push([]);
|
|
}
|
|
// const layering = _.map(_.range(maxRank(g) + 1), function() { return []; });
|
|
g.getAllNodes().forEach((node) => {
|
|
const rank = node.data.rank;
|
|
if (rank !== undefined && layeringNodes[rank]) {
|
|
layeringNodes[rank].push(node.id);
|
|
}
|
|
});
|
|
for (let i = 0; i < rankMax; i++) {
|
|
layeringNodes[i] = layeringNodes[i].sort((va, vb) => safeSort(g.getNode(va).data.order, g.getNode(vb).data.order));
|
|
}
|
|
return layeringNodes;
|
|
};
|
|
/*
|
|
* Adjusts the ranks for all nodes in the graph such that all nodes v have
|
|
* rank(v) >= 0 and at least one node w has rank(w) = 0.
|
|
*/
|
|
export const normalizeRanks = (g) => {
|
|
const nodeRanks = g
|
|
.getAllNodes()
|
|
.filter((v) => v.data.rank !== undefined)
|
|
.map((v) => v.data.rank);
|
|
const min = Math.min(...nodeRanks);
|
|
g.getAllNodes().forEach((v) => {
|
|
if (v.data.hasOwnProperty('rank') && min !== Infinity) {
|
|
v.data.rank -= min;
|
|
}
|
|
});
|
|
};
|
|
export const removeEmptyRanks = (g, nodeRankFactor = 0) => {
|
|
// Ranks may not start at 0, so we need to offset them
|
|
const nodes = g.getAllNodes();
|
|
const nodeRanks = nodes
|
|
.filter((v) => v.data.rank !== undefined)
|
|
.map((v) => v.data.rank);
|
|
const offset = Math.min(...nodeRanks);
|
|
const layers = [];
|
|
nodes.forEach((v) => {
|
|
const rank = (v.data.rank || 0) - offset;
|
|
if (!layers[rank]) {
|
|
layers[rank] = [];
|
|
}
|
|
layers[rank].push(v.id);
|
|
});
|
|
let delta = 0;
|
|
for (let i = 0; i < layers.length; i++) {
|
|
const vs = layers[i];
|
|
if (vs === undefined) {
|
|
if (i % nodeRankFactor !== 0) {
|
|
delta -= 1;
|
|
}
|
|
}
|
|
else if (delta) {
|
|
vs === null || vs === void 0 ? void 0 : vs.forEach((v) => {
|
|
const node = g.getNode(v);
|
|
if (node) {
|
|
node.data.rank = node.data.rank || 0;
|
|
node.data.rank += delta;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
export const addBorderNode = (g, prefix, rank, order) => {
|
|
const node = {
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
if (isNumber(rank) && isNumber(order)) {
|
|
node.rank = rank;
|
|
node.order = order;
|
|
}
|
|
return addDummyNode(g, 'border', node, prefix);
|
|
};
|
|
export const maxRank = (g) => {
|
|
let maxRank;
|
|
g.getAllNodes().forEach((v) => {
|
|
const rank = v.data.rank;
|
|
if (rank !== undefined) {
|
|
if (maxRank === undefined || rank > maxRank) {
|
|
maxRank = rank;
|
|
}
|
|
}
|
|
});
|
|
if (!maxRank) {
|
|
maxRank = 0;
|
|
}
|
|
return maxRank;
|
|
};
|
|
/*
|
|
* Partition a collection into two groups: `lhs` and `rhs`. If the supplied
|
|
* const returns true for an entry it goes into `lhs`. Otherwise it goes
|
|
* into `rhs.
|
|
*/
|
|
export const partition = (collection, fn) => {
|
|
const result = { lhs: [], rhs: [] };
|
|
collection === null || collection === void 0 ? void 0 : collection.forEach((value) => {
|
|
if (fn(value)) {
|
|
result.lhs.push(value);
|
|
}
|
|
else {
|
|
result.rhs.push(value);
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
export const minBy = (array, func) => {
|
|
return array.reduce((a, b) => {
|
|
const valA = func(a);
|
|
const valB = func(b);
|
|
return valA > valB ? b : a;
|
|
});
|
|
};
|
|
const doDFS = (graph, node, postorder, visited, navigator, result) => {
|
|
if (!visited.includes(node.id)) {
|
|
visited.push(node.id);
|
|
if (!postorder) {
|
|
result.push(node.id);
|
|
}
|
|
navigator(node.id).forEach((n) => doDFS(graph, n, postorder, visited, navigator, result));
|
|
if (postorder) {
|
|
result.push(node.id);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* @description DFS traversal.
|
|
* @description.zh-CN DFS 遍历。
|
|
*/
|
|
export const dfs = (graph, node, order, isDirected) => {
|
|
const nodes = Array.isArray(node) ? node : [node];
|
|
const navigator = (n) => (isDirected ? graph.getSuccessors(n) : graph.getNeighbors(n));
|
|
const results = [];
|
|
const visited = [];
|
|
nodes.forEach((node) => {
|
|
if (!graph.hasNode(node.id)) {
|
|
throw new Error(`Graph does not have node: ${node}`);
|
|
}
|
|
else {
|
|
doDFS(graph, node, order === 'post', visited, navigator, results);
|
|
}
|
|
});
|
|
return results;
|
|
};
|
|
//# sourceMappingURL=util.js.map
|