mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-20 07:49:43 +02:00
Tidy-tree getting closer handling of multi point edges
This commit is contained in:
@@ -112,15 +112,87 @@
|
|||||||
---
|
---
|
||||||
mindmap
|
mindmap
|
||||||
root((mindmap))
|
root((mindmap))
|
||||||
|
Origins
|
||||||
|
Long history
|
||||||
|
::icon(fa fa-book)
|
||||||
|
Popularisation
|
||||||
|
British popular psychology author Tony Buzan
|
||||||
|
Research
|
||||||
|
On effectiveness<br/>and features
|
||||||
|
On Automatic creation
|
||||||
|
Uses
|
||||||
|
Creative techniques
|
||||||
|
Strategic planning
|
||||||
|
Argument mapping
|
||||||
|
Tools
|
||||||
|
Pen and paper
|
||||||
|
Mermaid
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
|
||||||
|
B
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
a1
|
||||||
|
a2
|
||||||
|
C
|
||||||
|
e
|
||||||
|
f
|
||||||
|
g
|
||||||
|
h
|
||||||
|
i
|
||||||
|
D
|
||||||
|
q1
|
||||||
|
q2
|
||||||
|
I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on
|
||||||
|
q3
|
||||||
|
q4
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
Origins
|
Origins
|
||||||
Tools
|
Tools
|
||||||
e
|
e
|
||||||
Third
|
Third
|
||||||
|
q
|
||||||
|
w
|
||||||
|
e
|
||||||
|
r
|
||||||
|
t
|
||||||
|
y
|
||||||
Forth
|
Forth
|
||||||
a
|
a
|
||||||
b
|
b
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
flowchart TB
|
||||||
|
A --> n0["1"]
|
||||||
|
A --> n1["2"]
|
||||||
|
A --> n2["3"]
|
||||||
|
A --> n3["4"] --> Q & R & S & T
|
||||||
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
@@ -130,7 +202,7 @@
|
|||||||
A --> n0["1"]
|
A --> n0["1"]
|
||||||
A --> n1["2"]
|
A --> n1["2"]
|
||||||
A --> n2["3"]
|
A --> n2["3"]
|
||||||
A --> n3["4"]
|
A --> n3["4"] --> Q & R & S & T
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
|
@@ -102,7 +102,7 @@ export const render = async (
|
|||||||
|
|
||||||
layoutResult.nodes.forEach((positionedNode) => {
|
layoutResult.nodes.forEach((positionedNode) => {
|
||||||
const node = nodeDb[positionedNode.id];
|
const node = nodeDb[positionedNode.id];
|
||||||
if (node && node.domId) {
|
if (node?.domId) {
|
||||||
// Position the node at the calculated coordinates
|
// Position the node at the calculated coordinates
|
||||||
// The positionedNode.x/y represents the center of the node, so use directly
|
// The positionedNode.x/y represents the center of the node, so use directly
|
||||||
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||||
@@ -132,13 +132,11 @@ export const render = async (
|
|||||||
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||||
|
|
||||||
if (positionedEdge) {
|
if (positionedEdge) {
|
||||||
|
console.debug('APA01 positionedEdge', positionedEdge);
|
||||||
// Create edge path with positioned coordinates
|
// Create edge path with positioned coordinates
|
||||||
const edgeWithPath = {
|
const edgeWithPath = {
|
||||||
...edge,
|
...edge,
|
||||||
points: [
|
points: positionedEdge.points,
|
||||||
{ x: positionedEdge.startX, y: positionedEdge.startY },
|
|
||||||
{ x: positionedEdge.endX, y: positionedEdge.endY },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the edge path
|
// Insert the edge path
|
||||||
|
@@ -4,6 +4,7 @@ import { log } from '../../../logger.js';
|
|||||||
import type { LayoutData, Node, Edge } from '../../types.js';
|
import type { LayoutData, Node, Edge } from '../../types.js';
|
||||||
import type { LayoutResult, TidyTreeNode, PositionedNode, PositionedEdge } from './types.js';
|
import type { LayoutResult, TidyTreeNode, PositionedNode, PositionedEdge } from './types.js';
|
||||||
|
|
||||||
|
let intersectionShift = 50;
|
||||||
/**
|
/**
|
||||||
* Execute the tidy-tree layout algorithm on generic layout data
|
* Execute the tidy-tree layout algorithm on generic layout data
|
||||||
*
|
*
|
||||||
@@ -31,12 +32,22 @@ export function executeTidyTreeLayout(
|
|||||||
data.edges = []; // Allow empty edges for single-node trees
|
data.edges = []; // Allow empty edges for single-node trees
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the maximum number of children any one node have expect for the root node
|
||||||
|
const maxChildren = Math.max(...data.nodes.map((node) => node.children?.length ?? 0));
|
||||||
|
|
||||||
// Convert layout data to dual-tree format (left and right trees)
|
// Convert layout data to dual-tree format (left and right trees)
|
||||||
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
|
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
|
||||||
|
|
||||||
// Configure tidy-tree layout
|
// Configure tidy-tree layout
|
||||||
const gap = 20; // Horizontal gap between nodes
|
const gap = 20; // Vertical gap between nodes
|
||||||
const bottomPadding = 40; // Vertical gap between levels
|
const bottomPadding = 40; // Horizontal gap between levels
|
||||||
|
intersectionShift = 30;
|
||||||
|
|
||||||
|
// if (maxChildren > 5) {
|
||||||
|
// bottomPadding = 50;
|
||||||
|
// intersectionShift = 50;
|
||||||
|
// }
|
||||||
|
|
||||||
const bb = new BoundingBox(gap, bottomPadding);
|
const bb = new BoundingBox(gap, bottomPadding);
|
||||||
const layout = new Layout(bb);
|
const layout = new Layout(bb);
|
||||||
|
|
||||||
@@ -47,14 +58,12 @@ 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;
|
||||||
@@ -218,7 +227,6 @@ function convertNodeToTidyTreeTransposed(
|
|||||||
_originalNode: node,
|
_originalNode: node,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine and position the left and right trees around the root node
|
* Combine and position the left and right trees around the root node
|
||||||
* Creates a bidirectional layout where left tree grows left and right tree grows right
|
* Creates a bidirectional layout where left tree grows left and right tree grows right
|
||||||
@@ -265,19 +273,40 @@ function combineAndPositionTrees(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate center points for each tree separately
|
// Calculate center points for each tree separately
|
||||||
|
// Only use first-level children (direct children of root) for centering
|
||||||
let leftTreeCenterY = 0;
|
let leftTreeCenterY = 0;
|
||||||
let rightTreeCenterY = 0;
|
let rightTreeCenterY = 0;
|
||||||
|
|
||||||
if (leftTreeNodes.length > 0) {
|
if (leftTreeNodes.length > 0) {
|
||||||
const leftMinY = Math.min(...leftTreeNodes.map((node) => node.y));
|
// Filter to only first-level children (those closest to root on X axis)
|
||||||
const leftMaxY = Math.max(...leftTreeNodes.map((node) => node.y));
|
// Find the X coordinate closest to root for left tree nodes
|
||||||
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
|
||||||
|
(a, b) => b - a
|
||||||
|
); // Sort descending (closest to root first)
|
||||||
|
const firstLevelLeftX = leftTreeXPositions[0]; // X position closest to root
|
||||||
|
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
|
||||||
|
|
||||||
|
if (firstLevelLeftNodes.length > 0) {
|
||||||
|
const leftMinY = Math.min(...firstLevelLeftNodes.map((node) => node.y));
|
||||||
|
const leftMaxY = Math.max(...firstLevelLeftNodes.map((node) => node.y));
|
||||||
|
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightTreeNodes.length > 0) {
|
if (rightTreeNodes.length > 0) {
|
||||||
const rightMinY = Math.min(...rightTreeNodes.map((node) => node.y));
|
// Filter to only first-level children (those closest to root on X axis)
|
||||||
const rightMaxY = Math.max(...rightTreeNodes.map((node) => node.y));
|
// Find the X coordinate closest to root for right tree nodes
|
||||||
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
|
||||||
|
(a, b) => a - b
|
||||||
|
); // Sort ascending (closest to root first)
|
||||||
|
const firstLevelRightX = rightTreeXPositions[0]; // X position closest to root
|
||||||
|
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
|
||||||
|
|
||||||
|
if (firstLevelRightNodes.length > 0) {
|
||||||
|
const rightMinY = Math.min(...firstLevelRightNodes.map((node) => node.y));
|
||||||
|
const rightMaxY = Math.max(...firstLevelRightNodes.map((node) => node.y));
|
||||||
|
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate different offsets for each tree to center them around the root
|
// Calculate different offsets for each tree to center them around the root
|
||||||
@@ -289,6 +318,10 @@ function combineAndPositionTrees(
|
|||||||
id: String(rootNode.id),
|
id: String(rootNode.id),
|
||||||
x: rootX,
|
x: rootX,
|
||||||
y: rootY, // Root stays at center
|
y: rootY, // Root stays at center
|
||||||
|
section: 'root',
|
||||||
|
width: rootNode._originalNode?.width || rootNode.width,
|
||||||
|
height: rootNode._originalNode?.height || rootNode.height,
|
||||||
|
originalNode: rootNode._originalNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add left tree nodes with their specific offset
|
// Add left tree nodes with their specific offset
|
||||||
@@ -297,6 +330,10 @@ function combineAndPositionTrees(
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
x: node.x,
|
x: node.x,
|
||||||
y: node.y + leftTreeOffset,
|
y: node.y + leftTreeOffset,
|
||||||
|
section: 'left',
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
originalNode: node.originalNode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,6 +343,10 @@ function combineAndPositionTrees(
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
x: node.x,
|
x: node.x,
|
||||||
y: node.y + rightTreeOffset,
|
y: node.y + rightTreeOffset,
|
||||||
|
section: 'right',
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
originalNode: node.originalNode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,6 +374,9 @@ function positionLeftTreeBidirectional(
|
|||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
x: offsetX - distanceFromRoot, // Negative to grow left from root
|
x: offsetX - distanceFromRoot, // Negative to grow left from root
|
||||||
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
||||||
|
width: node._originalNode?.width || node.width,
|
||||||
|
height: node._originalNode?.height || node.height,
|
||||||
|
originalNode: node._originalNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -362,6 +406,9 @@ function positionRightTreeBidirectional(
|
|||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
x: offsetX + distanceFromRoot, // Positive to grow right from root
|
x: offsetX + distanceFromRoot, // Positive to grow right from root
|
||||||
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
|
||||||
|
width: node._originalNode?.width || node.width,
|
||||||
|
height: node._originalNode?.height || node.height,
|
||||||
|
originalNode: node._originalNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -372,34 +419,85 @@ function positionRightTreeBidirectional(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate edge positions based on positioned nodes
|
* Calculate edge positions based on positioned nodes
|
||||||
|
* Now includes tree membership and node dimensions for precise edge calculations
|
||||||
*/
|
*/
|
||||||
function calculateEdgePositions(
|
function calculateEdgePositions(
|
||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
positionedNodes: PositionedNode[]
|
positionedNodes: PositionedNode[]
|
||||||
): PositionedEdge[] {
|
): PositionedEdge[] {
|
||||||
const nodePositions = new Map<string, { x: number; y: number }>();
|
// Create a comprehensive map of node information
|
||||||
|
const nodeInfo = new Map<string, PositionedNode>();
|
||||||
positionedNodes.forEach((node) => {
|
positionedNodes.forEach((node) => {
|
||||||
nodePositions.set(node.id, { x: node.x, y: node.y });
|
nodeInfo.set(node.id, node);
|
||||||
});
|
});
|
||||||
|
|
||||||
return edges.map((edge) => {
|
return edges.map((edge) => {
|
||||||
const startPos = nodePositions.get(edge.start || '') || { x: 0, y: 0 };
|
const sourceNode = nodeInfo.get(edge.start ?? '');
|
||||||
const endPos = nodePositions.get(edge.end || '') || { x: 0, y: 0 };
|
const targetNode = nodeInfo.get(edge.end ?? '');
|
||||||
|
console.debug('APA01 calculateEdgePositions', edge, sourceNode, 'targetNode', targetNode);
|
||||||
|
|
||||||
|
if (!sourceNode) {
|
||||||
|
console.error('APA01 Source node not found for edge', edge);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetNode === undefined) {
|
||||||
|
console.error('APA01 Target node not found for edge', edge);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback positions if nodes not found
|
||||||
|
const startPos = sourceNode ? { x: sourceNode.x, y: sourceNode.y } : { x: 0, y: 0 };
|
||||||
|
const endPos = targetNode ? { x: targetNode.x, y: targetNode.y } : { 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;
|
||||||
const midY = (startPos.y + endPos.y) / 2;
|
const midY = (startPos.y + endPos.y) / 2;
|
||||||
|
|
||||||
|
const points = [startPos];
|
||||||
|
if (sourceNode.section === 'left') {
|
||||||
|
points.push({
|
||||||
|
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
|
||||||
|
y: sourceNode.y,
|
||||||
|
});
|
||||||
|
} else if (sourceNode.section === 'right') {
|
||||||
|
points.push({
|
||||||
|
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
|
||||||
|
y: sourceNode.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetNode.section === 'left') {
|
||||||
|
points.push({
|
||||||
|
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
|
||||||
|
y: targetNode.y,
|
||||||
|
});
|
||||||
|
} else if (targetNode.section === 'right') {
|
||||||
|
points.push({
|
||||||
|
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
|
||||||
|
y: targetNode.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push(endPos);
|
||||||
|
|
||||||
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,
|
||||||
midY,
|
midY,
|
||||||
endX: endPos.x,
|
endX: endPos.x,
|
||||||
endY: endPos.y,
|
endY: endPos.y,
|
||||||
|
points,
|
||||||
|
// Include tree membership information
|
||||||
|
sourceSection: sourceNode?.section,
|
||||||
|
targetSection: targetNode?.section,
|
||||||
|
// Include node dimensions for precise edge point calculations
|
||||||
|
sourceWidth: sourceNode?.width,
|
||||||
|
sourceHeight: sourceNode?.height,
|
||||||
|
targetWidth: targetNode?.width,
|
||||||
|
targetHeight: targetNode?.height,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -152,15 +152,17 @@ export const render = async (
|
|||||||
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||||
|
|
||||||
if (positionedEdge) {
|
if (positionedEdge) {
|
||||||
|
console.debug('APA01 positionedEdge', positionedEdge);
|
||||||
// Create edge path with positioned coordinates
|
// Create edge path with positioned coordinates
|
||||||
const edgeWithPath = {
|
const edgeWithPath = {
|
||||||
...edge,
|
...edge,
|
||||||
points: [
|
points: positionedEdge.points,
|
||||||
{ x: positionedEdge.startX, y: positionedEdge.startY },
|
// points: [
|
||||||
{ x: positionedEdge.endX, y: positionedEdge.endY },
|
// { x: positionedEdge.startX, y: positionedEdge.startY },
|
||||||
],
|
// { x: positionedEdge.endX, y: positionedEdge.endY },
|
||||||
|
// ],
|
||||||
};
|
};
|
||||||
|
// debugger;
|
||||||
// Insert the edge path
|
// Insert the edge path
|
||||||
const paths = insertEdge(
|
const paths = insertEdge(
|
||||||
edgePaths,
|
edgePaths,
|
||||||
|
@@ -7,6 +7,13 @@ export interface PositionedNode {
|
|||||||
id: string;
|
id: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
// Tree membership information for edge calculations
|
||||||
|
section?: 'root' | 'left' | 'right';
|
||||||
|
// Original node dimensions for edge point calculations
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
// Reference to original node for additional data
|
||||||
|
originalNode?: Node;
|
||||||
[key: string]: unknown; // Allow additional properties
|
[key: string]: unknown; // Allow additional properties
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +30,14 @@ export interface PositionedEdge {
|
|||||||
midY: number;
|
midY: number;
|
||||||
endX: number;
|
endX: number;
|
||||||
endY: number;
|
endY: number;
|
||||||
|
// Tree membership information for edge styling/routing
|
||||||
|
sourceSection?: 'root' | 'left' | 'right';
|
||||||
|
targetSection?: 'root' | 'left' | 'right';
|
||||||
|
// Node dimensions for precise edge point calculations
|
||||||
|
sourceWidth?: number;
|
||||||
|
sourceHeight?: number;
|
||||||
|
targetWidth?: number;
|
||||||
|
targetHeight?: number;
|
||||||
[key: string]: unknown; // Allow additional properties
|
[key: string]: unknown; // Allow additional properties
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user