import { __awaiter } from "tslib"; import { isString } from '@antv/util'; import { cloneFormatData, floydWarshall, formatNodeSizeToNumber, getAdjMatrix, getEuclideanDistance, } from '../util'; import { handleSingleNodeGraph } from '../util/common'; import { mds } from './mds'; import { radialNonoverlapForce, } from './radial-nonoverlap-force'; const DEFAULTS_LAYOUT_OPTIONS = { maxIteration: 1000, focusNode: null, unitRadius: null, linkDistance: 50, preventOverlap: false, strictRadial: true, maxPreventOverlapIteration: 200, sortStrength: 10, }; /** * 径向布局 * * Radial layout */ export class RadialLayout { constructor(options = {}) { this.options = options; this.id = 'radial'; this.options = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS), options); } /** * Return the positions of nodes and edges(if needed). */ execute(graph, options) { return __awaiter(this, void 0, void 0, function* () { return this.genericRadialLayout(false, graph, options); }); } /** * To directly assign the positions to the nodes. */ assign(graph, options) { return __awaiter(this, void 0, void 0, function* () { yield this.genericRadialLayout(true, graph, options); }); } genericRadialLayout(assign, graph, options) { return __awaiter(this, void 0, void 0, function* () { const mergedOptions = Object.assign(Object.assign({}, this.options), options); const { width: propsWidth, height: propsHeight, center: propsCenter, focusNode: propsFocusNode, unitRadius: propsUnitRadius, nodeSize, nodeSpacing, strictRadial, preventOverlap, maxPreventOverlapIteration, sortBy, linkDistance = 50, sortStrength = 10, maxIteration = 1000, } = mergedOptions; const nodes = graph.getAllNodes(); const edges = graph.getAllEdges(); const width = !propsWidth && typeof window !== 'undefined' ? window.innerWidth : propsWidth; const height = !propsHeight && typeof window !== 'undefined' ? window.innerHeight : propsHeight; const center = (!propsCenter ? [width / 2, height / 2] : propsCenter); if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length) || nodes.length === 1) { return handleSingleNodeGraph(graph, assign, center); } let focusNode = nodes[0]; if (isString(propsFocusNode)) { for (let i = 0; i < nodes.length; i++) { if (nodes[i].id === propsFocusNode) { focusNode = nodes[i]; break; } } } else { focusNode = propsFocusNode || nodes[0]; } // the index of the focusNode in data const focusIndex = getIndexById(nodes, focusNode.id); // the graph-theoretic distance (shortest path distance) matrix const adjMatrix = getAdjMatrix({ nodes, edges }, false); const distances = floydWarshall(adjMatrix); const maxDistance = maxToFocus(distances, focusIndex); // replace first node in unconnected component to the circle at (maxDistance + 1) handleInfinity(distances, focusIndex, maxDistance + 1); // the shortest path distance from each node to focusNode const focusNodeD = distances[focusIndex]; let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0]; let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1]; if (semiWidth === 0) { semiWidth = width / 2; } if (semiHeight === 0) { semiHeight = height / 2; } // the maxRadius of the graph const maxRadius = Math.min(semiWidth, semiHeight); const maxD = Math.max(...focusNodeD); // the radius for each nodes away from focusNode const radii = []; const unitRadius = !propsUnitRadius ? maxRadius / maxD : propsUnitRadius; focusNodeD.forEach((value, i) => { radii[i] = value * unitRadius; }); const idealDistances = eIdealDisMatrix(nodes, distances, linkDistance, radii, unitRadius, sortBy, sortStrength); // the weight matrix, Wij = 1 / dij^(-2) const weights = getWeightMatrix(idealDistances); // the initial positions from mds, move the graph to origin, centered at focusNode const mdsResult = mds(linkDistance, idealDistances, linkDistance); let positions = mdsResult.map(([x, y]) => ({ x: (isNaN(x) ? Math.random() * linkDistance : x) - mdsResult[focusIndex][0], y: (isNaN(y) ? Math.random() * linkDistance : y) - mdsResult[focusIndex][1], })); this.run(maxIteration, positions, weights, idealDistances, radii, focusIndex); let nodeSizeFunc; // stagger the overlapped nodes if (preventOverlap) { nodeSizeFunc = formatNodeSizeToNumber(nodeSize, nodeSpacing); const nonoverlapForceParams = { nodes, nodeSizeFunc, positions, radii, height, width, strictRadial: Boolean(strictRadial), focusIdx: focusIndex, iterations: maxPreventOverlapIteration || 200, k: positions.length / 4.5, }; positions = radialNonoverlapForce(graph, nonoverlapForceParams); } // move the graph to center const layoutNodes = []; positions.forEach((p, i) => { const cnode = cloneFormatData(nodes[i]); cnode.data.x = p.x + center[0]; cnode.data.y = p.y + center[1]; layoutNodes.push(cnode); }); if (assign) { layoutNodes.forEach((node) => graph.mergeNodeData(node.id, { x: node.data.x, y: node.data.y, })); } const result = { nodes: layoutNodes, edges, }; return result; }); } run(maxIteration, positions, weights, idealDistances, radii, focusIndex) { for (let i = 0; i <= maxIteration; i++) { const param = i / maxIteration; this.oneIteration(param, positions, radii, idealDistances, weights, focusIndex); } } oneIteration(param, positions, radii, distances, weights, focusIndex) { const vparam = 1 - param; positions.forEach((v, i) => { // v const originDis = getEuclideanDistance(v, { x: 0, y: 0 }); const reciODis = originDis === 0 ? 0 : 1 / originDis; if (i === focusIndex) { return; } let xMolecule = 0; let yMolecule = 0; let denominator = 0; positions.forEach((u, j) => { // u if (i === j) { return; } // the euclidean distance between v and u const edis = getEuclideanDistance(v, u); const reciEdis = edis === 0 ? 0 : 1 / edis; const idealDis = distances[j][i]; // same for x and y denominator += weights[i][j]; // x xMolecule += weights[i][j] * (u.x + idealDis * (v.x - u.x) * reciEdis); // y yMolecule += weights[i][j] * (u.y + idealDis * (v.y - u.y) * reciEdis); }); const reciR = radii[i] === 0 ? 0 : 1 / radii[i]; denominator *= vparam; denominator += param * reciR * reciR; // x xMolecule *= vparam; xMolecule += param * reciR * v.x * reciODis; v.x = xMolecule / denominator; // y yMolecule *= vparam; yMolecule += param * reciR * v.y * reciODis; v.y = yMolecule / denominator; }); } } const eIdealDisMatrix = (nodes, distances, linkDistance, radii, unitRadius, sortBy, sortStrength) => { if (!nodes) return []; const result = []; if (distances) { // cache the value of field sortBy for nodes to avoid dupliate calculation const sortValueCache = {}; distances.forEach((row, i) => { const newRow = []; row.forEach((v, j) => { var _a, _b; if (i === j) { newRow.push(0); } else if (radii[i] === radii[j]) { // i and j are on the same circle if (sortBy === 'data') { // sort the nodes on the same circle according to the ordering of the data newRow.push((v * (Math.abs(i - j) * sortStrength)) / (radii[i] / unitRadius)); } else if (sortBy) { // sort the nodes on the same circle according to the attributes let iValue; let jValue; if (sortValueCache[nodes[i].id]) { iValue = sortValueCache[nodes[i].id]; } else { const value = (sortBy === 'id' ? nodes[i].id : (_a = nodes[i].data) === null || _a === void 0 ? void 0 : _a[sortBy]) || 0; if (isString(value)) { iValue = value.charCodeAt(0); } else { iValue = value; } sortValueCache[nodes[i].id] = iValue; } if (sortValueCache[nodes[j].id]) { jValue = sortValueCache[nodes[j].id]; } else { const value = (sortBy === 'id' ? nodes[j].id : (_b = nodes[j].data) === null || _b === void 0 ? void 0 : _b[sortBy]) || 0; if (isString(value)) { jValue = value.charCodeAt(0); } else { jValue = value; } sortValueCache[nodes[j].id] = jValue; } newRow.push((v * (Math.abs(iValue - jValue) * sortStrength)) / (radii[i] / unitRadius)); } else { newRow.push((v * linkDistance) / (radii[i] / unitRadius)); } } else { // i and j are on different circles const link = (linkDistance + unitRadius) / 2; newRow.push(v * link); } }); result.push(newRow); }); } return result; }; const getWeightMatrix = (idealDistances) => { const rows = idealDistances.length; const cols = idealDistances[0].length; const result = []; for (let i = 0; i < rows; i++) { const row = []; for (let j = 0; j < cols; j++) { if (idealDistances[i][j] !== 0) { row.push(1 / (idealDistances[i][j] * idealDistances[i][j])); } else { row.push(0); } } result.push(row); } return result; }; const getIndexById = (array, id) => { let index = -1; array.forEach((a, i) => { if (a.id === id) { index = i; } }); return Math.max(index, 0); }; const handleInfinity = (matrix, focusIndex, step) => { const length = matrix.length; // 遍历 matrix 中遍历 focus 对应行 for (let i = 0; i < length; i++) { // matrix 关注点对应行的 Inf 项 if (matrix[focusIndex][i] === Infinity) { matrix[focusIndex][i] = step; matrix[i][focusIndex] = step; // 遍历 matrix 中的 i 行,i 行中非 Inf 项若在 focus 行为 Inf,则替换 focus 行的那个 Inf for (let j = 0; j < length; j++) { if (matrix[i][j] !== Infinity && matrix[focusIndex][j] === Infinity) { matrix[focusIndex][j] = step + matrix[i][j]; matrix[j][focusIndex] = step + matrix[i][j]; } } } } // 处理其他行的 Inf。根据该行对应点与 focus 距离以及 Inf 项点 与 focus 距离,决定替换值 for (let i = 0; i < length; i++) { if (i === focusIndex) { continue; } for (let j = 0; j < length; j++) { if (matrix[i][j] === Infinity) { let minus = Math.abs(matrix[focusIndex][i] - matrix[focusIndex][j]); minus = minus === 0 ? 1 : minus; matrix[i][j] = minus; } } } }; const maxToFocus = (matrix, focusIndex) => { let max = 0; for (let i = 0; i < matrix[focusIndex].length; i++) { if (matrix[focusIndex][i] === Infinity) { continue; } max = matrix[focusIndex][i] > max ? matrix[focusIndex][i] : max; } return max; }; //# sourceMappingURL=index.js.map