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

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 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

  • 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.