The MindGraph layout system uses D3's hierarchical cluster layout algorithm to position mind map nodes in a tree structure. This guide explains how the layout works and how to tune it for different spacing requirements.
- layoutHelpers.ts - Main layout calculation engine
- D3-hierarchy - Tree positioning algorithm (
d3.cluster()) - React Flow - Canvas rendering with Bezier curve edges
Flat NodeInstance array
↓
buildHierarchyTree() - Converts to tree structure
↓
applyBalancedLayout() - Calculates positions using D3
↓
React Flow nodes with (x, y) coordinates
All layout parameters are centralized in LAYOUT_CONFIG:
const LAYOUT_CONFIG = {
nodeMaxWidth: 400, // Maximum node width in pixels
lineHeight: 24, // Text line height in pixels
basePadding: 20, // Base padding for height calculation
horizontalSpacing: 280, // Fixed spacing between depth levels
verticalSpacingMultiplier: 0.5, // Controls vertical compactness
minVerticalSeparation: 0.01, // Base separation between siblings
};All critical tuning variables are centralized in the LAYOUT_CONFIG object at the top of layoutHelpers.ts.
What it controls: The horizontal gap between a parent node's right edge and its child node's left edge. This creates consistent visual edge lengths regardless of the parent node's text length or width.
Current value: 80px
How to adjust:
const LAYOUT_CONFIG = {
// ...
horizontalSpacing: 80, // Increase for more spacing, decrease for less
// ...
};How it works:
The system calculates each node's actual rendered width based on text content (considering word wrapping). Child nodes are positioned at: parentX + parentWidth + horizontalSpacing. This ensures edges always have the same visual length from right edge to left edge.
Impact:
- Increase (e.g., 120px): More space between nodes, longer edges
- Decrease (e.g., 50px): Tighter horizontal layout, shorter edges
- Consistent: Edge length is now independent of parent node width
When to change:
- Want more or less breathing room between connected nodes
- Edges feel too short or too long
- Need to fit more nodes horizontally on screen
- Want to balance horizontal and vertical spacing
What it controls: The vertical space multiplier applied to the sum of all visible node heights. This is the primary vertical spacing control that works consistently whether nodes are collapsed or expanded.
Current value: 0.5 (comfortable spacing)
How to adjust:
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.5, // Change this value
// ...
};How it works:
treeHeight = totalNodeHeight × verticalSpacingMultiplierWhere totalNodeHeight is the sum of all visible node heights. This ensures:
- ✅ Collapsed trees (fewer visible nodes) maintain consistent spacing
- ✅ Expanded trees (more visible nodes) don't get overly spaced out
- ✅ Spacing remains proportional to actual content height
Impact:
- Decrease to 0.3: More compact vertical spacing
- Current (0.5): Comfortable, balanced spacing
- Increase to 0.7: More breathing room between nodes
- Increase to 1.0: Very spacious layout
Common values:
| Multiplier | Spacing Style | Use Case |
|---|---|---|
| 0.3 | Compact | Dense information display |
| 0.5 | Comfortable (current) | Balanced readability |
| 0.7 | Spacious | Good for presentations |
| 1.0+ | Very loose | Whiteboard-style |
When to change:
- Graph feels too cramped or too loose vertically
- After collapsing/expanding nodes, spacing feels inconsistent
- Want predictable spacing regardless of tree size
What it controls: The base minimum vertical distance between sibling nodes. The system automatically adds extra separation for taller nodes (multi-line text) to prevent overlap.
Current value: 0.01 (base minimum)
How to adjust:
const LAYOUT_CONFIG = {
// ...
minVerticalSeparation: 0.01, // Change this value
// ...
};Impact:
- Current (0.01): Small base spacing, automatically scales for taller nodes
- Increase to 0.5: More base spacing between all siblings
- Increase to 1.0: Significant base vertical gaps
How it works with node height: The separation function automatically accounts for node heights to prevent overlap:
// Taller nodes (multi-line text) automatically get more space
separation = minVerticalSeparation + (heightFactor × 0.5)Where heightFactor is the average height of two adjacent nodes divided by the maximum node height in the tree.
Advanced usage:
You can modify the separation function in the code for custom spacing rules:
// In applyBalancedLayout function
.separation((a, b) => {
// Custom logic based on nodes a and b
const heightA = a.data?.estimatedHeight || maxHeight;
const heightB = b.data?.estimatedHeight || maxHeight;
const avgHeight = (heightA + heightB) / 2;
const heightFactor = avgHeight / maxHeight;
// Adjust the multiplier (0.5) to control height-based spacing
return LAYOUT_CONFIG.minVerticalSeparation + (heightFactor * 0.5);
})When to change:
- Want more/less base spacing between all siblings
- Need to adjust the height-based spacing multiplier (currently 0.5)
- verticalSpacingMultiplier alone isn't giving desired results
The layout system automatically adjusts vertical spacing based on node heights to prevent overlap when text wraps to multiple lines while ensuring identical spacing for similar subtrees.
The system uses a sophisticated separation calculation that ensures consistent spacing across different parts of your mindgraph:
- Estimates node height based on text content and word wrapping
- Calculates relative separation for each pair of adjacent siblings:
relativeSeparation = (maxNodeHeight / avgNodeHeight) × 0.02 - D3 scales this by the tree's height to get absolute pixel spacing
The Challenge: D3's .separation() function returns a relative multiplier, not absolute pixels. Each root node tree calculates its own treeHeight based on visible nodes, which means the same separation value could produce different pixel spacing in different subtrees.
The Solution: By calculating separation relative to the average node height in each tree, the system ensures that:
- ✅ Similar subtrees get identical spacing - Same structure = same spacing
- ✅ Independence - A tall node in one branch doesn't affect spacing elsewhere
- ✅ Overlap prevention - Taller nodes automatically get more space
- ✅ Mathematical consistency - The relative factors cancel out to produce absolute spacing
// What the separation function returns (relative multiplier)
relativeSeparation = (maxNodeHeight / avgNodeHeight) × 0.02
// What D3 calculates for actual pixel spacing
actualSpacing = relativeSeparation × (treeHeight / nodeCount)
= (maxNodeHeight / avgNodeHeight) × 0.02 × (totalNodeHeight × 0.5 / nodeCount)
= (maxNodeHeight / avgNodeHeight) × 0.02 × (totalNodeHeight × 0.5 / nodeCount)
// Since avgNodeHeight = totalNodeHeight / nodeCount, this simplifies to:
= maxNodeHeight × 0.01
// Result: Spacing depends ONLY on actual node height!For a node with height 88px (double-line):
relativeSeparation = (88 / 44) × 0.02 = 0.04
actualSpacing ≈ 88 × 0.01 = 0.88 pixels (in D3's normalized space)
This produces consistent spacing regardless of what else is in the tree!
To adjust spacing, modify the 0.02 multiplier in the separation function:
// In the separation function
const relativeSeparation = (maxNodeHeight / avgNodeHeight) * 0.02;
// ^^^^
// Increase for more space
// Decrease for less spaceCommon values:
0.01- Very compact (may cause overlap with 5+ line nodes)0.02- Balanced (current, good for most cases)0.03- More spacious0.04- Very spacious
Important: This multiplier works consistently across all subtrees because it's mathematically normalized!
The layout system is designed to maintain consistent spacing whether nodes are collapsed or expanded:
Key Innovation: Total Height-Based Calculation
treeHeight = totalNodeHeight × verticalSpacingMultiplierInstead of using nodeCount (which changes dramatically), we use the sum of all visible node heights:
- When collapsed: Fewer visible nodes → smaller
totalNodeHeight→ proportionally smallertreeHeight - When expanded: More visible nodes → larger
totalNodeHeight→ proportionally largertreeHeight
Result: The verticalSpacingMultiplier has consistent effect regardless of collapse state!
Given a tree with:
- 1 root node (44px)
- 2 children (44px each)
- 4 grandchildren (44px each)
Fully Expanded:
totalNodeHeight = 7 × 44px = 308px
treeHeight = 308px × 0.3 = 92.4px spacing budget
Root Collapsed (only root visible):
totalNodeHeight = 1 × 44px = 44px
treeHeight = 44px × 0.3 = 13.2px spacing budget
Parent Collapsed (root + 2 children):
totalNodeHeight = 3 × 44px = 132px
treeHeight = 132px × 0.3 = 39.6px spacing budget
The spacing scales proportionally with the amount of visible content!
Old approach (count-based):
- ❌ Collapsed:
nodeCount = 1→ tiny spacing effect - ❌ Expanded:
nodeCount = 100→ huge spacing effect - ❌ Multiplier value needs constant adjustment
New approach (height-based):
- ✅ Collapsed: Small total height → appropriately small spacing
- ✅ Expanded: Large total height → appropriately scaled spacing
- ✅ Multiplier value works consistently
The system estimates node dimensions based on text content and CSS constraints:
// From LAYOUT_CONFIG
nodeMaxWidth: 400; // CSS max-width constraint
lineHeight: 24; // CSS line-height (1.5rem)How it works:
- Calculates characters per line based on
nodeMaxWidth(400px) and average character width (11px) - Simulates word wrapping to count total lines
- Calculates height:
(lines × lineHeight) + basePadding + extraPadding
To adjust node sizes:
- Change
nodeMaxWidthin LAYOUT_CONFIG - Update corresponding CSS in
App.css:.node-title { max-width: 400px; /* Match LAYOUT_CONFIG.nodeMaxWidth */ font-size: 1.2rem; /* Affects character width estimation */ }
Problem: This was an issue with the old count-based approach but should be fixed now.
If still experiencing issues:
- Verify you're using the latest version with
totalNodeHeightcalculation - The multiplier should now work consistently - try adjusting it globally:
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.4, // Increase for more space everywhere
// ...
};Expected result: Consistent spacing behavior regardless of collapse state
Solution: Increase verticalSpacingMultiplier
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.5, // Changed from 0.3
// ...
};Expected result: More vertical space between all nodes, scales with content
Solution: Adjust horizontalSpacing
const LAYOUT_CONFIG = {
// ...
horizontalSpacing: 300, // For longer edges (was 240)
// OR
horizontalSpacing: 200, // For shorter edges (was 240)
// ...
};Expected result: Uniform change in edge length across all depths
Solution: Increase verticalSpacingMultiplier AND minVerticalSeparation
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.2, // Double current (was 0.1)
minVerticalSeparation: 0.5, // 500× current (was 0.001)
// ...
};Expected result: More vertical space between siblings
Solution: Implement depth-based horizontal spacing
// Replace constant spacing with depth-based calculation
node.x = node.depth * getSpacingForDepth(node.depth);
function getSpacingForDepth(depth: number): number {
if (depth === 0) return 0;
if (depth === 1) return 300; // First level gets more space
return 200; // Deeper levels more compact
}Status: ✅ FIXED - The separation function now calculates spacing relative to each tree's average node height, ensuring consistent results.
How it works:
- Separation is calculated as
(maxNodeHeight / avgNodeHeight) × 0.02 - This creates a relative multiplier that D3 scales by
treeHeight - The math ensures that similar structures produce identical absolute spacing
- Example: Two subtrees with 3 nodes of 44px each will always have the same spacing
If still seeing inconsistencies:
- Verify nodes actually have the same text (different text = different heights)
- Check that LAYOUT_CONFIG values match between sessions
- Clear localStorage and recreate the nodes if needed
Current Configuration:
- Node max-width: 400px (reduces line count by ~33%)
- Separation multiplier: 0.02 (relative to tree average height)
- Character width estimation: 11px (matches 1.2rem font)
- Extra padding for 5+ line nodes: +10px
If still experiencing overlap:
- Increase the separation multiplier to 0.03 or 0.04 in the code:
const relativeSeparation = (maxNodeHeight / avgNodeHeight) * 0.03;
- Increase verticalSpacingMultiplier for more overall space:
verticalSpacingMultiplier: 0.6; // Increase from 0.5
- Increase node max-width to reduce wrapping:
And update CSS:
nodeMaxWidth: 500; // In LAYOUT_CONFIG
.node-title { max-width: 500px; }
Cause: Horizontal spacing too small for wide parent nodes
Fix: Increase horizontalSpacing
const LAYOUT_CONFIG = {
// ...
horizontalSpacing: 280, // Was 240
// ...
};Cause: verticalSpacingMultiplier too small or minVerticalSeparation too tight
Fix: Increase multiplier
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.15, // Was 0.1
// ...
};Cause: verticalSpacingMultiplier too large
Fix: Decrease multiplier
const LAYOUT_CONFIG = {
// ...
verticalSpacingMultiplier: 0.08, // Was 0.1
// ...
};D3's cluster layout creates a dendrogram (tree diagram) where:
- Nodes at the same depth are horizontally aligned
- Leaf nodes are evenly distributed vertically
- The layout automatically balances the tree
const cluster = d3
.cluster<TreeNode>()
.size([treeHeight, 1]) // [vertical space, horizontal space (ignored)]
.separation(() => 0.001); // Minimum vertical gap between nodessize([height, width]):
height: Total vertical space to distribute nodes acrosswidth: We ignore this and apply our own horizontal spacing
separation(function):
- Controls vertical spacing between siblings
- Returns a multiplier applied to the base spacing
0.001= absolute minimum, effectively letstreeHeightcontrol spacing
- Always adjust horizontalSpacing first when dealing with horizontal layout issues
- Use verticalSpacingMultiplier as primary vertical spacing control (easier than minVerticalSeparation)
- Keep minVerticalSeparation at 0.001 unless you need fine-grained sibling spacing rules
- Test with various graph sizes - small (5 nodes), medium (20 nodes), large (100+ nodes)
- Match CSS and LAYOUT_CONFIG - Ensure
nodeMaxWidthmatches.node-title max-width - Document any custom changes - Add comments explaining non-standard values
| Variable | Current Value | Primary Effect | Recommended Range |
|---|---|---|---|
| horizontalSpacing | 280px | Edge length | 220-400px |
| verticalSpacingMultiplier | 0.5 | Vertical spacing (collapse-stable) | 0.3-1.0 |
| minVerticalSeparation | 0.01 | Base sibling spacing | 0.01-0.1 |
| nodeMaxWidth | 400px | Node text wrapping | 300-600px |
| fontSize | 1.2rem | Text size, affects wrapping | 1rem-1.5rem |
| separationMultiplier | 0.02 | Height-based spacing (tree-relative) | 0.01-0.04 |
- src/utils/layoutHelpers.ts - Layout calculation logic
- src/App.css - Node styling (
.node-title) - src/components/MindNode.tsx - Individual node component with edge handles
- src/components/Canvas.tsx - React Flow canvas integration