collab/mermaid/packages/mermaid-layout-tidy-tree/src/render.tsblame
View source
6dd74de1import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
6dd74de2import { executeTidyTreeLayout } from './layout.js';
6dd74de3
6dd74de4interface NodeWithPosition {
6dd74de5 id: string;
6dd74de6 x?: number;
6dd74de7 y?: number;
6dd74de8 width?: number;
6dd74de9 height?: number;
6dd74de10 domId?: any;
6dd74de11 [key: string]: any;
6dd74de12}
6dd74de13
6dd74de14/**
6dd74de15 * Render function for bidirectional tidy-tree layout algorithm
6dd74de16 *
6dd74de17 * This follows the same pattern as ELK and dagre renderers:
6dd74de18 * 1. Insert nodes into DOM to get their actual dimensions
6dd74de19 * 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
6dd74de20 * 3. Position the nodes and edges based on layout results
6dd74de21 *
6dd74de22 * The bidirectional layout creates two trees that grow horizontally in opposite
6dd74de23 * directions from a central root node:
6dd74de24 * - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
6dd74de25 * - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
6dd74de26 */
6dd74de27export const render = async (
6dd74de28 data4Layout: LayoutData,
6dd74de29 svg: SVG,
6dd74de30 {
6dd74de31 insertCluster,
6dd74de32 insertEdge,
6dd74de33 insertEdgeLabel,
6dd74de34 insertMarkers,
6dd74de35 insertNode,
6dd74de36 log,
6dd74de37 positionEdgeLabel,
6dd74de38 }: InternalHelpers,
6dd74de39 { algorithm: _algorithm }: RenderOptions
6dd74de40) => {
6dd74de41 const nodeDb: Record<string, NodeWithPosition> = {};
6dd74de42 const clusterDb: Record<string, any> = {};
6dd74de43
6dd74de44 const element = svg.select('g');
6dd74de45 insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
6dd74de46
6dd74de47 const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
6dd74de48 const edgePaths = element.insert('g').attr('class', 'edgePaths');
6dd74de49 const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
6dd74de50 const nodes = element.insert('g').attr('class', 'nodes');
6dd74de51 // Step 1: Insert nodes into DOM to get their actual dimensions
6dd74de52 log.debug('Inserting nodes into DOM for dimension calculation');
6dd74de53
6dd74de54 await Promise.all(
6dd74de55 data4Layout.nodes.map(async (node) => {
6dd74de56 if (node.isGroup) {
6dd74de57 const clusterNode: NodeWithPosition = {
6dd74de58 ...node,
6dd74de59 id: node.id,
6dd74de60 width: node.width,
6dd74de61 height: node.height,
6dd74de62 };
6dd74de63 clusterDb[node.id] = clusterNode;
6dd74de64 nodeDb[node.id] = clusterNode;
6dd74de65
6dd74de66 await insertCluster(subGraphsEl, node);
6dd74de67 } else {
6dd74de68 const nodeWithPosition: NodeWithPosition = {
6dd74de69 ...node,
6dd74de70 id: node.id,
6dd74de71 width: node.width,
6dd74de72 height: node.height,
6dd74de73 };
6dd74de74 nodeDb[node.id] = nodeWithPosition;
6dd74de75
6dd74de76 const nodeEl = await insertNode(nodes, node, {
6dd74de77 config: data4Layout.config,
6dd74de78 dir: data4Layout.direction || 'TB',
6dd74de79 });
6dd74de80
6dd74de81 const boundingBox = nodeEl.node()!.getBBox();
6dd74de82 nodeWithPosition.width = boundingBox.width;
6dd74de83 nodeWithPosition.height = boundingBox.height;
6dd74de84 nodeWithPosition.domId = nodeEl;
6dd74de85
6dd74de86 log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
6dd74de87 }
6dd74de88 })
6dd74de89 );
6dd74de90 // Step 2: Run the bidirectional tidy-tree layout algorithm
6dd74de91 log.debug('Running bidirectional tidy-tree layout algorithm');
6dd74de92
6dd74de93 const updatedLayoutData = {
6dd74de94 ...data4Layout,
6dd74de95 nodes: data4Layout.nodes.map((node) => {
6dd74de96 const nodeWithDimensions = nodeDb[node.id];
6dd74de97 return {
6dd74de98 ...node,
6dd74de99 width: nodeWithDimensions.width ?? node.width ?? 100,
6dd74de100 height: nodeWithDimensions.height ?? node.height ?? 50,
6dd74de101 };
6dd74de102 }),
6dd74de103 };
6dd74de104
6dd74de105 const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
6dd74de106 // Step 3: Position the nodes based on bidirectional layout results
6dd74de107 log.debug('Positioning nodes based on bidirectional layout results');
6dd74de108
6dd74de109 layoutResult.nodes.forEach((positionedNode) => {
6dd74de110 const node = nodeDb[positionedNode.id];
6dd74de111 if (node?.domId) {
6dd74de112 // Position the node at the calculated coordinates from bidirectional layout
6dd74de113 // The layout algorithm has already calculated positions for:
6dd74de114 // - Root node at center (0, 0)
6dd74de115 // - Left tree nodes with negative x coordinates (growing left)
6dd74de116 // - Right tree nodes with positive x coordinates (growing right)
6dd74de117 node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
6dd74de118 // Store the final position
6dd74de119 node.x = positionedNode.x;
6dd74de120 node.y = positionedNode.y;
6dd74de121 // Step 3: Position the nodes based on bidirectional layout results
6dd74de122 log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
6dd74de123 }
6dd74de124 });
6dd74de125
6dd74de126 log.debug('Inserting and positioning edges');
6dd74de127
6dd74de128 await Promise.all(
6dd74de129 data4Layout.edges.map(async (edge) => {
6dd74de130 await insertEdgeLabel(edgeLabels, edge);
6dd74de131
6dd74de132 const startNode = nodeDb[edge.start ?? ''];
6dd74de133 const endNode = nodeDb[edge.end ?? ''];
6dd74de134
6dd74de135 if (startNode && endNode) {
6dd74de136 const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
6dd74de137
6dd74de138 if (positionedEdge) {
6dd74de139 log.debug('APA01 positionedEdge', positionedEdge);
6dd74de140 const edgeWithPath = {
6dd74de141 ...edge,
6dd74de142 points: positionedEdge.points,
6dd74de143 };
6dd74de144 const paths = insertEdge(
6dd74de145 edgePaths,
6dd74de146 edgeWithPath,
6dd74de147 clusterDb,
6dd74de148 data4Layout.type,
6dd74de149 startNode,
6dd74de150 endNode,
6dd74de151 data4Layout.diagramId
6dd74de152 );
6dd74de153
6dd74de154 positionEdgeLabel(edgeWithPath, paths);
6dd74de155 } else {
6dd74de156 const edgeWithPath = {
6dd74de157 ...edge,
6dd74de158 points: [
6dd74de159 { x: startNode.x ?? 0, y: startNode.y ?? 0 },
6dd74de160 { x: endNode.x ?? 0, y: endNode.y ?? 0 },
6dd74de161 ],
6dd74de162 };
6dd74de163
6dd74de164 const paths = insertEdge(
6dd74de165 edgePaths,
6dd74de166 edgeWithPath,
6dd74de167 clusterDb,
6dd74de168 data4Layout.type,
6dd74de169 startNode,
6dd74de170 endNode,
6dd74de171 data4Layout.diagramId
6dd74de172 );
6dd74de173 positionEdgeLabel(edgeWithPath, paths);
6dd74de174 }
6dd74de175 }
6dd74de176 })
6dd74de177 );
6dd74de178
6dd74de179 log.debug('Bidirectional tidy-tree rendering completed');
6dd74de180};