Install
openclaw skills install dagre-react-flowAutomatic 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.
openclaw skills install dagre-react-flowDagre 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.
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 };
};
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,
};
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,
});
| Value | Direction | Use Case |
|---|---|---|
TB | Top to Bottom | Org charts, decision trees |
BT | Bottom to Top | Dependency graphs (deps at bottom) |
LR | Left to Right | Timelines, horizontal flows |
RL | Right to Left | RTL layouts |
g.setGraph({ rankdir: 'LR' }); // Horizontal layout
Run these in order before treating layout as correct (each step has an objective pass condition):
width and height given to g.setNode for that id are the same numbers used to compute position.x / position.y from g.node(id) (half-width / half-height must match the dagre node box).position is { x: centerX - width/2, y: centerY - height/2 }, not raw g.node(id).x / .y alone.setNodes / setEdges receive a new array instance (e.g. [...layouted] or layouted.map(...)), not the previous reference unchanged.fitView after layout, it runs after nodes are committed (e.g. next requestAnimationFrame or setTimeout(0)), not in the same synchronous tick as setNodes with stale measurements.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 };
}
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>
);
}
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 ... />
</>
);
}
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 });
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>
);
}
Smooth position changes using CSS transitions:
.react-flow__node {
transition: transform 300ms ease-out;
}
For programmatic animation, see reference.md.
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 };
};
Increase spacing:
g.setGraph({
rankdir: 'TB',
nodesep: 100, // Increase horizontal spacing
ranksep: 100, // Increase vertical spacing
});
Ensure new array references:
// Wrong - same reference
setNodes(layoutedNodes);
// Correct - new reference
setNodes([...layoutedNodes]);
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
}
useMemo for layout functionSee reference.md for complete dagre configuration options.