mcpField
D
dagre-react-flow

dagre-react-flow

github.com/existential-birdsデザイン・UI85243 回インストール11 週間前に更新
リポジトリ

dagre-react-flow について

dagre-react-flowは、有向グラフのノードを自動配置したいJavaScript開発者向けに、DagreのレイアウトをReact Flowで使う方法を説明します。座標変換、ノード寸法、レイアウト方向、再利用可能な自動レイアウトフックを扱います。

主な機能

  1. 01階層型有向グラフのレイアウト計算
  2. 024種類のレイアウト方向に対応
  3. 03DagreとReact Flowの座標変換
  4. 04ノード寸法と間隔の設定
  5. 05自動レイアウトフックの提供

活用シーン

  1. 01組織図や決定木の配置
  2. 02横向きのタイムラインやフローの作成
  3. 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

必須チェック項目

レイアウトが正しいと判断する前に、以下を順番に実行してください(各ステップには客観的な合格条件があります)。

  1. 寸法と変換の一致 — すべてのノード ID について、その ID に対して g.setNode に渡す width と height は、g.node(id) から position.x/position.y を計算する際に使う数値と同じである必要があります(幅の半分/高さの半分は dagre のノードボックスと一致している必要があります)。
  2. 中心座標から左上座標への変換 — position は { x: centerX - width/2, y: centerY - height/2 } とします。g.node(id).x/.y をそのまま使用してはいけません。
  3. React Flow の状態更新 — プログラムによるレイアウトの後、setNodes/setEdges には前回と同じ参照ではなく、新しい配列インスタンス(例:[...layouted] または layouted.map(...))を渡します。
  4. 任意の確認 — レイアウト後に 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 を参照してください。

コメント