5.7 KB181 lines
Blame
1import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
2import { executeTidyTreeLayout } from './layout.js';
3
4interface NodeWithPosition {
5 id: string;
6 x?: number;
7 y?: number;
8 width?: number;
9 height?: number;
10 domId?: any;
11 [key: string]: any;
12}
13
14/**
15 * Render function for bidirectional tidy-tree layout algorithm
16 *
17 * This follows the same pattern as ELK and dagre renderers:
18 * 1. Insert nodes into DOM to get their actual dimensions
19 * 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
20 * 3. Position the nodes and edges based on layout results
21 *
22 * The bidirectional layout creates two trees that grow horizontally in opposite
23 * directions from a central root node:
24 * - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
25 * - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
26 */
27export const render = async (
28 data4Layout: LayoutData,
29 svg: SVG,
30 {
31 insertCluster,
32 insertEdge,
33 insertEdgeLabel,
34 insertMarkers,
35 insertNode,
36 log,
37 positionEdgeLabel,
38 }: InternalHelpers,
39 { algorithm: _algorithm }: RenderOptions
40) => {
41 const nodeDb: Record<string, NodeWithPosition> = {};
42 const clusterDb: Record<string, any> = {};
43
44 const element = svg.select('g');
45 insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
46
47 const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
48 const edgePaths = element.insert('g').attr('class', 'edgePaths');
49 const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
50 const nodes = element.insert('g').attr('class', 'nodes');
51 // Step 1: Insert nodes into DOM to get their actual dimensions
52 log.debug('Inserting nodes into DOM for dimension calculation');
53
54 await Promise.all(
55 data4Layout.nodes.map(async (node) => {
56 if (node.isGroup) {
57 const clusterNode: NodeWithPosition = {
58 ...node,
59 id: node.id,
60 width: node.width,
61 height: node.height,
62 };
63 clusterDb[node.id] = clusterNode;
64 nodeDb[node.id] = clusterNode;
65
66 await insertCluster(subGraphsEl, node);
67 } else {
68 const nodeWithPosition: NodeWithPosition = {
69 ...node,
70 id: node.id,
71 width: node.width,
72 height: node.height,
73 };
74 nodeDb[node.id] = nodeWithPosition;
75
76 const nodeEl = await insertNode(nodes, node, {
77 config: data4Layout.config,
78 dir: data4Layout.direction || 'TB',
79 });
80
81 const boundingBox = nodeEl.node()!.getBBox();
82 nodeWithPosition.width = boundingBox.width;
83 nodeWithPosition.height = boundingBox.height;
84 nodeWithPosition.domId = nodeEl;
85
86 log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
87 }
88 })
89 );
90 // Step 2: Run the bidirectional tidy-tree layout algorithm
91 log.debug('Running bidirectional tidy-tree layout algorithm');
92
93 const updatedLayoutData = {
94 ...data4Layout,
95 nodes: data4Layout.nodes.map((node) => {
96 const nodeWithDimensions = nodeDb[node.id];
97 return {
98 ...node,
99 width: nodeWithDimensions.width ?? node.width ?? 100,
100 height: nodeWithDimensions.height ?? node.height ?? 50,
101 };
102 }),
103 };
104
105 const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
106 // Step 3: Position the nodes based on bidirectional layout results
107 log.debug('Positioning nodes based on bidirectional layout results');
108
109 layoutResult.nodes.forEach((positionedNode) => {
110 const node = nodeDb[positionedNode.id];
111 if (node?.domId) {
112 // Position the node at the calculated coordinates from bidirectional layout
113 // The layout algorithm has already calculated positions for:
114 // - Root node at center (0, 0)
115 // - Left tree nodes with negative x coordinates (growing left)
116 // - Right tree nodes with positive x coordinates (growing right)
117 node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
118 // Store the final position
119 node.x = positionedNode.x;
120 node.y = positionedNode.y;
121 // Step 3: Position the nodes based on bidirectional layout results
122 log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
123 }
124 });
125
126 log.debug('Inserting and positioning edges');
127
128 await Promise.all(
129 data4Layout.edges.map(async (edge) => {
130 await insertEdgeLabel(edgeLabels, edge);
131
132 const startNode = nodeDb[edge.start ?? ''];
133 const endNode = nodeDb[edge.end ?? ''];
134
135 if (startNode && endNode) {
136 const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
137
138 if (positionedEdge) {
139 log.debug('APA01 positionedEdge', positionedEdge);
140 const edgeWithPath = {
141 ...edge,
142 points: positionedEdge.points,
143 };
144 const paths = insertEdge(
145 edgePaths,
146 edgeWithPath,
147 clusterDb,
148 data4Layout.type,
149 startNode,
150 endNode,
151 data4Layout.diagramId
152 );
153
154 positionEdgeLabel(edgeWithPath, paths);
155 } else {
156 const edgeWithPath = {
157 ...edge,
158 points: [
159 { x: startNode.x ?? 0, y: startNode.y ?? 0 },
160 { x: endNode.x ?? 0, y: endNode.y ?? 0 },
161 ],
162 };
163
164 const paths = insertEdge(
165 edgePaths,
166 edgeWithPath,
167 clusterDb,
168 data4Layout.type,
169 startNode,
170 endNode,
171 data4Layout.diagramId
172 );
173 positionEdgeLabel(edgeWithPath, paths);
174 }
175 }
176 })
177 );
178
179 log.debug('Bidirectional tidy-tree rendering completed');
180};
181