JavaScript is required

顺序树组布局

这个示例读取一棵本地层级树,将一级分支拆分为独立 relation-graph 分组,并在固定画布内为每组应用各自的左到右 folder 布局。随后它会把共享根节点重新居中到合并边界上方,添加仅展示用正交连接线,并在分支展开/收起时重新执行整体组合布局。

在共享根节点下组合顺序树分组

这个示例构建了什么

这个示例构建了一个只读层级查看器:在一个较大的 All Departments 根节点之下,将多个一级子树分组按水平方向排成一行。每个分组都来自同一棵源树,但画布通过从共享根节点到各分组根节点绘制独立的、仅用于展示的正交连接线,将它们呈现为一张图。

用户可以展开或折叠分支,使用内置工具栏和小地图进行导航,拖动或最小化浮动说明窗口,在设置抽屉中切换画布滚轮和拖拽模式,并将图导出为图片。这个示例的核心启发是布局组合:虽然辅助文字提到了多种树方向,但审查后的代码实际上对每个分组都使用了相同的、从左侧起始的 folder 布局,重点在于如何在一张固定图中把这些分组组合排列起来。

数据如何组织

源数据是一个本地嵌套树对象,包含 idtextchildren,由 getMyTreeJsonData() 异步返回。整体根节点并不是通过把完整层级传给 setJsonData(...) 来渲染的。相反,analysisGroups(...) 会取出根节点的一级子节点,并把每棵子树转换成独立的 MyTreeGroup

flatNodeData(...) 内部,每棵嵌套子树都会被转换为扁平的 nodeslines 数组。在这个预处理过程中,每个节点都会得到 data.hasChildrendata.deep,而 buildMyTreeGroup(...) 会再补充 data.myGroupId,以便布局代码查询某个分组下的全部节点。父节点还会把展开控制柄移动到右侧。在真实应用中,这种结构可以表示产品分类、组织分部、能力地图,或任何需要让一级分支在视觉上保持区分、同时又共享一个顶层根节点的分类体系。

如何使用 relation-graph

index.tsx 使用 RGProvider 包裹整个示例,MyGraph.tsx 则以 layoutName: 'fixed' 渲染 RelationGraph。这种固定的外层布局是有意为之:这个示例并没有让 relation-graph 自动放置整棵层级树,而是通过 RGHooks.useGraphInstance() 以命令式方式加载数据、为节点移动启用动画、等待初始渲染完成、运行自定义布局管理器、添加手工根连接线,最后再将视口居中并自适应缩放。

自定义逻辑位于 MyMixTreeLayout 中。它通过 addNodes(...)addLines(...) 加载注入的根节点以及每个已扁平化的分组。对于每个分组,它都会创建一个局部 folder 布局,并设置 from: 'left'fixedRootNode = truetreeNodeGapV = 20treeNodeGapH = -100。后续分组会基于前一个分组通过 getNodesRectBox(...) 得到的边界框进行定位,这就形成了并排组合的效果。所有分组放置完成后,代码会重新设置一级分组根节点的样式,计算整体边界,并把共享根节点重新定位到这一横排行的上方。

可见的顶层连接线同样是手工添加的。connectRootToChildrenTreeRoot() 会加入加粗的正交线,并标记为 forDisplayOnly,因此这些连线只是为了展示而绘制,而不是直接从扁平化后的子树数据中派生出来。交互钩子保持得很精简:onNodeExpandonNodeCollapse 会重新执行整套分组布局流程,而节点和连线点击仅用于输出日志。这个示例仍然以查看器为导向;虽然布局辅助类里有一个未使用的选择辅助方法,但它并没有暴露图编辑控制。

relation-graph 的插槽负责表现层。RGSlotOnNode 用居中的文本卡片替换默认节点内容,RGSlotOnView 则挂载 RGMiniView 作为小地图覆盖层。共享的 DraggableWindow 组件增加了一个浮动说明面板、一个可通过 setOptions(...) 实时更新画布选项的设置抽屉,以及借助 prepareForImageGeneration()restoreAfterImageGeneration() 实现的截图导出功能。随后,SCSS 文件又覆盖了内部节点文字颜色,以及选中连线的标签与描边样式。

关键交互

  • 展开或折叠任意分支都会触发 applyAllGroupLayout(),因此一次可见性变化就可能重新定位同级分组,并让共享根节点重新居中。
  • 内置工具栏和 RGMiniView 让用户无需编写自定义视口代码,也能方便地浏览这种横向展开的宽布局。
  • 浮动辅助窗口可以被拖动、最小化,并重新展开为设置抽屉。
  • 设置抽屉会在运行时修改滚轮行为和画布拖拽行为,然后借助图实例的图片生成生命周期导出截图。

