JavaScript is required

双向关系树布局

这个示例构建了一棵以单个根节点为中心的双侧树。一侧分支流向根节点,另一侧分支从根节点向外展开,并且整个图可以在不更改数据集本身的前提下,在横向树和纵向树之间切换。

以根节点为中心、支持父级箭头反向的双向树

这个示例构建了什么

这个示例构建了一棵以单个根节点为中心的双侧树。一侧分支流向根节点,另一侧分支从根节点向外展开,并且整个图可以在不更改数据集本身的前提下,在横向树和纵向树之间切换。

用户看到的是一个紧凑的层级查看器,包含矩形节点、绿色的子级侧分支,以及样式不同的流入分支。最重要的细节在于,流入侧并不只是布局完成后被重新着色;它的连线还可以切换为反向箭头,从而在图用于表达上游与下游关系时,把方向性明确地表现出来。

这个演示还包含一个悬浮工具窗口。它承载了方向选择器、父级箭头选择器,以及一个用于画布拖拽模式、滚轮行为和图片导出的共享设置面板。

数据是如何组织的

图数据以内联方式定义为一个 RGJsonData 对象,其中包含单个 rootId、扁平的 nodes 数组,以及扁平的 lines 数组。这个结构不会预先计算明确的“左侧”或“右侧”样式元数据。相反,它依赖 relation-graph 的树布局结果,来判断哪一支分支最终落在根节点的负方向一侧。

流入分支通过指向根节点的边来表示,例如 R-b -> a;而流出分支使用常规的从根到子的边,例如 a -> b。同样的模式也可以表示父子关系、上下游依赖、围绕某个焦点资产的血缘,或任何一类分裂层级结构,其中一侧应被理解为“朝向中心”,另一侧应被理解为“远离中心”。

在执行 setJsonData() 之前,唯一的预处理步骤就是按方向组装选项。代码会根据当前选择器的值,选择一个 tree 预设、节点间距、连接点以及默认线条形状。分支级别的着色和箭头逻辑则发生在更后面,也就是每个节点都拿到布局元数据之后。

relation-graph 是如何使用的

这个示例包裹在 RGProvider 中,然后 MyGraph 通过 RGHooks.useGraphInstance() 获取当前激活的图实例。渲染出来的 RelationGraph 一开始传入的是空的 options 属性,真正的配置会在运行时通过 graphInstance.setOptions() 和随后执行的 graphInstance.setJsonData() 应用进去。

在布局方面,这个演示始终使用内置的 tree 布局,并在两个预设之间切换。横向模式使用 from: 'left'treeNodeGapH: 150treeNodeGapV: 20RGJunctionPoint.lrRGLineShape.Curve2。纵向模式使用 from: 'top'treeNodeGapH: 20treeNodeGapV: 150RGJunctionPoint.tbRGLineShape.StandardCurve。两种模式都保持矩形节点,默认固定尺寸为 130 x 40,边框宽度为 0。

主要的运行时技巧是布局完成后的重新设样。数据集加载后,代码会读取 graphInstance.getNodes() 并检查 node.lot.level。层级值为负的节点会被视为流入分支。这些节点会被重新着色,它们的 ID 会被收集起来,然后再结合 graphInstance.getLines()graphInstance.updateLine() 对所有相连的线重新设样。当启用反向箭头选项时,这些线会切换为 RGLineShape.StandardOrthogonal、红色、showStartArrow: trueshowEndArrow: false。否则,同一分支仍保持正交走线、默认箭头方向,以及琥珀色。

这里没有自定义节点、连线、画布或视口插槽。这个示例依赖默认的图渲染器,转而通过 SCSS 覆盖样式:节点文本被强制设为白色,而被选中的线则获得更强的橙色描边和标签样式。

共享的 DraggableWindow 辅助组件带来了第二类图实例用法。它的 CanvasSettingsPanel 通过 RGHooks.useGraphStore() 读取图存储中的选项,使用 graphInstance.setOptions() 修改 wheelEventActiondragEventAction,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 导出当前画布。

这是一个偏查看器风格的示例,而不是编辑器。节点和连线点击处理器都接到了 RelationGraph 上,但它们只会记录被点击的对象,不会修改图。

关键交互

首要交互是方向切换。选择 Horizontal TreeVertical Tree 时,图会使用不同的树预设、不同的连接点默认值,以及不同的基础线条形状重新构建,然后重新居中并重新适配视口。

第二个核心交互是父级箭头切换。它不会重新加载数据,也不会重新计算图结构。相反,它会重新执行布局后的设样流程,并且只在那些通过负值 node.lot.level 节点连接起来的流入分支上翻转箭头。

悬浮工具窗口带来了两个辅助交互。用户可以在滚动、缩放和无操作之间切换滚轮行为,也可以在框选、移动和无操作之间切换画布拖拽行为。同一个面板还可以先向 relation-graph 请求一个适合导出的画布 DOM,再把当前图视图导出为图片。

