- Created PlantUML diagrams for custodian types, full schema, legal status, and organizational structure. - Implemented a script to generate GraphViz DOT diagrams from OWL/RDF ontology files. - Developed a script to generate UML diagrams from modular LinkML schema, supporting both Mermaid and PlantUML formats. - Enhanced class definitions and relationships in UML diagrams to reflect the latest schema updates.
14 KiB
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
npm install dagre @types/dagre
2. Modified Files
A. UMLVisualization.tsx (Main Changes)
Import dagre:
import dagre from 'dagre';
Added layoutType prop:
interface UMLVisualizationProps {
diagram: UMLDiagram;
width?: number;
height?: number;
diagramType?: DiagramType;
layoutType?: 'force' | 'dagre'; // ← NEW
}
export const UMLVisualization: React.FC<UMLVisualizationProps> = ({
diagram,
width = 1200,
height = 800,
diagramType = 'mermaid-class',
layoutType = 'force' // ← NEW (defaults to force for backwards compatibility)
}) => {
Implemented dual layout system (lines 250-308):
// Layout algorithm: Force simulation or Dagre grid
let simulation: d3.Simulation<any, undefined> | 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:
// 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:
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:
return () => {
if (simulation) simulation.stop(); // ← Only stop if exists
cleanup();
};
Added layoutType dependency to useEffect:
}, [diagram, width, height, layoutType]); // ← Re-render when layout changes
B. UMLViewerPage.tsx (UI Controls)
Added layout state with localStorage persistence:
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):
{/* Layout Toggle */}
<div className="uml-viewer-page__layout-toggle">
<button
className={`uml-viewer-page__toolbar-button ${layoutType === 'force' ? 'uml-viewer-page__toolbar-button--active' : ''}`}
title="Force layout (scattered physics-based)"
onClick={() => {
setLayoutType('force');
localStorage.setItem('uml-layout-type', 'force');
}}
>
<svg>...</svg>
Force
</button>
<button
className={`uml-viewer-page__toolbar-button ${layoutType === 'dagre' ? 'uml-viewer-page__toolbar-button--active' : ''}`}
title="Grid layout (hierarchical, like Mermaid)"
onClick={() => {
setLayoutType('dagre');
localStorage.setItem('uml-layout-type', 'dagre');
}}
>
<svg>...</svg>
Grid
</button>
</div>
Passed layoutType to UMLVisualization:
<UMLVisualization
diagram={diagram}
width={1400}
height={900}
diagramType={selectedFile?.type === 'erdiagram' ? 'mermaid-er' : 'mermaid-class'}
layoutType={layoutType} // ← NEW
/>
C. UMLViewerPage.css (Styling)
Added layout toggle styles (lines 631-650):
/* 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
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 spacingranksep: 100- Tighter vertical spacingranker: '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
-
Dagre Direction: Currently hardcoded to Top-to-Bottom (
rankdir: 'TB')- Could add LR/RL/BT options in future
-
No Live Simulation: Dagre positions are static
- This is intentional (matches Mermaid behavior)
- Dragging works but doesn't trigger physics
-
Link Routing: Uses straight lines
- Dagre supports edge routing, but not implemented yet
- Future: Could add orthogonal or curved routing
-
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?
- Industry Standard: Used by Mermaid, GraphViz, Cytoscape
- Proven Algorithm: Sugiyama hierarchical layout (1981 research paper)
- TypeScript Support: First-class
@types/dagrepackage - Active Maintenance: Regularly updated, well-documented
Why Keep Force Simulation?
- Organic Layouts: Good for discovering relationships
- Interactive Exploration: Physics responds to user input
- Flexibility: Works with any graph structure
- 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
.fxand.fyto lock positions after layout
Testing URL
Dev Server: http://localhost:5173/uml-viewer
Test Steps:
- Select any diagram from sidebar
- Click Grid button in toolbar
- Observe tight hierarchical layout
- Click Force button
- Observe scattered physics layout
- 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 differenceEXPORT_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.