glam/DAGRE_GRID_LAYOUT_IMPLEMENTATION.md
kempersc 3ff0e33bf9 Add UML diagrams and scripts for custodian schema
- 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.
2025-11-23 23:05:33 +01:00

474 lines
14 KiB
Markdown

# 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<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):
```typescript
// 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:
```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 */}
<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**:
```tsx
<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):
```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.