Skills dagre-react-flow
Automatic graph layout using dagre with React Flow (@xyflow/react). Use when implementing auto-layout, hierarchical layouts, tree structures, or arranging nodes programmatically. Triggers on dagre, auto-layout, automatic layout, getLayoutedElements, rankdir, hierarchical graph.
git clone https://github.com/openclaw/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/anderskev/dagre-react-flow" ~/.claude/skills/clawdbot-skills-dagre-react-flow && rm -rf "$T"
skills/anderskev/dagre-react-flow/SKILL.mdDagre with React Flow
Dagre is a JavaScript library for laying out directed graphs. It computes optimal node positions for hierarchical/tree layouts. React Flow handles rendering; dagre handles positioning.
Quick Start
pnpm add @dagrejs/dagre
import dagre from '@dagrejs/dagre'; import { Node, Edge } from '@xyflow/react'; const getLayoutedElements = ( nodes: Node[], edges: Edge[], direction: 'TB' | 'LR' = 'TB' ) => { const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: direction }); g.setDefaultEdgeLabel(() => ({})); nodes.forEach((node) => { g.setNode(node.id, { width: 172, height: 36 }); }); edges.forEach((edge) => { g.setEdge(edge.source, edge.target); }); dagre.layout(g); const layoutedNodes = nodes.map((node) => { const pos = g.node(node.id); return { ...node, position: { x: pos.x - 86, y: pos.y - 18 }, // Center to top-left }; }); return { nodes: layoutedNodes, edges }; };
Core Concepts
Coordinate System Difference
Critical: Dagre returns center coordinates; React Flow uses top-left.
// Dagre output: center of node const dagrePos = g.node(nodeId); // { x: 100, y: 50 } = center // React Flow expects: top-left corner const rfPosition = { x: dagrePos.x - nodeWidth / 2, y: dagrePos.y - nodeHeight / 2, };
Node Dimensions
Dagre requires explicit dimensions. Three approaches:
1. Fixed dimensions (simplest):
g.setNode(node.id, { width: 172, height: 36 });
2. Per-node dimensions from data:
g.setNode(node.id, { width: node.data.width ?? 172, height: node.data.height ?? 36, });
3. Measured dimensions (most accurate):
// After React Flow measures nodes g.setNode(node.id, { width: node.measured?.width ?? 172, height: node.measured?.height ?? 36, });
Layout Directions
| Value | Direction | Use Case |
|---|---|---|
| Top to Bottom | Org charts, decision trees |
| Bottom to Top | Dependency graphs (deps at bottom) |
| Left to Right | Timelines, horizontal flows |
| Right to Left | RTL layouts |
g.setGraph({ rankdir: 'LR' }); // Horizontal layout
Complete Implementation
Basic Layout Function
import dagre from '@dagrejs/dagre'; import type { Node, Edge } from '@xyflow/react'; interface LayoutOptions { direction?: 'TB' | 'BT' | 'LR' | 'RL'; nodeWidth?: number; nodeHeight?: number; nodesep?: number; // Horizontal spacing ranksep?: number; // Vertical spacing (between ranks) } export function getLayoutedElements( nodes: Node[], edges: Edge[], options: LayoutOptions = {} ): { nodes: Node[]; edges: Edge[] } { const { direction = 'TB', nodeWidth = 172, nodeHeight = 36, nodesep = 50, ranksep = 50, } = options; const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: direction, nodesep, ranksep }); g.setDefaultEdgeLabel(() => ({})); nodes.forEach((node) => { const width = node.measured?.width ?? nodeWidth; const height = node.measured?.height ?? nodeHeight; g.setNode(node.id, { width, height }); }); edges.forEach((edge) => { g.setEdge(edge.source, edge.target); }); dagre.layout(g); const layoutedNodes = nodes.map((node) => { const pos = g.node(node.id); const width = node.measured?.width ?? nodeWidth; const height = node.measured?.height ?? nodeHeight; return { ...node, position: { x: pos.x - width / 2, y: pos.y - height / 2, }, }; }); return { nodes: layoutedNodes, edges }; }
React Flow Integration
import { useCallback } from 'react'; import { ReactFlow, useNodesState, useEdgesState, useReactFlow, ReactFlowProvider, } from '@xyflow/react'; import { getLayoutedElements } from './layout'; const initialNodes = [ { id: '1', data: { label: 'Start' }, position: { x: 0, y: 0 } }, { id: '2', data: { label: 'Process' }, position: { x: 0, y: 0 } }, { id: '3', data: { label: 'End' }, position: { x: 0, y: 0 } }, ]; const initialEdges = [ { id: 'e1-2', source: '1', target: '2' }, { id: 'e2-3', source: '2', target: '3' }, ]; // Apply initial layout const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( initialNodes, initialEdges, { direction: 'TB' } ); function Flow() { const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); const { fitView } = useReactFlow(); const onLayout = useCallback((direction: 'TB' | 'LR') => { const { nodes: newNodes, edges: newEdges } = getLayoutedElements( nodes, edges, { direction } ); setNodes([...newNodes]); setEdges([...newEdges]); // Fit view after layout with animation window.requestAnimationFrame(() => { fitView({ duration: 300 }); }); }, [nodes, edges, setNodes, setEdges, fitView]); return ( <div style={{ width: '100%', height: '100vh' }}> <div style={{ position: 'absolute', zIndex: 10, padding: 10 }}> <button onClick={() => onLayout('TB')}>Vertical</button> <button onClick={() => onLayout('LR')}>Horizontal</button> </div> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView /> </div> ); } export default function App() { return ( <ReactFlowProvider> <Flow /> </ReactFlowProvider> ); }
useAutoLayout Hook
Reusable hook for automatic layout:
import { useCallback, useEffect, useRef } from 'react'; import { useReactFlow, useNodesInitialized, type Node, type Edge, } from '@xyflow/react'; import dagre from '@dagrejs/dagre'; interface UseAutoLayoutOptions { direction?: 'TB' | 'BT' | 'LR' | 'RL'; nodesep?: number; ranksep?: number; } export function useAutoLayout(options: UseAutoLayoutOptions = {}) { const { direction = 'TB', nodesep = 50, ranksep = 50 } = options; const { getNodes, getEdges, setNodes, fitView } = useReactFlow(); const nodesInitialized = useNodesInitialized(); const layoutApplied = useRef(false); const runLayout = useCallback(() => { const nodes = getNodes(); const edges = getEdges(); const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: direction, nodesep, ranksep }); g.setDefaultEdgeLabel(() => ({})); nodes.forEach((node) => { g.setNode(node.id, { width: node.measured?.width ?? 172, height: node.measured?.height ?? 36, }); }); edges.forEach((edge) => { g.setEdge(edge.source, edge.target); }); dagre.layout(g); const layouted = nodes.map((node) => { const pos = g.node(node.id); const width = node.measured?.width ?? 172; const height = node.measured?.height ?? 36; return { ...node, position: { x: pos.x - width / 2, y: pos.y - height / 2 }, }; }); setNodes(layouted); window.requestAnimationFrame(() => fitView({ duration: 200 })); }, [direction, nodesep, ranksep, getNodes, getEdges, setNodes, fitView]); // Auto-layout on initialization useEffect(() => { if (nodesInitialized && !layoutApplied.current) { runLayout(); layoutApplied.current = true; } }, [nodesInitialized, runLayout]); return { runLayout }; }
Usage:
function Flow() { const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 }); return ( <> <button onClick={runLayout}>Re-layout</button> <ReactFlow ... /> </> ); }
Edge Options
Control edge routing with weight and minlen:
edges.forEach((edge) => { g.setEdge(edge.source, edge.target, { weight: edge.data?.priority ?? 1, // Higher = more direct path minlen: edge.data?.minRanks ?? 1, // Minimum ranks between nodes }); });
weight: Higher weight edges are prioritized for shorter, more direct paths.
minlen: Forces minimum rank separation between connected nodes.
// Force 2 ranks between nodes g.setEdge('a', 'b', { minlen: 2 });
Common Patterns
Handle Position Based on Direction
Adjust handles for horizontal vs vertical layouts:
function CustomNode({ data }: NodeProps) { const isHorizontal = data.direction === 'LR' || data.direction === 'RL'; return ( <div> <Handle type="target" position={isHorizontal ? Position.Left : Position.Top} /> <div>{data.label}</div> <Handle type="source" position={isHorizontal ? Position.Right : Position.Bottom} /> </div> ); }
Animated Layout Transitions
Smooth position changes using CSS transitions:
.react-flow__node { transition: transform 300ms ease-out; }
For programmatic animation, see reference.md.
Layout with Node Groups
Exclude group nodes from dagre layout:
const layoutWithGroups = (nodes: Node[], edges: Edge[]) => { // Separate regular nodes from groups const regularNodes = nodes.filter((n) => n.type !== 'group'); const groupNodes = nodes.filter((n) => n.type === 'group'); // Layout only regular nodes const { nodes: layouted } = getLayoutedElements(regularNodes, edges); // Combine back return { nodes: [...groupNodes, ...layouted], edges }; };
Troubleshooting
Nodes Overlapping
Increase spacing:
g.setGraph({ rankdir: 'TB', nodesep: 100, // Increase horizontal spacing ranksep: 100, // Increase vertical spacing });
Layout Not Updating
Ensure new array references:
// Wrong - same reference setNodes(layoutedNodes); // Correct - new reference setNodes([...layoutedNodes]);
Nodes at Wrong Position
Check coordinate conversion:
// Dagre returns center, React Flow needs top-left position: { x: pos.x - width / 2, // Not just pos.x y: pos.y - height / 2, // Not just pos.y }
Performance with Large Graphs
- Layout in a Web Worker
- Debounce layout calls
- Use
for layout functionuseMemo - Only re-layout changed portions
Configuration Reference
See reference.md for complete dagre configuration options.