mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 02:44:17 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			181 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			181 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
 | |
| import { executeTidyTreeLayout } from './layout.js';
 | |
| 
 | |
| interface NodeWithPosition {
 | |
|   id: string;
 | |
|   x?: number;
 | |
|   y?: number;
 | |
|   width?: number;
 | |
|   height?: number;
 | |
|   domId?: any;
 | |
|   [key: string]: any;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Render function for bidirectional tidy-tree layout algorithm
 | |
|  *
 | |
|  * This follows the same pattern as ELK and dagre renderers:
 | |
|  * 1. Insert nodes into DOM to get their actual dimensions
 | |
|  * 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
 | |
|  * 3. Position the nodes and edges based on layout results
 | |
|  *
 | |
|  * The bidirectional layout creates two trees that grow horizontally in opposite
 | |
|  * directions from a central root node:
 | |
|  * - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
 | |
|  * - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
 | |
|  */
 | |
| export const render = async (
 | |
|   data4Layout: LayoutData,
 | |
|   svg: SVG,
 | |
|   {
 | |
|     insertCluster,
 | |
|     insertEdge,
 | |
|     insertEdgeLabel,
 | |
|     insertMarkers,
 | |
|     insertNode,
 | |
|     log,
 | |
|     positionEdgeLabel,
 | |
|   }: InternalHelpers,
 | |
|   { algorithm: _algorithm }: RenderOptions
 | |
| ) => {
 | |
|   const nodeDb: Record<string, NodeWithPosition> = {};
 | |
|   const clusterDb: Record<string, any> = {};
 | |
| 
 | |
|   const element = svg.select('g');
 | |
|   insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
 | |
| 
 | |
|   const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
 | |
|   const edgePaths = element.insert('g').attr('class', 'edgePaths');
 | |
|   const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
 | |
|   const nodes = element.insert('g').attr('class', 'nodes');
 | |
|   // Step 1: Insert nodes into DOM to get their actual dimensions
 | |
|   log.debug('Inserting nodes into DOM for dimension calculation');
 | |
| 
 | |
|   await Promise.all(
 | |
|     data4Layout.nodes.map(async (node) => {
 | |
|       if (node.isGroup) {
 | |
|         const clusterNode: NodeWithPosition = {
 | |
|           ...node,
 | |
|           id: node.id,
 | |
|           width: node.width,
 | |
|           height: node.height,
 | |
|         };
 | |
|         clusterDb[node.id] = clusterNode;
 | |
|         nodeDb[node.id] = clusterNode;
 | |
| 
 | |
|         await insertCluster(subGraphsEl, node);
 | |
|       } else {
 | |
|         const nodeWithPosition: NodeWithPosition = {
 | |
|           ...node,
 | |
|           id: node.id,
 | |
|           width: node.width,
 | |
|           height: node.height,
 | |
|         };
 | |
|         nodeDb[node.id] = nodeWithPosition;
 | |
| 
 | |
|         const nodeEl = await insertNode(nodes, node, {
 | |
|           config: data4Layout.config,
 | |
|           dir: data4Layout.direction || 'TB',
 | |
|         });
 | |
| 
 | |
|         const boundingBox = nodeEl.node()!.getBBox();
 | |
|         nodeWithPosition.width = boundingBox.width;
 | |
|         nodeWithPosition.height = boundingBox.height;
 | |
|         nodeWithPosition.domId = nodeEl;
 | |
| 
 | |
|         log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
 | |
|       }
 | |
|     })
 | |
|   );
 | |
|   // Step 2: Run the bidirectional tidy-tree layout algorithm
 | |
|   log.debug('Running bidirectional tidy-tree layout algorithm');
 | |
| 
 | |
|   const updatedLayoutData = {
 | |
|     ...data4Layout,
 | |
|     nodes: data4Layout.nodes.map((node) => {
 | |
|       const nodeWithDimensions = nodeDb[node.id];
 | |
|       return {
 | |
|         ...node,
 | |
|         width: nodeWithDimensions.width ?? node.width ?? 100,
 | |
|         height: nodeWithDimensions.height ?? node.height ?? 50,
 | |
|       };
 | |
|     }),
 | |
|   };
 | |
| 
 | |
|   const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
 | |
|   // Step 3: Position the nodes based on bidirectional layout results
 | |
|   log.debug('Positioning nodes based on bidirectional layout results');
 | |
| 
 | |
|   layoutResult.nodes.forEach((positionedNode) => {
 | |
|     const node = nodeDb[positionedNode.id];
 | |
|     if (node?.domId) {
 | |
|       // Position the node at the calculated coordinates from bidirectional layout
 | |
|       // The layout algorithm has already calculated positions for:
 | |
|       // - Root node at center (0, 0)
 | |
|       // - Left tree nodes with negative x coordinates (growing left)
 | |
|       // - Right tree nodes with positive x coordinates (growing right)
 | |
|       node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
 | |
|       // Store the final position
 | |
|       node.x = positionedNode.x;
 | |
|       node.y = positionedNode.y;
 | |
|       // Step 3: Position the nodes based on bidirectional layout results
 | |
|       log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   log.debug('Inserting and positioning edges');
 | |
| 
 | |
|   await Promise.all(
 | |
|     data4Layout.edges.map(async (edge) => {
 | |
|       await insertEdgeLabel(edgeLabels, edge);
 | |
| 
 | |
|       const startNode = nodeDb[edge.start ?? ''];
 | |
|       const endNode = nodeDb[edge.end ?? ''];
 | |
| 
 | |
|       if (startNode && endNode) {
 | |
|         const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
 | |
| 
 | |
|         if (positionedEdge) {
 | |
|           log.debug('APA01 positionedEdge', positionedEdge);
 | |
|           const edgeWithPath = {
 | |
|             ...edge,
 | |
|             points: positionedEdge.points,
 | |
|           };
 | |
|           const paths = insertEdge(
 | |
|             edgePaths,
 | |
|             edgeWithPath,
 | |
|             clusterDb,
 | |
|             data4Layout.type,
 | |
|             startNode,
 | |
|             endNode,
 | |
|             data4Layout.diagramId
 | |
|           );
 | |
| 
 | |
|           positionEdgeLabel(edgeWithPath, paths);
 | |
|         } else {
 | |
|           const edgeWithPath = {
 | |
|             ...edge,
 | |
|             points: [
 | |
|               { x: startNode.x ?? 0, y: startNode.y ?? 0 },
 | |
|               { x: endNode.x ?? 0, y: endNode.y ?? 0 },
 | |
|             ],
 | |
|           };
 | |
| 
 | |
|           const paths = insertEdge(
 | |
|             edgePaths,
 | |
|             edgeWithPath,
 | |
|             clusterDb,
 | |
|             data4Layout.type,
 | |
|             startNode,
 | |
|             endNode,
 | |
|             data4Layout.diagramId
 | |
|           );
 | |
|           positionEdgeLabel(edgeWithPath, paths);
 | |
|         }
 | |
|       }
 | |
|     })
 | |
|   );
 | |
| 
 | |
|   log.debug('Bidirectional tidy-tree rendering completed');
 | |
| };
 | 
