📦 Dagre React Flow — 自动图布局
v1.1.0基于 dagre 算法为 React Flow 提供一键自动布局,支持层次图、树形结构及动态节点排列,无需手动计算坐标。
详细分析 ▾
运行时依赖
版本
- 扩展并完善 SKILL.md 文档,新增详细使用说明、高级配置及与 React Flow 的集成示例。 - 补充完整的布局函数、React Flow 集成代码及可复用的 useAutoLayout Hook 示例。 - 阐明坐标系差异、节点尺寸、布局方向等核心概念。 - 强化自动布局、层次布局及测量节点尺寸的使用指导。 - 文档现提供快速入门与深入实现的全面细节,覆盖所有主要用例。
安装命令
点击复制技能文档
# Dagre 与 React Flow Dagre 是一个用于布局有向图的 JavaScript 库,可计算层次/树形布局中节点的最佳位置。React Flow 负责渲染,dagre 负责定位。 ## 快速开始 ``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 }; }; ` ## 核心概念 ### 坐标系差异 关键: Dagre 返回的是节点中心坐标;React Flow 使用左上角坐标。 `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 需要显式尺寸。三种方式: 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 // After React Flow measures nodes g.setNode(node.id, { width: node.measured?.width ?? 172, height: node.measured?.height ?? 36, }); ` ### 布局方向 | 值 | 方向 | 用例 | |-------|-----------|----------| | TB | 从上到下 | 组织结构图、决策树 | | BT | 从下到上 | 依赖图(依赖项在底部) | | LR | 从左到右 | 时间线、水平流 | | RL | 从右到左 | RTL 布局 | `typescript g.setGraph({ rankdir: 'LR' }); // Horizontal layout ` ## 完整实现 ### 基础布局函数 `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 }; } ` ### 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 (
onLayout('TB')}>Vertical onLayout('LR')}>Horizontal ); } export default function App() { return ( ); } ` ## useAutoLayout Hook 可复用的自动布局 hook: `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 }; } ` 用法: `tsx function Flow() { const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 }); return ( <> Re-layout ); } ` ## Edge 选项 通过 weight 与 minlen 控制 edge 路由: `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:权重越高,路径越短越直接。 minlen:强制连接节点间最小 rank 间隔。 `typescript // Force 2 ranks between nodes g.setEdge('a', 'b', { minlen: 2 }); ` ## 常见模式 ### 根据方向调整 Handle 位置 针对横向 vs 纵向布局调整 handle: `tsx function CustomNode({ data }: NodeProps) { const isHorizontal = data.direction === 'LR' || data.direction === 'RL'; return ( {data.label} ); } ` ### 带动画的布局过渡 使用 CSS 过渡实现平滑位置变化: `css .react-flow__node { transition: transform 300ms ease-out; } ` 如需程序化动画,见 reference.md。 ### 含节点组的布局 将组节点排除在 dagre 布局之外: `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 }; }; ` ## 故障排查 ### 节点重叠 增加间距: `typescript g.setGraph({ rankdir: 'TB', nodesep: 100, // Increase horizontal spacing ranksep: 100, // Increase vertical spacing }); ` ### 布局未更新 确保生成新数组引用: `typescript // Wrong - same reference setNodes(layoutedNodes); // Correct - new reference setNodes([...layoutedNodes]); ` ### 节点位置错误 检查坐标转换: `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 } ` ### 大图性能优化 - 在 Web Worker 中执行布局 - 防抖布局调用 - 对布局函数使用 useMemo` - 仅重新布局变更部分 ## 配置参考 完整 dagre 配置选项见 reference.md。