dagre-react-flow

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dagre with React Flow

结合React Flow使用Dagre

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.
Dagre是一个用于布局有向图的JavaScript库。它可为层级/树形布局计算最优的节点位置。React Flow负责渲染;dagre负责节点定位。

Quick Start

快速开始

bash
pnpm add @dagrejs/dagre
typescript
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 };
};
bash
pnpm add @dagrejs/dagre
typescript
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 }, // 从中心转换为左上角
    };
  });

  return { nodes: layoutedNodes, edges };
};

Core Concepts

核心概念

Coordinate System Difference

坐标系差异

Critical: Dagre returns center coordinates; React Flow uses top-left.
typescript
// 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返回的是节点中心坐标;React Flow使用左上角坐标。
typescript
// Dagre输出:节点中心坐标
const dagrePos = g.node(nodeId); // { x: 100, y: 50 } = 中心位置

// React Flow需要:左上角坐标
const rfPosition = {
  x: dagrePos.x - nodeWidth / 2,
  y: dagrePos.y - nodeHeight / 2,
};

Node Dimensions

节点尺寸

Dagre requires explicit dimensions. Three approaches:
1. Fixed dimensions (simplest):
typescript
g.setNode(node.id, { width: 172, height: 36 });
2. Per-node dimensions from data:
typescript
g.setNode(node.id, {
  width: node.data.width ?? 172,
  height: node.data.height ?? 36,
});
3. Measured dimensions (most accurate):
typescript
// After React Flow measures nodes
g.setNode(node.id, {
  width: node.measured?.width ?? 172,
  height: node.measured?.height ?? 36,
});
Dagre需要明确的节点尺寸。有三种设置方式:
1. 固定尺寸(最简单):
typescript
g.setNode(node.id, { width: 172, height: 36 });
2. 从节点数据中获取尺寸:
typescript
g.setNode(node.id, {
  width: node.data.width ?? 172,
  height: node.data.height ?? 36,
});
3. 测量实际尺寸(最准确):
typescript
// 在React Flow测量节点尺寸后调用
g.setNode(node.id, {
  width: node.measured?.width ?? 172,
  height: node.measured?.height ?? 36,
});

Layout Directions

布局方向

ValueDirectionUse Case
TB
Top to BottomOrg charts, decision trees
BT
Bottom to TopDependency graphs (deps at bottom)
LR
Left to RightTimelines, horizontal flows
RL
Right to LeftRTL layouts
typescript
g.setGraph({ rankdir: 'LR' }); // Horizontal layout
取值方向使用场景
TB
从上到下组织结构图、决策树
BT
从下到上依赖关系图(依赖项在底部)
LR
从左到右时间线、水平流程
RL
从右到左RTL布局
typescript
g.setGraph({ rankdir: 'LR' }); // 水平布局

Complete Implementation

完整实现

Basic Layout Function

基础布局函数

typescript
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 };
}
typescript
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;  // 水平间距
  ranksep?: number;  // 层级间垂直间距
}

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

React Flow集成

tsx
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>
  );
}
tsx
import { useCallback } from 'react';
import {
  ReactFlow,
  useNodesState,
  useEdgesState,
  useReactFlow,
  ReactFlowProvider,
} from '@xyflow/react';
import { getLayoutedElements } from './layout';

const initialNodes = [
  { id: '1', data: { label: '开始' }, position: { x: 0, y: 0 } },
  { id: '2', data: { label: '流程' }, position: { x: 0, y: 0 } },
  { id: '3', data: { label: '结束' }, position: { x: 0, y: 0 } },
];

const initialEdges = [
  { id: 'e1-2', source: '1', target: '2' },
  { id: 'e2-3', source: '2', target: '3' },
];

// 应用初始布局
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]);

    // 布局完成后适配视图并添加动画
    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')}>垂直布局</button>
        <button onClick={() => onLayout('LR')}>水平布局</button>
      </div>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        fitView
      />
    </div>
  );
}

export default function App() {
  return (
    <ReactFlowProvider>
      <Flow />
    </ReactFlowProvider>
  );
}

useAutoLayout Hook

useAutoLayout 钩子

