# Dagre Grid Layout Implementation - Complete ✅ **Date**: 2025-11-23 **Status**: Implemented and Ready for Testing **Priority**: HIGH - This is the MOST IMPORTANT difference vs Mermaid --- ## Problem Identified The UML Viewer used **D3 force simulation** (physics-based, scattered layout) while Mermaid uses **dagre** (hierarchical grid layout). This created a fundamentally different visual organization: ``` ❌ BEFORE (Force Simulation): ✅ AFTER (Dagre Grid): ┌───────────────────────┐ ┌─────┬─────┬─────┐ │ A B │ │ A │ B │ C │ │ │ ├─────┼─────┼─────┤ │ D C │ │ D │ E │ F │ │ │ ├─────┼─────┼─────┤ │ E F G │ │ G │ H │ I │ │ H I │ └─────┴─────┴─────┘ └───────────────────────┘ ``` --- ## Implementation Details ### 1. Installed Dagre Library ```bash npm install dagre @types/dagre ``` ### 2. Modified Files #### A. `UMLVisualization.tsx` (Main Changes) **Import dagre**: ```typescript import dagre from 'dagre'; ``` **Added `layoutType` prop**: ```typescript interface UMLVisualizationProps { diagram: UMLDiagram; width?: number; height?: number; diagramType?: DiagramType; layoutType?: 'force' | 'dagre'; // ← NEW } export const UMLVisualization: React.FC = ({ diagram, width = 1200, height = 800, diagramType = 'mermaid-class', layoutType = 'force' // ← NEW (defaults to force for backwards compatibility) }) => { ``` **Implemented dual layout system** (lines 250-308): ```typescript // Layout algorithm: Force simulation or Dagre grid let simulation: d3.Simulation | null = null; if (layoutType === 'dagre') { // Dagre grid layout (tight, hierarchical like Mermaid) const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: 'TB', // Top to Bottom nodesep: 80, // Horizontal spacing between nodes ranksep: 120, // Vertical spacing between ranks marginx: 50, marginy: 50 }); g.setDefaultEdgeLabel(() => ({})); // Add nodes to dagre graph diagram.nodes.forEach(node => { g.setNode(node.id, { width: node.width || nodeWidth, height: node.height || nodeHeaderHeight }); }); // Add edges to dagre graph diagram.links.forEach(link => { g.setEdge( typeof link.source === 'string' ? link.source : (link.source as any).id, typeof link.target === 'string' ? link.target : (link.target as any).id ); }); // Run dagre layout dagre.layout(g); // Apply computed positions to nodes diagram.nodes.forEach(node => { const dagreNode = g.node(node.id); if (dagreNode) { node.x = dagreNode.x; node.y = dagreNode.y; // Lock positions (no physics simulation) (node as any).fx = dagreNode.x; (node as any).fy = dagreNode.y; } }); // No simulation needed - positions are fixed simulation = null; } else { // Force simulation (original scattered physics-based layout) simulation = d3.forceSimulation(diagram.nodes as any) .force('link', d3.forceLink(diagram.links)...); } ``` **Updated tick handler** to work with both layouts: ```typescript // Update positions on tick (force simulation) or immediately (dagre) if (simulation) { simulation.on('tick', () => { // ... standard force simulation updates }); } else { // Dagre layout - positions are already computed, update immediately links.select('line').attr('x1', ...).attr('y1', ...)... nodes.attr('transform', ...) } ``` **Updated drag functions**: ```typescript function dragstarted(event: any, d: any) { if (simulation && !event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event: any, d: any) { d.fx = event.x; d.fy = event.y; // For dagre layout, manually update position since there's no simulation tick if (!simulation) { d.x = event.x; d.y = event.y; // ... manual position updates for nodes and links } } ``` **Updated cleanup**: ```typescript return () => { if (simulation) simulation.stop(); // ← Only stop if exists cleanup(); }; ``` **Added layoutType dependency** to useEffect: ```typescript }, [diagram, width, height, layoutType]); // ← Re-render when layout changes ``` #### B. `UMLViewerPage.tsx` (UI Controls) **Added layout state with localStorage persistence**: ```typescript const [layoutType, setLayoutType] = useState<'force' | 'dagre'>(() => { // Load saved preference from localStorage const saved = localStorage.getItem('uml-layout-type'); return (saved === 'dagre' || saved === 'force') ? saved : 'force'; }); ``` **Added layout toggle buttons in toolbar** (lines 460-502): ```tsx {/* Layout Toggle */}
``` **Passed layoutType to UMLVisualization**: ```tsx ``` #### C. `UMLViewerPage.css` (Styling) **Added layout toggle styles** (lines 631-650): ```css /* Layout Toggle */ .uml-viewer-page__layout-toggle { display: flex; gap: 0.5rem; margin-left: 1.5rem; padding-left: 1.5rem; border-left: 1px solid var(--border-color, #e0e0e0); } .uml-viewer-page__toolbar-button--active { background: #0a3dfa !important; color: white !important; border-color: #0a3dfa !important; } .uml-viewer-page__toolbar-button--active:hover { background: #083ab3 !important; border-color: #083ab3 !important; } ``` --- ## Features Implemented ### 1. **Dual Layout System** - **Force Layout**: Original scattered physics-based simulation - **Dagre Layout**: Tight hierarchical grid (like Mermaid) ### 2. **Toggle Buttons in Toolbar** - Visual icons (scattered nodes vs. grid) - Active state highlighting (blue background) - Descriptive tooltips ### 3. **localStorage Persistence** - User's layout preference saved - Restored on page reload ### 4. **Drag Support for Both Layouts** - Force layout: Uses D3 simulation alpha restart - Dagre layout: Manual position updates (no simulation) - Positions lock after drag ### 5. **Immediate Rendering (Dagre)** - No animation delay (positions computed upfront) - Links and nodes positioned immediately --- ## Dagre Configuration ```typescript g.setGraph({ rankdir: 'TB', // Top to Bottom (vertical hierarchy) nodesep: 80, // 80px horizontal spacing ranksep: 120, // 120px vertical spacing between ranks marginx: 50, // 50px left/right margins marginy: 50 // 50px top/bottom margins }); ``` **Alternative Configurations** (for future tuning): - `rankdir: 'LR'` - Left to Right (horizontal hierarchy) - `nodesep: 50` - Tighter horizontal spacing - `ranksep: 100` - Tighter vertical spacing - `ranker: 'tight-tree'` - Alternative ranking algorithm --- ## Testing Checklist ### Basic Functionality - [ ] Force layout still works (default) - [ ] Dagre layout renders grid correctly - [ ] Toggle buttons switch between layouts - [ ] Active button highlights correctly - [ ] localStorage saves preference - [ ] Preference loads on page reload ### Layout Quality (Dagre) - [ ] Nodes arranged in hierarchical grid - [ ] Inheritance arrows point top-to-bottom - [ ] No overlapping nodes - [ ] Spacing feels balanced (not too tight/loose) - [ ] Links connect to correct node edges ### Interaction - [ ] Zoom works in both layouts - [ ] Pan works in both layouts - [ ] Drag nodes in force layout (simulation restarts) - [ ] Drag nodes in dagre layout (manual update) - [ ] Export PNG/SVG works in both layouts - [ ] Fit to screen works in both layouts ### Edge Cases - [ ] Switch layouts with diagram loaded - [ ] Switch layouts during force simulation (mid-animation) - [ ] Complex diagrams (20+ nodes) render correctly in dagre - [ ] ER diagrams work with dagre layout - [ ] PlantUML diagrams work with dagre layout --- ## Known Limitations 1. **Dagre Direction**: Currently hardcoded to Top-to-Bottom (`rankdir: 'TB'`) - Could add LR/RL/BT options in future 2. **No Live Simulation**: Dagre positions are static - This is intentional (matches Mermaid behavior) - Dragging works but doesn't trigger physics 3. **Link Routing**: Uses straight lines - Dagre supports edge routing, but not implemented yet - Future: Could add orthogonal or curved routing 4. **Rank Assignment**: Uses default dagre ranking - For inheritance hierarchies, could manually assign ranks - Future: Detect class hierarchies and optimize ranking --- ## Future Enhancements ### Phase 2A: Layout Refinements - [ ] Add LR/TB direction toggle - [ ] Implement edge routing (orthogonal/curved) - [ ] Manual rank assignment for class hierarchies - [ ] Compact mode (tighter spacing) ### Phase 2B: Layout-Specific Features - **Force Layout**: - [ ] Adjustable force strengths (slider) - [ ] Different force types (radial, hierarchical) - [ ] Freeze/unfreeze simulation button - **Dagre Layout**: - [ ] Alignment options (UL/UR/DL/DR) - [ ] Rank separation adjustment - [ ] Custom node ordering ### Phase 3: Hybrid Layouts - [ ] Hybrid force+dagre (constrained physics) - [ ] Cluster-based layouts (group related nodes) - [ ] Time-based layouts (for historical changes) --- ## Code Structure ``` frontend/src/ ├── components/uml/ │ └── UMLVisualization.tsx ← Main layout logic (dagre + force) │ ├── pages/ │ ├── UMLViewerPage.tsx ← Layout toggle buttons + state │ └── UMLViewerPage.css ← Layout toggle styling │ └── types/ └── uml.ts ← Type definitions ``` --- ## Performance Considerations ### Force Layout - Simulation runs for ~200 ticks - CPU-intensive for large graphs (50+ nodes) - Animation delay before stable positions ### Dagre Layout - One-time computation (no simulation) - O(N+E) complexity (nodes + edges) - Instant rendering (no animation delay) - Better for large diagrams **Recommendation**: Use dagre for production diagrams (faster, predictable), use force for exploratory analysis (organic, flexible). --- ## Comparison with Mermaid | Feature | Mermaid | Our Implementation | Status | |---------|---------|-------------------|--------| | Grid layout | ✅ dagre | ✅ dagre | ✅ Matched | | Top-to-bottom | ✅ Default | ✅ Default | ✅ Matched | | Node spacing | ✅ Balanced | ✅ Configurable | ✅ Matched | | Link routing | ✅ Straight | ✅ Straight | ✅ Matched | | Direction toggle | ❌ Fixed | ⏳ Planned | 🔄 Future | | Edge routing | ❌ Limited | ⏳ Planned | 🔄 Future | --- ## Developer Notes ### Why Dagre? 1. **Industry Standard**: Used by Mermaid, GraphViz, Cytoscape 2. **Proven Algorithm**: Sugiyama hierarchical layout (1981 research paper) 3. **TypeScript Support**: First-class `@types/dagre` package 4. **Active Maintenance**: Regularly updated, well-documented ### Why Keep Force Simulation? 1. **Organic Layouts**: Good for discovering relationships 2. **Interactive Exploration**: Physics responds to user input 3. **Flexibility**: Works with any graph structure 4. **User Preference**: Some users prefer scattered layouts ### Implementation Insights - **Simulation vs. Static**: Force = animated, Dagre = instant - **Drag Behavior**: Force uses `.alphaTarget()`, Dagre uses manual updates - **Memory Management**: Dagre creates temporary graph, cleaned up by GC - **Position Locking**: Both use `.fx` and `.fy` to lock positions after layout --- ## Testing URL **Dev Server**: http://localhost:5173/uml-viewer **Test Steps**: 1. Select any diagram from sidebar 2. Click **Grid** button in toolbar 3. Observe tight hierarchical layout 4. Click **Force** button 5. Observe scattered physics layout 6. Reload page - preference should persist --- ## Related Documentation - `PHASE1_QUICK_WINS_COMPLETE.md` - Phase 1 features (search, collapse, export dropdown) - `UML_VIEWER_VS_MERMAID_ANALYSIS.md` - Original analysis identifying layout difference - `EXPORT_FUNCTIONALITY_IMPLEMENTATION.md` - Export feature documentation --- ## Summary **Status**: ✅ **Implementation Complete** **Impact**: 🔥 **HIGH - Addresses the MOST important UX difference vs. Mermaid** **Testing**: ⏳ **Ready for user testing** **Next**: Phase 2 - Resizable panels, layout direction toggle, edge routing The dagre grid layout fundamentally transforms the UML Viewer from a scattered force-directed graph to a clean, hierarchical diagram matching professional UML tools. This is the **single most impactful change** for user experience. **User Benefit**: Toggle between exploratory physics layout (Force) and production-ready hierarchical grid (Dagre) with one click.