关键代码片段

这段初始化流程展示了该示例如何加载原始树数据、运行自定义组合器、添加手工根连接线,然后才对视口执行自适应缩放。

const initializeGraph = async () => {
    graphInstance.enableNodeXYAnimation();
    const myTreeJsonData = await getMyTreeJsonData();
    await myMixTreeLayout.current.loadData(myTreeJsonData);
    await graphInstance.sleep(200);
    myMixTreeLayout.current.applyAllGroupLayout();
    myMixTreeLayout.current.connectRootToChildrenTreeRoot();
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};

这个预处理步骤会为每个扁平化后的节点分配一个稳定的分组 id,并把展开控制柄移到右侧,这也是后续能够按分组执行布局查询的基础。

const myGroupId = 'G-' + treeData.id;
const rootNodeId = treeData.id;
const { nodes, lines } = flatNodeData([treeData], null);
nodes.forEach(node => {
    if (!node.data) node.data = {};
    node.data.myGroupId = myGroupId;
    if (node.data.hasChildren) {
        node.expandHolderPosition = 'right';
    }
});

这条放置规则正是把多个独立树布局组合成一个顺序横向布局的关键。

if (group.refs.length > 0) {
    const refedMyGroupId = group.refs[0];
    const refedGroup = this.getGroupById(refedMyGroupId);
    if (!group.layouted) {
        this.layoutGroup(refedGroup);
    }
    const refedGroupView = this.getGroupViewInfo(refedMyGroupId);
    groupRootNodeXy.x = refedGroupView.maxX + 50;
}

这个局部布局调用说明,每棵子树复用的都是 relation-graph 的 folder 布局器,而不是在多个可见布局引擎之间切换。

const myGroupLayout = this.graphInstance.createLayout(layoutOptions as RGLayoutOptions);
myGroupLayout.isMainLayouer = false;
myGroupLayout.layoutOptions.fixedRootNode = true;
myGroupLayout.placeNodes(groupNodes, groupRootNode);

这段片段表明,顶层根连接线是作为展示连线被显式添加的,而不是直接来自原始树数据。

const line: JsonLine = {
    id: 'root-to-' + group.rootNodeId,
    from: this.rootId,
    to: group.rootNodeId,
    lineShape: RGLineShape.SimpleOrthogonal,
    fromJunctionPoint: RGJunctionPoint.bottom,
    toJunctionPoint: RGJunctionPoint.top,
    lineWidth: 4,
    forDisplayOnly: true,
    color: 'rgba(0,0,0,0.2)',
    showEndArrow: false
};

这个插槽配置让图使用自定义文本卡片,并在不改变底层节点数据的情况下添加了一个小地图覆盖层。

<RGSlotOnNode>
    {({ node }) => {
        return (
            <div className="px-2 min-w-[140px] flex items-center justify-center w-full h-full text-sm text-slate-800 font-medium select-none">
                {node.text}
            </div>
        );
    }}
</RGSlotOnNode>
<RGSlotOnView>
    <RGMiniView />
</RGSlotOnView>

这个示例的独特之处

这个示例的特别之处在于,它把固定外层图、按分组执行的 folder 布局、基于前一分组边界的顺序放置、单独注入的共享根节点,以及仅用于展示的手工根连接线组合在了一起。这种组合使它成为分组层级组合方案的强参考,而不只是一个普通的树渲染示例。

organizational-chart 相比,它采用了非常相似的分组树架构,但节点表现更简单,因此无需角色卡片式业务样式的干扰,也更容易专注研究布局模式。与 mix-layout-2 相比,它把完整结构都保留在图节点和连线中,而不是把分组锚定到 HTML 仪表盘行或隐藏的画布目标上。与 layout-treeuse-dagre-layout-2 相比,它的价值既不在于预设方向切换,也不在于外部布局引擎调优;它真正独特的启发在于,多个局部树布局流程如何在一张固定图中共存,并在展开或折叠变化后一起重新计算。

这种模式还适用于哪里

  • 产品分类查看器:每个顶层目录分支都保持自己的局部树结构,同时仍然汇聚到同一个产品组合根节点。
  • 能力地图或服务地图:一级领域需要独立的间距规则,但在管理层审查时又必须保持视觉上的连通。
  • 分组式组织浏览器:当部门被展开或折叠时,各个事业部分组可以一起重新流动排布。
  • 标准、组件或知识分类浏览器:需要围绕一个宽幅多分支层级结构提供只读小地图和导出工作流。