使用 dagre 与 React Flow (@xyflow/react) 实现自动图布局。适用于自动布局、层级布局、树形结构或节点排列等场景。
Dagre 带有 React Flow.是一项面向实际任务的技能,主要用于Dagre 是用于绘制定向图表的 JavaScript 库;它计算出用于等级/ 树形布局的最佳节点位置;
使用时应结合输入条件选择合适的执行方式,核对必要参数、依赖环境与输出内容,并按原始要求处理异常情况。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。
执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。该技能适合用于一次性任务,也可以接入自动化工作流,与其他技能或上层代理配合完成更完整的业务链路;在组合使用时,应明确每一步的输入输出关系,并避免不同步骤之间出现参数冲突。
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 }, // 从中心点转为左上角对齐
};
});
return { nodes: layoutedNodes, edges };
};
关键注意: Dagre 返回的是节点中心坐标;React Flow 使用的是节点左上角坐标。
// Dagre 输出:节点中心位置
const dagrePos = g.node(nodeId); // { x: 100, y: 50 } = 中心点
// React Flow 期望:左上角坐标
const rfPosition = {
x: dagrePos.x - nodeWidth / 2,
y: dagrePos.y - nodeHeight / 2,
};
Dagre 需要显式指定节点宽高。三种常用方式:
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. 实际测量尺寸(最精确):
// React Flow 完成节点尺寸测量后
g.setNode(node.id, {
width: node.measured?.width ?? 172,
height: node.measured?.height ?? 36,
});
| 值 | 方向 | 适用场景 |
|---|---|---|
TB |
自上而下 | 组织架构图、决策树 |
BT |
自下而上 | 依赖关系图(依赖项置于底部) |
LR |
从左到右 | 时间线、水平流程图 |
RL |
从右到左 | RTL(从右向左)布局 |
g.setGraph({ rankdir: 'LR' }); // 水平布局
在将布局视为正确前,必须按顺序执行以下检查(每一步均有明确的通过条件):
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 原始值。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; // 水平间距
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 };
}
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' },
];
// 应用初始布局
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 (
);
}
export default function App() {
return (
);
}
可复用的自动布局 Hook:
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 };
}
使用方式:
function Flow() {
const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });
return (
<>
>
);
}
通过 weight 和 minlen 控制边的路径走向:
edges.forEach((edge) => {
g.setEdge(edge.source, edge.target, {
weight: edge.data?.priority ?? 1, // 值越大,路径越短、越直接
minlen: edge.data?.minRanks ?? 1, // 连接节点间的最小层级间隔
});
});
weight:权重越高的边,在布局中越优先采用更短、更直接的路径。
minlen:强制规定相连节点之间至少相隔的层级数量。
// 强制节点 a 与 b 之间相隔 2 个层级
g.setEdge('a', 'b', { minlen: 2 });
针对水平与垂直布局分别设置 Handle 位置:
function CustomNode({ data }: NodeProps) {
const isHorizontal = data.direction === 'LR' || data.direction === 'RL';
return (
{data.label}
);
}
使用 CSS transition 实现平滑的位置变化:
.react-flow__node {
transition: transform 300ms ease-out;
}
如需程序化控制动画,请参考 reference.md。
在 Dagre 布局中排除分组节点(group nodes):
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 };
};
增大节点间距:
g.setGraph({
rankdir: 'TB',
nodesep: 100, // 增大水平间距
ranksep: 100, // 增大垂直间距
});
确保传入新数组引用:
// 错误 —— 使用同一引用 setNodes(layoutedNodes); // 正确 —— 创建新引用 setNodes([...layoutedNodes]);
检查坐标转换逻辑:
// Dagre 返回中心点,React Flow 需要左上角
position: {
x: pos.x - width / 2, // 不能仅用 pos.x
y: pos.y - height / 2, // 不能仅用 pos.y
}
useMemo 缓存完整 Dagre 配置选项请参阅 reference.md。