import { isNil } from '@antv/util'; import type { ID } from '../../types'; import { EdgeData, NodeData, PointObject } from '../../types'; import { run as runAcyclic, undo as undoAcyclic } from './acyclic'; import { addBorderSegments } from './add-border-segments'; import { adjust as adjustCoordinateSystem, undo as undoCoordinateSystem, } from './coordinate-system'; import { DagreGraph, GraphEdge } from './graph'; import { cleanup as cleanupNestingGraph, run as runNestingGraph, } from './nesting-graph'; import { run as runNormalize, undo as undoNormalize } from './normalize'; import { order } from './order'; import { initDataOrder } from './order/init-data-order'; import { parentDummyChains } from './parent-dummy-chains'; import { position } from './position'; import { rank } from './rank'; import type { DagreAlign, DagreRankdir } from './types'; import { addDummyNode, asNonCompoundGraph, buildLayerMatrix, intersectRect, normalizeRanks, removeEmptyRanks, } from './util'; // const graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"]; // const graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" }; // const graphAttrs = ["acyclicer", "ranker", "rankdir", "align"]; export const layout = ( g: DagreGraph, options: { keepNodeOrder: boolean; prevGraph: DagreGraph | null; edgeLabelSpace?: boolean; align?: DagreAlign; nodesep?: number; edgesep?: number; ranksep?: number; acyclicer: string; nodeOrder: ID[]; ranker: 'network-simplex' | 'tight-tree' | 'longest-path'; rankdir: DagreRankdir; }, ) => { const { edgeLabelSpace, keepNodeOrder, prevGraph, rankdir, ranksep } = options; // 如果在原图基础上修改,继承原图的order结果 if (!keepNodeOrder && prevGraph) { inheritOrder(g, prevGraph); } const layoutGraph = buildLayoutGraph(g); // 控制是否为边的label留位置(这会影响是否在边中间添加dummy node) if (!!edgeLabelSpace) { options.ranksep = makeSpaceForEdgeLabels(layoutGraph, { rankdir, ranksep, }); } let dimension; // TODO: 暂时处理层级设置不正确时的异常报错,提示设置正确的层级 try { dimension = runLayout(layoutGraph, options); } catch (e: any) { if ( e.message === 'Not possible to find intersection inside of the rectangle' ) { console.error( "The following error may be caused by improper layer setting, please make sure your manual layer setting does not violate the graph's structure:\n", e, ); return; } throw e; } updateInputGraph(g, layoutGraph); return dimension; }; const runLayout = ( g: DagreGraph, options: { acyclicer: string; keepNodeOrder: boolean; nodeOrder: ID[]; ranker: 'network-simplex' | 'tight-tree' | 'longest-path'; rankdir: DagreRankdir; align?: DagreAlign; nodesep?: number; edgesep?: number; ranksep?: number; }, ) => { const { acyclicer, ranker, rankdir = 'tb', nodeOrder, keepNodeOrder, align, nodesep = 50, edgesep = 20, ranksep = 50, } = options; removeSelfEdges(g); runAcyclic(g, acyclicer); const { nestingRoot, nodeRankFactor } = runNestingGraph(g); rank(asNonCompoundGraph(g), ranker); injectEdgeLabelProxies(g); removeEmptyRanks(g, nodeRankFactor); cleanupNestingGraph(g, nestingRoot); normalizeRanks(g); assignRankMinMax(g); removeEdgeLabelProxies(g); const dummyChains: ID[] = []; runNormalize(g, dummyChains); parentDummyChains(g, dummyChains); addBorderSegments(g); if (keepNodeOrder) { initDataOrder(g, nodeOrder); } order(g, keepNodeOrder); insertSelfEdges(g); adjustCoordinateSystem(g, rankdir); position(g, { align, nodesep, edgesep, ranksep, }); positionSelfEdges(g); removeBorderNodes(g); undoNormalize(g, dummyChains); fixupEdgeLabelCoords(g); undoCoordinateSystem(g, rankdir); const { width, height } = translateGraph(g); assignNodeIntersects(g); reversePointsForReversedEdges(g); undoAcyclic(g); return { width, height }; }; /** * 继承上一个布局中的order,防止翻转 * TODO: 暂时没有考虑涉及层级变动的布局,只保证原来布局层级和相对顺序不变 */ const inheritOrder = (currG: DagreGraph, prevG: DagreGraph) => { currG.getAllNodes().forEach((n) => { const node = currG.getNode(n.id)!; if (prevG.hasNode(n.id)) { const prevNode = prevG.getNode(n.id)!; node.data.fixorder = prevNode.data._order as number; delete prevNode.data._order; } else { delete node.data.fixorder; } }); }; /* * Copies final layout information from the layout graph back to the input * graph. This process only copies whitelisted attributes from the layout graph * to the input graph, so it serves as a good place to determine what * attributes can influence layout. */ const updateInputGraph = (inputGraph: DagreGraph, layoutGraph: DagreGraph) => { inputGraph.getAllNodes().forEach((v) => { const inputLabel = inputGraph.getNode(v.id); if (inputLabel) { const layoutLabel = layoutGraph.getNode(v.id)!; inputLabel.data.x = layoutLabel.data.x; inputLabel.data.y = layoutLabel.data.y; inputLabel.data._order = layoutLabel.data.order; inputLabel.data._rank = layoutLabel.data.rank; if (layoutGraph.getChildren(v.id)?.length) { inputLabel.data.width = layoutLabel.data.width; inputLabel.data.height = layoutLabel.data.height; } } }); inputGraph.getAllEdges().forEach((e) => { const inputLabel = inputGraph.getEdge(e.id)!; const layoutLabel = layoutGraph.getEdge(e.id)!; inputLabel.data.points = layoutLabel ? layoutLabel.data.points : []; if (layoutLabel && layoutLabel.data.hasOwnProperty('x')) { inputLabel.data.x = layoutLabel.data.x; inputLabel.data.y = layoutLabel.data.y; } }); // inputGraph.graph().width = layoutGraph.graph().width; // inputGraph.graph().height = layoutGraph.graph().height; }; const nodeNumAttrs = ['width', 'height', 'layer', 'fixorder']; // 需要传入layer, fixOrder作为参数参考 const nodeDefaults = { width: 0, height: 0 }; const edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset']; const edgeDefaults = { minlen: 1, weight: 1, width: 0, height: 0, labeloffset: 10, labelpos: 'r', }; const edgeAttrs = ['labelpos']; /* * Constructs a new graph from the input graph, which can be used for layout. * This process copies only whitelisted attributes from the input graph to the * layout graph. Thus this function serves as a good place to determine what * attributes can influence layout. */ const buildLayoutGraph = (inputGraph: DagreGraph) => { const g = new DagreGraph({ tree: [] }); inputGraph.getAllNodes().forEach((v) => { const node = canonicalize(inputGraph.getNode(v.id).data); const defaultNode = { ...nodeDefaults, ...node, } as NodeData; const defaultAttrs = selectNumberAttrs(defaultNode, nodeNumAttrs) as Node; if (!g.hasNode(v.id)) { g.addNode({ id: v.id, data: { ...defaultAttrs, }, }); } const parent = inputGraph.hasTreeStructure('combo') ? inputGraph.getParent(v.id, 'combo') : inputGraph.getParent(v.id); if (!isNil(parent)) { if (!g.hasNode(parent.id)) { g.addNode({ ...parent }); } g.setParent(v.id, parent.id); } }); inputGraph.getAllEdges().forEach((e) => { const edge = canonicalize(inputGraph.getEdge(e.id).data); const pickedProperties: any = {}; edgeAttrs?.forEach((key) => { if (edge[key] !== undefined) pickedProperties[key] = edge[key]; }); g.addEdge({ id: e.id, source: e.source, target: e.target, data: Object.assign( {}, edgeDefaults, selectNumberAttrs(edge, edgeNumAttrs), pickedProperties, ), }); }); return g; }; /* * This idea comes from the Gansner paper: to account for edge labels in our * layout we split each rank in half by doubling minlen and halving ranksep. * Then we can place labels at these mid-points between nodes. * * We also add some minimal padding to the width to push the label for the edge * away from the edge itself a bit. */ const makeSpaceForEdgeLabels = ( g: DagreGraph, options: { ranksep?: number; rankdir: string; }, ) => { const { ranksep = 0, rankdir } = options; g.getAllNodes().forEach((node) => { if (!isNaN(node.data.layer!)) { if (!node.data.layer) node.data.layer = 0; } }); g.getAllEdges().forEach((edge) => { edge.data.minlen! *= 2; if ((edge.data.labelpos as string)?.toLowerCase() !== 'c') { if (rankdir === 'TB' || rankdir === 'BT') { edge.data.width! += edge.data.labeloffset!; } else { edge.data.height! += edge.data.labeloffset!; } } }); return ranksep / 2; }; /* * Creates temporary dummy nodes that capture the rank in which each edge's * label is going to, if it has one of non-zero width and height. We do this * so that we can safely remove empty ranks while preserving balance for the * label's position. */ const injectEdgeLabelProxies = (g: DagreGraph) => { g.getAllEdges().forEach((e) => { if (e.data.width && e.data.height) { const v = g.getNode(e.source)!; const w = g.getNode(e.target)!; const label = { e, rank: (w.data.rank! - v.data.rank!) / 2 + v.data.rank!, }; addDummyNode(g, 'edge-proxy', label, '_ep'); } }); }; const assignRankMinMax = (g: DagreGraph): number => { let maxRank = 0; g.getAllNodes().forEach((node) => { if (node.data.borderTop) { node.data.minRank = g.getNode(node.data.borderTop as ID)?.data.rank; node.data.maxRank = g.getNode(node.data.borderBottom as ID)?.data.rank; maxRank = Math.max(maxRank, node.data.maxRank || -Infinity); } }); return maxRank; }; const removeEdgeLabelProxies = (g: DagreGraph) => { g.getAllNodes().forEach((node) => { if (node.data.dummy === 'edge-proxy') { g.getEdge(node.data.e!.id)!.data.labelRank = node.data.rank; g.removeNode(node.id); } }); }; const translateGraph = ( g: DagreGraph, options?: { marginx: number; marginy: number; }, ) => { let minX: number; let maxX = 0; let minY: number; let maxY = 0; const { marginx: marginX = 0, marginy: marginY = 0 } = options || {}; const getExtremes = (attrs: any) => { if (!attrs.data) return; const x = attrs.data.x; const y = attrs.data.y; const w = attrs.data.width; const h = attrs.data.height; if (!isNaN(x) && !isNaN(w)) { if (minX === undefined) { minX = x - w / 2; } minX = Math.min(minX, x - w / 2); maxX = Math.max(maxX, x + w / 2); } if (!isNaN(y) && !isNaN(h)) { if (minY === undefined) { minY = y - h / 2; } minY = Math.min(minY, y - h / 2); maxY = Math.max(maxY, y + h / 2); } }; g.getAllNodes().forEach((v) => { getExtremes(v); }); g.getAllEdges().forEach((e) => { if (e?.data.hasOwnProperty('x')) { getExtremes(e); } }); minX! -= marginX; minY! -= marginY; g.getAllNodes().forEach((node) => { node.data.x! -= minX; node.data.y! -= minY; }); g.getAllEdges().forEach((edge) => { edge.data.points?.forEach((p: PointObject) => { p.x -= minX; p.y -= minY; }); if (edge.data.hasOwnProperty('x')) { edge.data.x! -= minX; } if (edge.data.hasOwnProperty('y')) { edge.data.y! -= minY; } }); return { width: maxX - minX! + marginX, height: maxY - minY! + marginY, }; }; const assignNodeIntersects = (g: DagreGraph) => { g.getAllEdges().forEach((e) => { const nodeV = g.getNode(e.source)!; const nodeW = g.getNode(e.target)!; let p1: PointObject; let p2: PointObject; if (!e.data.points) { e.data.points = []; p1 = { x: nodeW.data.x!, y: nodeW.data.y! }; p2 = { x: nodeV.data.x!, y: nodeV.data.y! }; } else { p1 = e.data.points[0]; p2 = e.data.points[e.data.points.length - 1]; } e.data.points.unshift(intersectRect(nodeV.data, p1)); e.data.points.push(intersectRect(nodeW.data, p2)); }); }; const fixupEdgeLabelCoords = (g: DagreGraph) => { g.getAllEdges().forEach((edge) => { if (edge.data.hasOwnProperty('x')) { if (edge.data.labelpos === 'l' || edge.data.labelpos === 'r') { edge.data.width! -= edge.data.labeloffset!; } switch (edge.data.labelpos) { case 'l': edge.data.x! -= edge.data.width! / 2 + edge.data.labeloffset!; break; case 'r': edge.data.x! += edge.data.width! / 2 + edge.data.labeloffset!; break; } } }); }; const reversePointsForReversedEdges = (g: DagreGraph) => { g.getAllEdges().forEach((edge) => { if (edge.data.reversed) { edge.data.points?.reverse(); } }); }; const removeBorderNodes = (g: DagreGraph) => { g.getAllNodes().forEach((v) => { if (g.getChildren(v.id)?.length) { const node = g.getNode(v.id)!; const t = g.getNode(node.data.borderTop as ID); const b = g.getNode(node.data.borderBottom as ID); const l = g.getNode( (node.data.borderLeft as ID[])[ (node.data.borderLeft as ID[])?.length - 1 ], ); const r = g.getNode( (node.data.borderRight as ID[])[ (node.data.borderRight as ID[])?.length - 1 ], ); node.data.width = Math.abs(r?.data.x! - l?.data.x!) || 10; node.data.height = Math.abs(b?.data.y! - t?.data.y!) || 10; node.data.x = (l?.data.x! || 0) + node.data.width! / 2; node.data.y = (t?.data.y! || 0) + node.data.height! / 2; } }); g.getAllNodes().forEach((n) => { if (n.data.dummy === 'border') { g.removeNode(n.id); } }); }; const removeSelfEdges = (g: DagreGraph) => { g.getAllEdges().forEach((e) => { if (e.source === e.target) { const node = g.getNode(e.source)!; if (!node.data.selfEdges) { node.data.selfEdges = []; } node.data.selfEdges.push(e); g.removeEdge(e.id); } }); }; const insertSelfEdges = (g: DagreGraph) => { const layers = buildLayerMatrix(g); layers?.forEach((layer: ID[]) => { let orderShift = 0; layer?.forEach((v: ID, i: number) => { const node = g.getNode(v)!; node.data.order = i + orderShift; node.data.selfEdges?.forEach((selfEdge: GraphEdge) => { addDummyNode( g, 'selfedge', { width: selfEdge.data.width, height: selfEdge.data.height, rank: node.data.rank, order: i + ++orderShift, e: selfEdge, }, '_se', ); }); delete node.data.selfEdges; }); }); }; const positionSelfEdges = (g: DagreGraph) => { g.getAllNodes().forEach((v) => { const node = g.getNode(v.id)!; if (node.data.dummy === 'selfedge') { const selfNode = g.getNode(node.data.e!.source)!; const x = selfNode.data.x! + selfNode.data.width! / 2; const y = selfNode.data.y!; const dx = node.data.x! - x; const dy = selfNode.data.height! / 2; if (g.hasEdge(node.data.e!.id)) { g.updateEdgeData(node.data.e!.id, node.data.e!.data); } else { g.addEdge({ id: node.data.e!.id, source: node.data.e!.source, target: node.data.e!.target, data: node.data.e!.data, }); } g.removeNode(v.id); node.data.e!.data.points = [ { x: x + (2 * dx) / 3, y: y - dy }, { x: x + (5 * dx) / 6, y: y - dy }, { y, x: x + dx }, { x: x + (5 * dx) / 6, y: y + dy }, { x: x + (2 * dx) / 3, y: y + dy }, ]; node.data.e!.data.x = node.data.x; node.data.e!.data.y = node.data.y; } }); }; const selectNumberAttrs = (obj: Record, attrs: string[]) => { const pickedProperties: Record = {}; attrs?.forEach((key: string) => { if (obj[key] === undefined) return; pickedProperties[key] = +obj[key]; }); return pickedProperties; }; const canonicalize = (attrs: Record = {}) => { const newAttrs: Record = {}; Object.keys(attrs).forEach((k: string) => { newAttrs[k.toLowerCase()] = attrs[k]; }); return newAttrs; };