📦 Dagre React Flow — 自动图布局

v1.1.0

基于 dagre 算法为 React Flow 提供一键自动布局,支持层次图、树形结构及动态节点排列,无需手动计算坐标。

0· 77·1 当前·1 累计
by @anderskev (Kevin Anderson)·MIT-0
下载技能包
License
MIT-0
最后更新
2026/3/27
0
安全扫描
VirusTotal
无害
查看报告
OpenClaw
安全
high confidence
该纯指令技能与其声明目的(使用 dagre + React Flow 实现自动布局)一致,不请求凭据或系统访问,仅提供实现指导与示例代码。
评估建议
本技能为纯指令型,仅作为集成 dagre 与 React Flow 的实现指南,不请求密钥或写入磁盘。使用前:1) 核对并自行安装项目所需包名与版本(如 @dagrejs/dagre、@xyflow/react);2) 确认示例代码与您的 React Flow 版本及类型匹配;3) 注意缺失来源/主页信息——尽管代码无害,仍建议优先使用知名仓库或自行验证的示例;4) 无需凭据,无网络端点或隐藏安装器。...
详细分析 ▾
用途与能力
名称/描述(dagre + React Flow 自动布局)与指令及代码示例一致。所有所需项均为代码级(引入 @dagrejs/dagre 与 @xyflow/react),符合所述功能。
指令范围
SKILL.md 仅含简洁代码片段与集成指导,未指示代理读取无关文件、访问系统环境变量、向外部端点传输数据或执行越权操作。
安装机制
无安装脚本与下载;技能为纯指令型。唯一的安装提示为常规包添加(pnpm add @dagrejs/dagre),符合预期且风险低。
凭证需求
技能未声明环境变量、凭据或配置路径,与仅展示本地代码用法与包导入的内容一致。
持久化与权限
always 为 false,技能未请求持久或提升权限,亦不修改其他技能或系统级设置。
安全有层次,运行前请审查代码。

License

MIT-0

可自由使用、修改和再分发,无需署名。

运行时依赖

无特殊依赖

版本

latestv1.1.02026/3/27

- 扩展并完善 SKILL.md 文档,新增详细使用说明、高级配置及与 React Flow 的集成示例。 - 补充完整的布局函数、React Flow 集成代码及可复用的 useAutoLayout Hook 示例。 - 阐明坐标系差异、节点尺寸、布局方向等核心概念。 - 强化自动布局、层次布局及测量节点尺寸的使用指导。 - 文档现提供快速入门与深入实现的全面细节,覆盖所有主要用例。

无害

安装命令

点击复制
官方npx clawhub@latest install dagre-react-flow
镜像加速npx clawhub@latest install dagre-react-flow --registry https://cn.longxiaskill.com

技能文档

# 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

数据来源ClawHub ↗ · 中文优化:龙虾技能库