mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-09 02:27:05 +02: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');
|
|
};
|