mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 15:30:03 +02:00
Tidy-tree getting closer, now algorithm works
This commit is contained in:
@@ -114,11 +114,14 @@
|
|||||||
root((mindmap))
|
root((mindmap))
|
||||||
Origins
|
Origins
|
||||||
Tools
|
Tools
|
||||||
Pen and paper
|
e
|
||||||
Mermaid
|
|
||||||
Third
|
Third
|
||||||
|
Forth
|
||||||
|
a
|
||||||
|
b
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
|
@@ -83,7 +83,7 @@
|
|||||||
"@vitest/spy": "^3.0.6",
|
"@vitest/spy": "^3.0.6",
|
||||||
"@vitest/ui": "^3.0.6",
|
"@vitest/ui": "^3.0.6",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^3.6.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cpy-cli": "^5.0.0",
|
"cpy-cli": "^5.0.0",
|
||||||
|
@@ -106,7 +106,7 @@
|
|||||||
"@types/stylis": "^4.2.7",
|
"@types/stylis": "^4.2.7",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^3.6.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"csstree-validator": "^4.0.1",
|
"csstree-validator": "^4.0.1",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
|
@@ -47,12 +47,14 @@ export function executeTidyTreeLayout(
|
|||||||
let rightBoundingBox = null;
|
let rightBoundingBox = null;
|
||||||
|
|
||||||
if (leftTree) {
|
if (leftTree) {
|
||||||
|
console.log('combineAndPositionTrees leftTree', JSON.stringify(leftTree));
|
||||||
const leftLayoutResult = layout.layout(leftTree);
|
const leftLayoutResult = layout.layout(leftTree);
|
||||||
leftResult = leftLayoutResult.result;
|
leftResult = leftLayoutResult.result;
|
||||||
leftBoundingBox = leftLayoutResult.boundingBox;
|
leftBoundingBox = leftLayoutResult.boundingBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightTree) {
|
if (rightTree) {
|
||||||
|
console.log('combineAndPositionTrees leftTree', JSON.stringify(rightTree));
|
||||||
const rightLayoutResult = layout.layout(rightTree);
|
const rightLayoutResult = layout.layout(rightTree);
|
||||||
rightResult = rightLayoutResult.result;
|
rightResult = rightLayoutResult.result;
|
||||||
rightBoundingBox = rightLayoutResult.boundingBox;
|
rightBoundingBox = rightLayoutResult.boundingBox;
|
||||||
@@ -64,7 +66,8 @@ export function executeTidyTreeLayout(
|
|||||||
leftResult,
|
leftResult,
|
||||||
rightResult,
|
rightResult,
|
||||||
leftBoundingBox,
|
leftBoundingBox,
|
||||||
rightBoundingBox
|
rightBoundingBox,
|
||||||
|
data
|
||||||
);
|
);
|
||||||
const positionedEdges = calculateEdgePositions(data.edges, positionedNodes);
|
const positionedEdges = calculateEdgePositions(data.edges, positionedNodes);
|
||||||
|
|
||||||
@@ -167,6 +170,7 @@ function convertToDualTreeFormat(data: LayoutData): {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a subtree from a list of root children
|
* Build a subtree from a list of root children
|
||||||
|
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
|
||||||
*/
|
*/
|
||||||
function buildSubTree(
|
function buildSubTree(
|
||||||
rootChildren: string[],
|
rootChildren: string[],
|
||||||
@@ -181,7 +185,7 @@ function buildSubTree(
|
|||||||
children: rootChildren
|
children: rootChildren
|
||||||
.map((childId) => nodeMap.get(childId))
|
.map((childId) => nodeMap.get(childId))
|
||||||
.filter((child): child is Node => child !== undefined)
|
.filter((child): child is Node => child !== undefined)
|
||||||
.map((child) => convertNodeToTidyTree(child, children, nodeMap)),
|
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return virtualRoot;
|
return virtualRoot;
|
||||||
@@ -189,8 +193,9 @@ function buildSubTree(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively convert a node and its children to tidy-tree format
|
* Recursively convert a node and its children to tidy-tree format
|
||||||
|
* This version transposes width/height for horizontal tree layout
|
||||||
*/
|
*/
|
||||||
function convertNodeToTidyTree(
|
function convertNodeToTidyTreeTransposed(
|
||||||
node: Node,
|
node: Node,
|
||||||
children: Map<string, string[]>,
|
children: Map<string, string[]>,
|
||||||
nodeMap: Map<string, Node>
|
nodeMap: Map<string, Node>
|
||||||
@@ -199,12 +204,15 @@ function convertNodeToTidyTree(
|
|||||||
const childNodes = childIds
|
const childNodes = childIds
|
||||||
.map((childId) => nodeMap.get(childId))
|
.map((childId) => nodeMap.get(childId))
|
||||||
.filter((child): child is Node => child !== undefined)
|
.filter((child): child is Node => child !== undefined)
|
||||||
.map((child) => convertNodeToTidyTree(child, children, nodeMap));
|
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
|
||||||
|
|
||||||
|
// Transpose width and height for horizontal layout
|
||||||
|
// When tree grows horizontally, the original width becomes the height in the layout
|
||||||
|
// and the original height becomes the width in the layout
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
width: node.width || 100,
|
width: node.height || 50, // Original height becomes layout width
|
||||||
height: node.height || 50,
|
height: node.width || 100, // Original width becomes layout height
|
||||||
children: childNodes.length > 0 ? childNodes : undefined,
|
children: childNodes.length > 0 ? childNodes : undefined,
|
||||||
// Store original node data for later use
|
// Store original node data for later use
|
||||||
_originalNode: node,
|
_originalNode: node,
|
||||||
@@ -219,40 +227,88 @@ function combineAndPositionTrees(
|
|||||||
rootNode: TidyTreeNode,
|
rootNode: TidyTreeNode,
|
||||||
leftResult: TidyTreeNode | null,
|
leftResult: TidyTreeNode | null,
|
||||||
rightResult: TidyTreeNode | null,
|
rightResult: TidyTreeNode | null,
|
||||||
leftBoundingBox: any,
|
_leftBoundingBox: any,
|
||||||
rightBoundingBox: any
|
_rightBoundingBox: any,
|
||||||
|
_data: LayoutData
|
||||||
): PositionedNode[] {
|
): PositionedNode[] {
|
||||||
const positionedNodes: PositionedNode[] = [];
|
const positionedNodes: PositionedNode[] = [];
|
||||||
|
|
||||||
|
console.log('combineAndPositionTrees', {
|
||||||
|
leftResult,
|
||||||
|
rightResult,
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate root position (center of the layout)
|
// Calculate root position (center of the layout)
|
||||||
const rootX = 0;
|
const rootX = 0;
|
||||||
const rootY = 0;
|
const rootY = 0;
|
||||||
|
|
||||||
// Position the root node
|
|
||||||
positionedNodes.push({
|
|
||||||
id: rootNode.id,
|
|
||||||
x: rootX,
|
|
||||||
y: rootY,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate spacing between trees
|
// Calculate spacing between trees
|
||||||
const treeSpacing = 150; // Horizontal spacing from root to tree
|
const treeSpacing = 150; // Horizontal spacing from root to tree
|
||||||
|
|
||||||
|
// First, collect node positions for each tree separately
|
||||||
|
const leftTreeNodes: PositionedNode[] = [];
|
||||||
|
const rightTreeNodes: PositionedNode[] = [];
|
||||||
|
|
||||||
// Position left tree (grows to the left)
|
// Position left tree (grows to the left)
|
||||||
if (leftResult && leftResult.children) {
|
if (leftResult?.children) {
|
||||||
positionLeftTreeBidirectional(leftResult.children, positionedNodes, rootX - treeSpacing, rootY);
|
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position right tree (grows to the right)
|
// Position right tree (grows to the right)
|
||||||
if (rightResult && rightResult.children) {
|
if (rightResult?.children) {
|
||||||
positionRightTreeBidirectional(
|
positionRightTreeBidirectional(
|
||||||
rightResult.children,
|
rightResult.children,
|
||||||
positionedNodes,
|
rightTreeNodes,
|
||||||
rootX + treeSpacing,
|
rootX + treeSpacing,
|
||||||
rootY
|
rootY
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate center points for each tree separately
|
||||||
|
let leftTreeCenterY = 0;
|
||||||
|
let rightTreeCenterY = 0;
|
||||||
|
|
||||||
|
if (leftTreeNodes.length > 0) {
|
||||||
|
const leftMinY = Math.min(...leftTreeNodes.map((node) => node.y));
|
||||||
|
const leftMaxY = Math.max(...leftTreeNodes.map((node) => node.y));
|
||||||
|
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTreeNodes.length > 0) {
|
||||||
|
const rightMinY = Math.min(...rightTreeNodes.map((node) => node.y));
|
||||||
|
const rightMaxY = Math.max(...rightTreeNodes.map((node) => node.y));
|
||||||
|
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate different offsets for each tree to center them around the root
|
||||||
|
const leftTreeOffset = -leftTreeCenterY;
|
||||||
|
const rightTreeOffset = -rightTreeCenterY;
|
||||||
|
|
||||||
|
// Add the centered root
|
||||||
|
positionedNodes.push({
|
||||||
|
id: String(rootNode.id),
|
||||||
|
x: rootX,
|
||||||
|
y: rootY, // Root stays at center
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add left tree nodes with their specific offset
|
||||||
|
leftTreeNodes.forEach((node) => {
|
||||||
|
positionedNodes.push({
|
||||||
|
id: node.id,
|
||||||
|
x: node.x,
|
||||||
|
y: node.y + leftTreeOffset,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add right tree nodes with their specific offset
|
||||||
|
rightTreeNodes.forEach((node) => {
|
||||||
|
positionedNodes.push({
|
||||||
|
id: node.id,
|
||||||
|
x: node.x,
|
||||||
|
y: node.y + rightTreeOffset,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return positionedNodes;
|
return positionedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,15 +323,16 @@ function positionLeftTreeBidirectional(
|
|||||||
offsetY: number
|
offsetY: number
|
||||||
): void {
|
): void {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
// Rotate 90 degrees counterclockwise: (x,y) -> (-y, x)
|
// For left tree: transpose the tidy-tree coordinates
|
||||||
// Then mirror horizontally for left growth: (-y, x) -> (y, x)
|
// Tidy-tree Y becomes our X distance from root (grows left)
|
||||||
const rotatedX = node.y || 0; // Use y as new x (grows left)
|
// Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
|
||||||
const rotatedY = node.x || 0; // Use x as new y (vertical spread)
|
const distanceFromRoot = node.y ?? 0; // How far left from root
|
||||||
|
const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
|
||||||
|
|
||||||
positionedNodes.push({
|
positionedNodes.push({
|
||||||
id: node.id,
|
id: String(node.id),
|
||||||
x: offsetX - rotatedX, // Negative to grow left from root
|
x: offsetX - distanceFromRoot, // Negative to grow left from root
|
||||||
y: offsetY + rotatedY,
|
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -295,14 +352,16 @@ function positionRightTreeBidirectional(
|
|||||||
offsetY: number
|
offsetY: number
|
||||||
): void {
|
): void {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
// Rotate 90 degrees clockwise: (x,y) -> (y, -x)
|
// For right tree: transpose the tidy-tree coordinates
|
||||||
const rotatedX = node.y || 0; // Use y as new x (grows right)
|
// Tidy-tree Y becomes our X distance from root (grows right)
|
||||||
const rotatedY = -(node.x || 0); // Use -x as new y (vertical spread)
|
// Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
|
||||||
|
const distanceFromRoot = node.y ?? 0; // How far right from root
|
||||||
|
const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
|
||||||
|
|
||||||
positionedNodes.push({
|
positionedNodes.push({
|
||||||
id: node.id,
|
id: String(node.id),
|
||||||
x: offsetX + rotatedX, // Positive to grow right from root
|
x: offsetX + distanceFromRoot, // Positive to grow right from root
|
||||||
y: offsetY + rotatedY,
|
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -324,8 +383,8 @@ function calculateEdgePositions(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return edges.map((edge) => {
|
return edges.map((edge) => {
|
||||||
const startPos = nodePositions.get(edge.start) || { x: 0, y: 0 };
|
const startPos = nodePositions.get(edge.start || '') || { x: 0, y: 0 };
|
||||||
const endPos = nodePositions.get(edge.end) || { x: 0, y: 0 };
|
const endPos = nodePositions.get(edge.end || '') || { x: 0, y: 0 };
|
||||||
|
|
||||||
// Calculate midpoint for edge
|
// Calculate midpoint for edge
|
||||||
const midX = (startPos.x + endPos.x) / 2;
|
const midX = (startPos.x + endPos.x) / 2;
|
||||||
@@ -333,8 +392,8 @@ function calculateEdgePositions(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: edge.id,
|
id: edge.id,
|
||||||
source: edge.start,
|
source: edge.start || '',
|
||||||
target: edge.end,
|
target: edge.end || '',
|
||||||
startX: startPos.x,
|
startX: startPos.x,
|
||||||
startY: startPos.y,
|
startY: startPos.y,
|
||||||
midX,
|
midX,
|
||||||
|
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -74,8 +74,8 @@ importers:
|
|||||||
specifier: ^8.17.1
|
specifier: ^8.17.1
|
||||||
version: 8.17.1
|
version: 8.17.1
|
||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.3
|
specifier: ^3.6.0
|
||||||
version: 4.0.3
|
version: 3.6.0
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^9.1.2
|
specifier: ^9.1.2
|
||||||
version: 9.1.2
|
version: 9.1.2
|
||||||
@@ -330,8 +330,8 @@ importers:
|
|||||||
specifier: ^8.17.1
|
specifier: ^8.17.1
|
||||||
version: 8.17.1
|
version: 8.17.1
|
||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.3
|
specifier: ^3.6.0
|
||||||
version: 4.0.3
|
version: 3.6.0
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^9.1.2
|
specifier: ^9.1.2
|
||||||
version: 9.1.2
|
version: 9.1.2
|
||||||
@@ -4550,10 +4550,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
|
||||||
engines: {node: '>= 14.16.0'}
|
|
||||||
|
|
||||||
chrome-trace-event@1.0.4:
|
chrome-trace-event@1.0.4:
|
||||||
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -8417,10 +8413,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
|
||||||
engines: {node: '>= 14.18.0'}
|
|
||||||
|
|
||||||
real-require@0.2.0:
|
real-require@0.2.0:
|
||||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||||
engines: {node: '>= 12.13.0'}
|
engines: {node: '>= 12.13.0'}
|
||||||
@@ -15448,10 +15440,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
chokidar@4.0.3:
|
|
||||||
dependencies:
|
|
||||||
readdirp: 4.1.2
|
|
||||||
|
|
||||||
chrome-trace-event@1.0.4: {}
|
chrome-trace-event@1.0.4: {}
|
||||||
|
|
||||||
ci-info@3.9.0: {}
|
ci-info@3.9.0: {}
|
||||||
@@ -20160,8 +20148,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
rechoir@0.6.2:
|
rechoir@0.6.2:
|
||||||
|
Reference in New Issue
Block a user