节点和连线点击处理器在这个演示里刻意保持次要地位。它们主要用于查看和调试,并不是任何可见功能的入口。

关键代码片段

这个片段说明,数据是一个单独的内联树,包含一个焦点根节点,以及一条指向这个根节点的流入分支。

const myJsonData: RGJsonData = {
    rootId: 'a',
      nodes: [
        { id: 'a', text: 'Root Node a' },
        { id: 'R-b', text: 'R-b' }, { id: 'R-b-1', text: 'R-b-1' }, { id: 'R-b-2', text: 'R-b-2' }, { id: 'R-b-3', text: 'R-b-3' },
        { id: 'R-c', text: 'R-c' }, { id: 'R-c-1', text: 'R-c-1' }, { id: 'R-c-2', text: 'R-c-2' },

这个片段展示了横向预设:演示始终使用 layoutName: 'tree',但会将方向、间距、连接点和默认线条形状作为一个整体预设进行切换。

if (activeTabName === 'h') {
    layoutOptions = {
        layoutName: 'tree',
        from: 'left',
        treeNodeGapH: 150,
        treeNodeGapV: 20
    };
    defaultJunctionPoint = RGJunctionPoint.lr;
    defaultLineShape = RGLineShape.Curve2;
}

这个片段展示了方向变化会重新构建配置后的图,然后在视口中完成居中和适配。

const graphOptions: RGOptions = {
    debug: false,
    layout: layoutOptions,
    defaultNodeShape: RGNodeShape.rect,
    defaultNodeWidth: 130,
    defaultNodeHeight: 40,
    defaultLineShape,
    defaultJunctionPoint: defaultJunctionPoint,
    defaultNodeBorderWidth: 0
};

graphInstance.setOptions(graphOptions);
await graphInstance.setJsonData(myJsonData);

这个片段展示了布局后分类这一步如何基于 node.lot.level 推导分支身份,而不是依赖额外的输入标记。

for (const node of graphInstance.getNodes()) {
    if (node.lot && node.lot.level !== undefined && node.lot.level < 0) {
        graphInstance.updateNode(node, { color: '#ca8a04' });
        leftNodes.push(node);
    } else {
        graphInstance.updateNode(node, { color: '#3f9802' });
    }
}
const leftNodeIds: string[] = leftNodes.map(n => n.id);

这个片段展示了分支级别的箭头反转。该切换会在不重新加载数据集的情况下,修改现有连线样式。

if (leftNodeIds.includes(line.from) || leftNodeIds.includes(line.to)) {
    if (reverseParentArrows) {
        graphInstance.updateLine(line, {
            lineShape: RGLineShape.StandardOrthogonal,
            showStartArrow: true,
            showEndArrow: false,
            color: '#ff0000'
        });
    }

这个片段展示了共享工具面板在创建图片 blob 之前,如何使用 relation-graph 的导出生命周期。

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

这个示例的独特之处

根据对比数据,这个示例并不只是另一个树方向切换器。它的独特组合在于:以根节点为中心的双向 tree、通过 node.lot.level 进行布局后分支分类、基于极性的重新着色,以及可选的流入侧箭头反转,而且这一切都放在一个极简的查看器外壳中完成。

bothway-tree2 相比,这个版本把控制预算用在父级侧的箭头语义和正交分支重设样上,而不是分支级别的线标签位置控制上。与 layout-tree 相比,重点不在于通用的一方向树重布局,而在于一个以中心为核心的双侧层级结构,其中一条分支保留自己的颜色和箭头策略。与 io-tree-layout 相比,它保持使用内置的 tree 布局,而不是切换到 io-tree 走线和连接锚点重写。

稀有度元数据也支持一个更聚焦的判断:方向选择器、父级箭头选择器、按方向切换的连接点默认值,以及对现有连线进行运行时重设样,都是示例集合中较少见的特性。这使得这个演示在面对“不是画任意一棵树,而是围绕一个焦点节点,把反向一侧的分支以不同语义画出来”这类问题时,成为一个很强的起点。

这种模式还适用于哪里

这种模式很适合迁移到上下游依赖分析场景,例如包依赖查看器、围绕某张表的数据血缘,或者服务拓扑中调用方与被调用方需要分布在某个焦点服务两侧的场景。

它也适用于那些需要强调父级侧与子级侧差异的产品结构,包括物料清单树、继承查看器、类目祖先浏览器,以及主管与下属需要在视觉上分离、但又不想拆成两张图的组织场景。

布局后分类这一技巧并不局限于树。任何图只要布局元数据能够标识某条分支、某个深度范围,或焦点节点的某一侧,就可以用同样的 getNodes()updateLine() 模式,在布局引擎完成定位之后,为不同分支应用专属箭头、颜色或走线规则。