Tidy-tree getting closer, now algorithm works

This commit is contained in:
Knut Sveidqvist
2025-06-11 20:58:54 +02:00
parent d2463f41b5
commit c13ce2a5c0
5 changed files with 107 additions and 59 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View File

@@ -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: