JavaScript is required

智能树布局

这个示例展示如何将通用关系数据加载到 relation-graph 的智能树布局,并在运行时在横向与纵向预设间切换。它还演示了重布局前对现有节点重设尺寸、重置线形的额外步骤,并复用带小地图、画布设置和图片导出的共享查看器外壳。

在水平与垂直视图之间切换 Smart Tree 布局

这个示例构建了什么

这个示例围绕一份通用关系数据集构建了一个只读的 smart-tree 布局演示界面。默认视图是一个从左到右的树,节点为琥珀色矩形,连接线为曲线;同时,已经挂载的同一张图可以通过浮动窗口中的选择器切换为自上而下的预设。在自上而下模式中,节点会变成高而窄的竖向卡片,标签文字也会变为竖排,因此这个变化不仅关乎方向,也关乎可读性。

页面包含一个可拖拽的说明面板、一个用于控制滚轮和拖拽行为的设置浮层、一个内嵌的 mini-view,以及图片导出功能。这个示例最重要的思路是:布局变化时并不会重新加载另一份数据集。它会保持图已挂载的状态,并更新当前图,使每一种朝向都保持清晰可读。

数据是如何组织的

图数据是一个内联的 RGJsonData 对象,包含 rootId: 'root'、41 个节点和 40 条有向连线。它采用扁平结构而不是嵌套结构:节点以 { id, text } 对象数组声明,关系则以 { from, to, text } 边来声明。这使得该示例更接近通用的有向关系数据,而不是手工编写的树形 JSON 结构。

在调用 setJsonData() 之前,除了在 initializeGraph() 中构造这个对象之外,没有额外的预处理步骤。真正有意义的预处理发生在每次重新布局之前:代码会选出当前启用的预设,将所有现有节点更新为该预设对应的宽高,将所有现有连线更新为该预设对应的线条形状,然后再重新执行布局。在真实系统中,同样的模式可以应用于依赖链、推荐树、所有权层级、审批路径或调查路径等场景,这些场景都需要对同一组关系提供多种可读的展示方式。

relation-graph 是如何使用的

这个示例包裹在 RGProvider 中,主图组件和共享的浮动工具面板都通过 RGHooks.useGraphInstance() 获取实时的图实例。RelationGraph 只挂载一次,并使用水平预设;运行时的切换则通过 relation-graph 实例 API 以命令式方式完成。

两个 RGOptions 预设驱动了核心行为。它们都使用内置的 smart-tree 布局,但并不仅仅是改动 layout.from。水平预设使用 120x30 的矩形节点、RGJunctionPoint.lr、较大的水平间距、曲线连线,以及琥珀色的节点和线条颜色。垂直预设则切换为 30x120 的节点、RGJunctionPoint.tb、较大的垂直间距,并保留相同的曲线连接线风格。

当选择器修改 layoutFrom 时,示例会先通过 setOptions() 应用目标预设,然后使用 getNodes()updateNode() 重写所有已挂载节点的几何属性。它还会使用 getLines()updateLine() 重置线条形状,然后依次调用 sleep(100)doLayout()moveToCenter()zoomToFit()。这一流程是本示例最重要的技术要点:预设中的默认值不会自动重设图中已经存在节点的样式,因此必须先把当前图归一化,再重新运行布局。

这个示例还使用 RGSlotOnViewRGMiniView 放置在图的覆盖层中。共享的 CanvasSettingsPanel 通过 RGHooks.useGraphStore() 读取 dragEventActionwheelEventAction,通过 setOptions() 更新它们,并在 DOM 转图片导出流程前后分别调用 prepareForImageGeneration()restoreAfterImageGeneration()

这里并没有结构化编辑流程。虽然接入了 onNodeClickonLineClick,但它们只会记录被点击的对象。样式通过本地的 my-relation-graph.scss 定制:它会在默认主题下强制节点文字为白色,并在启用自上而下布局类时将标签切换为 writing-mode: vertical-rl

关键交互

  • 布局选择器可以在水平和垂直 smart-tree 预设之间切换,并触发选项替换、实时节点与线条归一化、重新布局、重新居中以及缩放适配。
  • 说明窗口可以拖拽和最小化,因此说明性界面外壳可以移开而不必离开页面。
  • 设置浮层可以把滚轮行为切换为滚动、缩放或无动作,并把画布拖拽行为切换为框选、移动或无动作。
  • Download Image 操作可以导出当前图视图,而不需要重新构建整张图。

