JavaScript is required

Dagre位置布局运行时控制

这个示例构建固定布局 relation-graph 场景,让 relation-graph 负责节点渲染与测量,再把位置计算交给 Dagre。重点是如何在运行时保持 Dagre 的 ranker 和间距参数可调,同时保留自定义节点卡片、正交带标签连线、小地图和共享画布工具。

固定 relation-graph 场景中的 Dagre 运行时位置调优

这个示例构建了什么

这个示例构建了一个全屏、只读的 relation-graph 查看器。它会加载一份静态网络,让 relation-graph 完成节点渲染与尺寸测量,然后仅使用 Dagre 计算节点位置。最终视图保留了自定义节点卡片、带标签的正交连接线、浮动辅助窗口以及小地图,而不是切换到 Dagre 专用渲染器。

用户可以在同一个图实例保持挂载的情况下,重新调整 Dagre 的 rankernodesepranksep。他们还可以拖动或最小化辅助窗口、打开共享画布设置、导出图片,并通过内嵌的小视图检查重新布局后的图。这个示例的重点是运行时重新布局循环,而不只是初始的 Dagre 接入。

数据是如何组织的

数据集以内联方式声明为一个 RGJsonData 对象,其中包含 rootId: 'root'、一个扁平的 nodes 数组以及一个扁平的 lines 数组。节点文本刻意保持通用,这样示例就能把重点放在布局行为上,而不是业务语义上。

在自定义布局运行之前,还有一个小型预处理步骤。代码会先为每条缺少 id 的线分配一个 id,调用 setJsonData(...) 让 relation-graph 渲染并测量每个节点,然后检查相对于根节点的实时连线,并通过 placeTexttextOffsetY 重写每条线的标签位置。

这种数据形态非常适合依赖图、工作流阶段、服务拓扑、组织分支,或任何希望保持图数据结构简单、并把坐标计算委托给外部布局引擎的网络场景。

relation-graph 是如何使用的

入口组件使用 RGProvider 包裹整个页面,而 MyGraph 通过 RGHooks.useGraphInstance() 作为主要的运行时 API。图本身始终保持在 layoutName: 'fixed',这是这种模式的关键设置:relation-graph 负责渲染场景并暴露测量后的节点尺寸,而 Dagre 在内置布局系统之外处理坐标计算。

RGOptions 对象设置了矩形节点、正交连接线、上下方向的连接点、无默认节点边框、灰色线条颜色以及白色节点填充。调用 setJsonData(...) 之后,代码会使用 getNodeById(...)getLinks()updateLine(...) 在首次执行 Dagre 之前调整线标签的位置。随后,自定义的 doMyLayout() 函数会基于 getNodes()getLines() 构建一个 Dagre 图,使用测量得到的 el_Wel_H 值来确保尺寸准确,运行 dagre.layout(g),再通过 updateNodePosition(...) 把结果位置写回。每次布局结束后都会调用 moveToCenter()zoomToFit()

运行时调优由 React state 驱动。dagreRankerdagreGapHdagreGapV 保存在组件状态中,并且 useEffect 会在它们任意一个变化时重新执行 doMyLayout()RGSlotOnNode 用来自定义节点渲染,使根节点显示为更大的灰色卡片,其余节点显示为较小的带边框标签。RGSlotOnView 则把 RGMiniView 挂载为视口覆盖层。

浮动的 DraggableWindow 是共享辅助 UI,但它对这个示例的工作模式同样重要。它承载 Dagre 控件,支持拖动和最小化,并能打开 CanvasSettingsPanel。后者通过 relation-graph hooks 修改滚轮行为、修改画布拖拽行为,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 导出图像。样式拆分在 slot 标记和 SCSS 之间:节点卡片定义在 JSX 中,而 .rg-line-label 则被重设为带灰色边框的小型白色徽标样式。

关键交互

最重要的交互是实时 Dagre 调优。修改 ranker 选择器或任意一个间距滑块,都会立即在已经加载的图上重新执行外部 Dagre 布局,这样用户就能在不重建数据的情况下比较不同布局变体。

辅助窗口也是体验的一部分。它可以被拖到一边、最小化,或者切换到设置覆盖层,在那里修改滚轮行为、修改画布拖拽行为,并下载图的图片。

即使经过多次重新布局,导航依然保持实用,因为这个示例始终在图视图中挂载 RGMiniView。节点和连线点击处理器是存在的,但它们只是在控制台输出对象,并不是主要功能的一部分。

关键代码片段

这个片段展示了 relation-graph 保持在 fixed 模式下运行,因此 Dagre 可以提供位置,而无需替换图渲染器的其余部分。

const graphOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'fixed' // 使用自定义布局时,建议设为 fixed
    },
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.StandardOrthogonal,
    defaultJunctionPoint: RGJunctionPoint.tb,
    defaultNodeBorderWidth: 0,
    defaultLineColor: '#666',
    defaultNodeColor: '#fff'
};

