- 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.
474 lines
14 KiB
Markdown
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.
|