关键代码片段

这个片段表明,该示例定义了一个专门的水平 smart-tree 预设,而不是依赖默认布局行为。

const graphOptionsH: RGOptions = {
    layout: {
        layoutName: 'smart-tree',
        treeNodeGapH: 300,
        treeNodeGapV: 10,
        from: 'left'
    },
    defaultNodeWidth: 120,
    defaultNodeHeight: 30,
    defaultJunctionPoint: RGJunctionPoint.lr
};

这个片段展示了,自上而下预设改变的不只是方向,也包括几何属性。

const graphOptionsV: RGOptions = {
    layout: {
        layoutName: 'smart-tree',
        treeNodeGapH: 10,
        treeNodeGapV: 300,
        from: 'top'
    },
    defaultNodeWidth: 30,
    defaultNodeHeight: 120,
    defaultJunctionPoint: RGJunctionPoint.tb
};

这个片段证明,示例会在重新执行布局之前更新已经加载的节点。

const targetOptions = layoutFrom === 'left' ? graphOptionsH : graphOptionsV;
graphInstance.setOptions(targetOptions);
graphInstance.getNodes().forEach((node) => {
    graphInstance.updateNode(node, {
        width: targetOptions.defaultNodeWidth,
        height: targetOptions.defaultNodeHeight
    });
});

这个片段补全了重新布局流程:先重置线条形状,再重新计算布局,并重新适配视口。

graphInstance.getLines().forEach((line) => {
    graphInstance.updateLine(line, {
        lineShape: targetOptions.defaultLineShape
    });
});
await graphInstance.sleep(100);
await graphInstance.doLayout();
graphInstance.moveToCenter();
graphInstance.zoomToFit();

这个片段展示了,界面通过一个直接的双选项控件暴露朝向切换功能。

<SimpleUISelect
    data={[
        { value: 'left', text: 'Horizontal Tree' },
        { value: 'top', text: 'Vertical Tree' }
    ]}
    currentValue={layoutFrom}
    onChange={(newValue: string) => { setLayoutFrom(newValue); }}
/>

这个片段证明,自上而下模式改变的不只是节点位置,也包括文字流向。

.my-graph.my-layout-top {
    .relation-graph {
        .rg-node-peel {
            .rg-node {
                .rg-node-text {
                    writing-mode: vertical-rl;
                }
            }
        }
    }
}

这个片段展示了,共享工具外壳如何在截图导出时,把 relation-graph 的图片生成 API 包装起来使用。

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

在已准备的对比集合中,这个示例的独特性并不只是因为它能在水平和垂直树视图之间切换。其他示例也能做到这一点。它真正突出的地方在于:演示的主题本身就是内置的 smart-tree 布局,而且同一份通用关系数据会保持挂载状态,再根据不同朝向对图进行适配。

layout-tree 相比,这个示例更适合作为将 smart-tree 应用于扁平有向关系数据的参考,而不是用于更传统的树形示例。与 io-tree-layout 相比,它专注于面向不同朝向的 smart-tree 重布局,而不是布局完成后的逐连线布线与锚点重写。与 bothway-treelayout-diy 相比,它避开了分支极性语义与自定义坐标,让重点始终围绕不同朝向下的可读性。

这组对比还凸显出一个少见的组合:两个 smart-tree 预设、在 doLayout() 之前对节点和线条进行实时归一化、与朝向相关的节点几何属性、自上而下模式下的竖排标签、曲线连接线,以及一个可复用的查看器外壳,附带 mini-view 和导出支持。当问题核心是展示方式切换,而不是自定义布线或编辑能力时,这个示例是一个很好的起点。

这种模式还适用于哪些场景

这种模式非常适合这样的场景:同一张有向图需要同时支持横向和纵向展示,而又不想复制数据模型。示例包括:既需要宽屏桌面视图又需要便于导出的审批链、需要在探索模式与文档模式之间切换的依赖树、标签较长的联盟或所有权网络,以及需要针对不同屏幕提供替代朝向的调查图或升级路径图。

这个可复用的思路很直接:保留一份 RGJsonData 载荷,定义少量与朝向相关的预设,并在重新布局之前把当前节点和线条的几何属性归一化。当朝向变化还会影响标签方向、节点纵横比或连接线样式时,这个示例展示了让结果保持可读所需的额外步骤。