这个片段说明,示例会先挂载图,然后在运行 Dagre 之前,基于实时连线重写线标签位置的启发式规则。

await graphInstance.setJsonData(myJsonData);
const rootNode = graphInstance.getNodeById(myJsonData.rootId)!;
graphInstance.getLinks().forEach(link => {
    if (link.fromNode.y < rootNode.y) {
        graphInstance.updateLine(link.line.id, { placeText: 'start', textOffsetY: 20 });
    } else {
        graphInstance.updateLine(link.line.id, { placeText: 'end', textOffsetY: -20 });
    }
});
await doMyLayout();

这个片段展示了“渲染-测量-布局”这一步:Dagre 消费 relation-graph 已测量出的节点尺寸和现有边数据。

const g = new dagre.graphlib.Graph();
g.setGraph({ nodesep: dagreGapH, ranksep: dagreGapV, ranker: dagreRanker });

graphInstance.getNodes().forEach(node => {
    g.setNode(node.id, { width: node.el_W || 100, height: node.el_H || 40 });
});

graphInstance.getLines().forEach(line => {
    g.setEdge(line.from, line.to, line);
});
dagre.layout(g);

这个片段展示了回写步骤:它把已有的 relation-graph 节点移动到 Dagre 计算出的坐标上,然后重新适配视口。

g.nodes().forEach((nodeId: string) => {
    const dagreNode = g.node(nodeId);
    graphInstance.updateNodePosition(nodeId, dagreNode.x, dagreNode.y);
});

graphInstance.moveToCenter();
graphInstance.zoomToFit();

这个片段展示了 Dagre 调优是作为普通的 React state 暴露出来的,而不是一次性的初始化配置。

<SimpleUISelect
    data={[
        { value: 'network-simplex', text: 'network-simplex' },
        { value: 'longest-path', text: 'longest-path' },
        { value: 'tight-tree', text: 'tight-tree' }
    ]}
    currentValue={dagreRanker}
    onChange={(newValue: string) => { setDagreRanker(newValue); }}
/>

这个片段说明,在图已经挂载之后,状态变化会触发重新布局。

useEffect(() => {
    doMyLayout();
}, [dagreRanker, dagreGapH, dagreGapV]);

这个片段展示了自定义节点 slot,它让根节点在视觉上比其他节点更突出。

<RGSlotOnNode>
    {({ node }) => {
        return (
            node.id === 'root' ? (
                <div className="px-2 min-w-[200px] min-h-[50px] rounded border border-gray-500 bg-gray-100 flex items-center justify-center w-full h-full text-sm text-slate-800 font-bold select-none">
                    {node.text}
                </div>
            ) : (
                <div className="px-2 min-w-[100px] rounded border border-gray-500 flex items-center justify-center w-full h-full text-sm text-slate-800 font-medium select-none">
                    {node.text}
                </div>

这个片段展示了 SCSS 覆盖样式:它把连接线标签变成了小型白色标签片,而不是保留默认的 line-label 样式。

.rg-line-peel {
    .rg-line-label {
        background-color: #fff;
        border: #666 solid 1px;
        font-size: 10px;;
    }
}

这个示例的独特之处

对比数据表明,最接近的示例是 use-dagre-layout,但这个示例更进一步,强调运行时实验能力。两个示例都采用相同的“渲染-测量-Dagre-回写”总体模式,但这个示例在初始加载之后仍然保持 rankernodesepranksep 可实时调整,因此当团队需要在同一个已挂载图上比较仅影响位置的 Dagre 变体时,它会是更好的起点。

use-d3-layout 相比,它的显著特征是稳定性,而不是几何变换。这个示例保持节点卡片和连接线样式稳定,仅重写位置;而 D3 示例会切换布局家族,并对节点几何本身做更多重写。

io-tree-layout 相比,它的重心是外部布局集成,而不是内置预设切换。图始终保持在 layoutName: 'fixed',算法位于 relation-graph 之外,而这篇文章的核心经验是如何让这种外部布局在运行时保持可控。

稀有度记录同样强调的是功能组合,而不是任何单一技巧:实时 Dagre 参数调优、线标签位置启发式、强调根节点的 slot 卡片、带标签的正交连接线,以及借助小地图的导航能力,都被组合进了同一个紧凑的布局工作台中。

这种模式还适用于哪里

这种模式非常适合内部布局评估工具、依赖关系浏览器、服务地图、组织网络,以及那些希望在图保持挂载的同时,让用户比较外部算法间距或排序变体的工作流查看器。

当 relation-graph 作为主要查看器外壳,而布局坐标来自其他来源时,它也同样有用。Dagre 可以被替换为其他外部布局器,甚至是后端服务;而 relation-graph 这一侧的结构仍然可以继续处理 slots、连接线样式、视口适配、画布设置以及导出功能。