dagre-react-flow について
dagre-react-flowは、有向グラフのノードを自動配置したいJavaScript開発者向けに、DagreのレイアウトをReact Flowで使う方法を説明します。座標変換、ノード寸法、レイアウト方向、再利用可能な自動レイアウトフックを扱います。
主な機能
- 01階層型有向グラフのレイアウト計算
- 024種類のレイアウト方向に対応
- 03DagreとReact Flowの座標変換
- 04ノード寸法と間隔の設定
- 05自動レイアウトフックの提供
活用シーン
- 01組織図や決定木の配置
- 02横向きのタイムラインやフローの作成
- 03ノード間隔とエッジ経路の調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill dagre-react-flowインストール後、Claude Code で/dagre-react-flowを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
React Flow での Dagre
Dagre は、有向グラフをレイアウトするための JavaScript ライブラリです。階層型/ツリー型レイアウトに最適なノード位置を計算します。React Flow は描画を担当し、dagre は位置決めを担当します。
クイックスタート
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 };
};
基本概念
座標系の違い
重要: Dagre は中心座標を返しますが、React Flow は左上座標を使用します。
// 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 には明示的な寸法が必要です。方法は 3 つあります。
1. 固定寸法(最も簡単):
g.setNode(node.id, { width: 172, height: 36 });
2. データからノードごとの寸法を指定:
g.setNode(node.id, {
width: node.data.width ?? 172,
height: node.data.height ?? 36,
});
3. 測定した寸法(最も正確):
// After React Flow measures nodes
g.setNode(node.id, {
width: node.measured?.width ?? 172,
height: node.measured?.height ?? 36,
});
レイアウトの方向
| 値 | 方向 | 用途 |
|---|---|---|
TB | 上から下 | 組織図、決定木 |
BT | 下から上 | 依存関係グラフ(依存先が下) |
LR | 左から右 | タイムライン、横方向のフロー |
RL | 右から左 | RTL レイアウト |
g.setGraph({ rankdir: 'LR' }); // Horizontal layout
必須チェック項目
レイアウトが正しいと判断する前に、以下を順番に実行してください(各ステップには客観的な合格条件があります)。
- 寸法と変換の一致 — すべてのノード ID について、その ID に対して
g.setNodeに渡すwidthとheightは、g.node(id)からposition.x/position.yを計算する際に使う数値と同じである必要があります(幅の半分/高さの半分は dagre のノードボックスと一致している必要があります)。 - 中心座標から左上座標への変換 —
positionは{ x: centerX - width/2, y: centerY - height/2 }とします。g.node(id).x/.yをそのまま使用してはいけません。 - React Flow の状態更新 — プログラムによるレイアウトの後、
setNodes/setEdgesには前回と同じ参照ではなく、新しい配列インスタンス(例:[...layouted]またはlayouted.map(...))を渡します。 - 任意の確認 — レイアウト後に
fitViewを使用する場合は、ノードが反映された後(例:次のrequestAnimationFrameまたはsetTimeout(0))に実行し、古い測定値のままsetNodesと同じ同期処理内で実行しないようにします。
実装例
基本的なレイアウト関数
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 との統合
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 フック
自動レイアウト用の再利用可能なフック:
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 };
}
使用方法:
function Flow() {
const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });
return (
<>
<button onClick={runLayout}>Re-layout</button>
<ReactFlow ... />
</>
);
}
エッジのオプション
weight と 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:weight の値が大きいエッジほど、短く直接的な経路が優先されます。
minlen:接続されたノード間のランク間隔の最小値を指定します。
// Force 2 ranks between nodes
g.setEdge('a', 'b', { minlen: 2 });
よく使われるパターン
方向に応じたハンドル位置
横方向と縦方向のレイアウトに合わせてハンドルを調整します。
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>
);
}
レイアウト遷移のアニメーション
CSS トランジションを使って位置の変化を滑らかにします。
.react-flow__node {
transition: transform 300ms ease-out;
}
プログラムによるアニメーションについては、reference.md を参照してください。
ノードグループを含むレイアウト
グループノードを dagre のレイアウト対象から除外します。
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 };
};
トラブルシューティング
ノードが重なっている
間隔を広げます。
g.setGraph({
rankdir: 'TB',
nodesep: 100, // Increase horizontal spacing
ranksep: 100, // Increase vertical spacing
});
レイアウトが更新されない
新しい配列参照を使用してください。
// Wrong - same reference
setNodes(layoutedNodes);
// Correct - new reference
setNodes([...layoutedNodes]);
ノードの位置が正しくない
座標変換を確認してください。
// 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
}
大規模グラフでのパフォーマンス
- Web Worker でレイアウトする
- レイアウトの呼び出しをデバウンスする
- レイアウト関数に
useMemoを使用する - 変更された部分のみを再レイアウトする
設定リファレンス
dagre の設定オプションの詳細については、reference.md を参照してください。