@antv/layout
Version:
graph layout algorithm
273 lines (270 loc) • 8.29 kB
JavaScript
import { isArray } from './array.js';
import isNumber from '../node_modules/@antv/util/esm/lodash/is-number.js';
/**
* Floyd-Warshall algorithm to find shortest paths (but with no negative cycles).
*/
const floydWarshall = (adjMatrix) => {
// initialize
const n = adjMatrix.length;
const dist = Array.from({ length: n }, () => new Array(n));
for (let i = 0; i < n; i++) {
const row = adjMatrix[i];
const drow = dist[i];
for (let j = 0; j < n; j++) {
drow[j] = i === j ? 0 : row[j] > 0 ? row[j] : Infinity;
}
}
// floyd
for (let k = 0; k < n; k++) {
const dk = dist[k];
for (let i = 0; i < n; i++) {
const di = dist[i];
const dik = di[k];
if (dik === Infinity)
continue;
for (let j = 0; j < n; j++) {
const dkj = dk[j];
if (dkj === Infinity)
continue;
const next = dik + dkj;
if (next < di[j]) {
di[j] = next;
}
}
}
}
return dist;
};
/**
* Get the adjacency matrix of the graph model.
*/
const getAdjMatrix = (model, directed) => {
const n = model.nodeCount();
const matrix = Array.from({ length: n }, () => new Array(n));
// map node with index in data.nodes
const nodeMap = {};
let i = 0;
model.forEachNode((node) => {
nodeMap[node.id] = i++;
});
model.forEachEdge((e) => {
const sIndex = nodeMap[e.source];
const tIndex = nodeMap[e.target];
if (sIndex === undefined || tIndex === undefined)
return;
matrix[sIndex][tIndex] = 1;
if (!directed) {
matrix[tIndex][sIndex] = 1;
}
});
return matrix;
};
/**
* Get the adjacency list of the graph model.
*/
const getAdjList = (model, directed) => {
const n = model.nodeCount();
const adjList = Array.from({ length: n }, () => []);
// map node with index
const nodeMap = {};
let idx = 0;
model.forEachNode((node) => {
nodeMap[node.id] = idx++;
});
model.forEachEdge((e) => {
const s = nodeMap[e.source];
const t = nodeMap[e.target];
if (s == null || t == null)
return;
adjList[s].push(t);
if (!directed)
adjList[t].push(s);
});
return adjList;
};
/**
* scale matrix
* @param matrix [ [], [], [] ]
* @param ratio
*/
const scaleMatrix = (matrix, ratio) => {
const n = matrix.length;
const result = new Array(n);
for (let i = 0; i < n; i++) {
const row = matrix[i];
const m = row.length;
const newRow = new Array(m);
for (let j = 0; j < m; j++) {
newRow[j] = row[j] * ratio;
}
result[i] = newRow;
}
return result;
};
/**
* calculate the bounding box for the nodes according to their x, y, and size
* @param nodes nodes in the layout
* @returns
*/
const getLayoutBBox = (nodes) => {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
nodes.forEach((node) => {
let size = node.data.size;
if (isArray(size)) {
if (size.length === 1)
size = [size[0], size[0]];
}
else if (size === undefined || isNaN(size)) {
size = [30, 30];
}
else if (isNumber(size)) {
size = [size, size];
}
const halfSize = [size[0] / 2, size[1] / 2];
const left = node.data.x - halfSize[0];
const right = node.data.x + halfSize[0];
const top = node.data.y - halfSize[1];
const bottom = node.data.y + halfSize[1];
if (minX > left)
minX = left;
if (minY > top)
minY = top;
if (maxX < right)
maxX = right;
if (maxY < bottom)
maxY = bottom;
});
return { minX, minY, maxX, maxY };
};
/**
* calculate the euclidean distance form p1 to p2
* @param p1
* @param p2
* @returns
*/
const getEuclideanDistance = (p1, p2) => Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
/**
* Depth first search begin from nodes in graphCore data.
* @param graphCore graphlib data structure
* @param nodes begin nodes
* @param fn will be called while visiting each node
* @param mode 'TB' - visit from top to bottom; 'BT' - visit from bottom to top;
* @returns
*/
const graphTreeDfs = (graph, nodes, fn, mode = 'TB', treeKey, stopFns = {}) => {
if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length))
return;
const { stopBranchFn, stopAllFn } = stopFns;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!graph.hasNode(node.id))
continue;
if (stopBranchFn === null || stopBranchFn === void 0 ? void 0 : stopBranchFn(node))
continue; // Stop this branch
if (stopAllFn === null || stopAllFn === void 0 ? void 0 : stopAllFn(node))
return; // Stop all
if (mode === 'TB')
fn(node); // Traverse from top to bottom
graphTreeDfs(graph, graph.getChildren(node.id, treeKey), fn, mode, treeKey, stopFns);
if (mode !== 'TB')
fn(node); // Traverse from bottom to top
}
};
/**
* Use Johnson + Dijkstra to compute APSP for sparse graph.
* Fully compatible with floydWarshall(adjMatrix).
*/
function johnson(adjList) {
const n = adjList.length;
// Step 1: add a dummy node q connected to all nodes with weight 0
new Array(n).fill(0);
// Bellman-Ford to compute potentials h(v)
// 因为权重全是 1,无负边,可直接跳过 BF,h 全 0 即可
// Step 2: reweight edges
// 因为 h(u)=h(v)=0,reweight 后仍然是 1,省略 reweight 过程
// Step 3: run Dijkstra from each node
const distAll = Array.from({ length: n }, () => new Array(n).fill(Infinity));
for (let s = 0; s < n; s++) {
distAll[s] = dijkstra(adjList, s);
}
return distAll;
}
/**
* Dijkstra algorithm to find shortest paths from source to all nodes.
*/
function dijkstra(adjList, source) {
const n = adjList.length;
const dist = new Array(n).fill(Infinity);
dist[source] = 0;
// Minimal binary heap
const heap = new MinHeap();
heap.push([0, source]); // [distance, node]
while (!heap.empty()) {
const [d, u] = heap.pop();
if (d !== dist[u])
continue;
const neighbors = adjList[u];
for (let i = 0; i < neighbors.length; i++) {
const v = neighbors[i];
const nd = d + 1;
if (nd < dist[v]) {
dist[v] = nd;
heap.push([nd, v]);
}
}
}
return dist;
}
class MinHeap {
constructor() {
this.data = [];
}
push(item) {
this.data.push(item);
this.bubbleUp(this.data.length - 1);
}
pop() {
const top = this.data[0];
const end = this.data.pop();
if (this.data.length > 0) {
this.data[0] = end;
this.bubbleDown(0);
}
return top;
}
empty() {
return this.data.length === 0;
}
bubbleUp(pos) {
const data = this.data;
while (pos > 0) {
const parent = (pos - 1) >> 1;
if (data[parent][0] <= data[pos][0])
break;
[data[parent], data[pos]] = [data[pos], data[parent]];
pos = parent;
}
}
bubbleDown(pos) {
const data = this.data;
const length = data.length;
while (true) {
const left = pos * 2 + 1;
const right = pos * 2 + 2;
let min = pos;
if (left < length && data[left][0] < data[min][0])
min = left;
if (right < length && data[right][0] < data[min][0])
min = right;
if (min === pos)
break;
[data[pos], data[min]] = [data[min], data[pos]];
pos = min;
}
}
}
export { floydWarshall, getAdjList, getAdjMatrix, getEuclideanDistance, getLayoutBBox, graphTreeDfs, johnson, scaleMatrix };
//# sourceMappingURL=math.js.map