Reusable hook for automatic layout:
typescript
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:
tsx
function Flow() {
  const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });

  return (
    <>
      <button onClick={runLayout}>Re-layout</button>
      <ReactFlow ... />
    </>
  );
}
用于自动布局的可复用钩子:
typescript
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]);

  // 初始化时自动执行布局
  useEffect(() => {
    if (nodesInitialized && !layoutApplied.current) {
      runLayout();
      layoutApplied.current = true;
    }
  }, [nodesInitialized, runLayout]);

  return { runLayout };
}
使用示例:
tsx
function Flow() {
  const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });

  return (
    <>
      <button onClick={runLayout}>重新布局</button>
      <ReactFlow ... />
    </>
  );
}

Edge Options

边选项

Control edge routing with weight and minlen:
typescript
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.
typescript
// Force 2 ranks between nodes
g.setEdge('a', 'b', { minlen: 2 });
通过weight和minlen控制边的路由:
typescript
edges.forEach((edge) => {
  g.setEdge(edge.source, edge.target, {
    weight: edge.data?.priority ?? 1,  // 值越大,路径越直接
    minlen: edge.data?.minRanks ?? 1,  // 节点间的最小层级数
  });
});
weight:权重越高的边,会优先使用更短、更直接的路径。
minlen:强制设置相连节点之间的最小层级间隔。
typescript
// 强制节点间间隔2个层级
g.setEdge('a', 'b', { minlen: 2 });

Common Patterns

常见模式

Handle Position Based on Direction

根据方向调整连接点

Adjust handles for horizontal vs vertical layouts:
tsx
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>
  );
}
针对水平和垂直布局调整节点连接点:
tsx
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:
css
.react-flow__node {
  transition: transform 300ms ease-out;
}
For programmatic animation, see reference.md.
使用CSS过渡实现平滑的位置变化:
css
.react-flow__node {
  transition: transform 300ms ease-out;
}
如需通过代码实现动画,请参考 reference.md

Layout with Node Groups

包含节点组的布局

Exclude group nodes from dagre layout:
typescript
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 };
};
将组节点排除在dagre布局之外:
typescript
const layoutWithGroups = (nodes: Node[], edges: Edge[]) => {
  // 区分普通节点和组节点
  const regularNodes = nodes.filter((n) => n.type !== 'group');
  const groupNodes = nodes.filter((n) => n.type === 'group');

  // 仅对普通节点执行布局
  const { nodes: layouted } = getLayoutedElements(regularNodes, edges);

  // 合并节点
  return { nodes: [...groupNodes, ...layouted], edges };
};

Troubleshooting

故障排查

Nodes Overlapping

节点重叠

Increase spacing:
typescript
g.setGraph({
  rankdir: 'TB',
  nodesep: 100,  // Increase horizontal spacing
  ranksep: 100,  // Increase vertical spacing
});
增大间距:
typescript
g.setGraph({
  rankdir: 'TB',
  nodesep: 100,  // 增大水平间距
  ranksep: 100,  // 增大垂直间距
});

Layout Not Updating

布局未更新

Ensure new array references:
typescript
// Wrong - same reference
setNodes(layoutedNodes);

// Correct - new reference
setNodes([...layoutedNodes]);
确保使用新的数组引用:
typescript
// 错误 - 引用未改变
setNodes(layoutedNodes);

// 正确 - 创建新引用
setNodes([...layoutedNodes]);

Nodes at Wrong Position

节点位置错误

Check coordinate conversion:
typescript
// 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
}
检查坐标转换是否正确:
typescript
// Dagre返回中心坐标,React Flow需要左上角坐标
position: {
  x: pos.x - width / 2,   // 不能直接使用pos.x
  y: pos.y - height / 2,  // 不能直接使用pos.y
}

Performance with Large Graphs

大型图的性能问题

  • Layout in a Web Worker
  • Debounce layout calls
  • Use
    useMemo
    for layout function
  • Only re-layout changed portions
  • 在Web Worker中执行布局
  • 对布局调用进行防抖处理
  • 使用
    useMemo
    缓存布局函数
  • 仅对修改的部分重新布局

Configuration Reference

配置参考

See reference.md for complete dagre configuration options.
完整的dagre配置选项请查看 reference.md