JavaScript is required

自定义 GraphInstance 混合布局

这个示例用自定义 `RelationGraphCore` 子类替换默认 relation-graph 内核,从 `graphOptions.data` 读取两份内联数据并组合为“中心布局 + 树布局”的同画布场景。它是 provider 级图内核注入、自定义 `doLayout()` 编排和单画布混合布局组合的聚焦参考。

使用自定义 GraphInstance 加载数据并组合两种布局

这个示例构建了什么

这个示例构建了一个偏查看器风格的 relation-graph 场景,在一个全高画布中渲染两组彼此断开的数据。左侧呈现为一个居中的小型圆形节点簇,使用直线连接;右侧则呈现为一棵从左到右展开的树,节点是更宽的矩形,并带有弯曲且带标签的边。

用户不会直接编辑图内容。他们会查看预先构建好的场景,使用右下角的内置工具栏,拖动或最小化浮动辅助窗口,在其设置面板中切换滚轮和拖拽行为,并将当前画布导出为图片。这里的关键点在于,这个混合场景由一个自定义 RelationGraphCore 子类持有,而不是仅通过组件层面的按钮处理器来拼装。

数据如何组织

图数据在 MyGraph.tsx 中声明为两个内联的 RGJsonData 对象:根节点为 amyLeftJsonData,以及根节点为 2myRightJsonData。示例没有把它们合并为一个载荷再调用 setJsonData(...),而是把这两个对象都放在 graphOptions.data 下,让自定义 graph instance 在布局阶段消费它们。

在节点摆放之前还有一个预处理步骤。在 MyGraphInstance.doLayout() 内部,两组数据会在插入前先在内存中重设样式:右侧节点会变为半透明且填充透明,左侧节点会变成 80x80 的圆形并使用更小的文字,左侧连线也会从图的默认样式改为连接到节点边框的直线。只有在这些修改完成之后,代码才会分别对两组数据调用 addNodes(...)addLines(...)

在真实系统中,这种形态可以表示一个焦点网络加一个需要不同布局规则的层级结构,例如一个服务集群加下游依赖、一张核心设备图加一个组件树,或者一个知识中心加一个分类分支。示例中的标签只是占位符,但这种数据传递模式可以直接映射到真实的实体 ID、标签和关系类型。

如何使用 relation-graph

index.tsx 通过 RGProvider relationGraphCore={MyGraphInstance} 包装整个示例。这是该示例的架构核心:RelationGraph 依然负责渲染画布,但实际运行的 graph core 被替换成了一个重写 doLayout() 的子类。在 MyGraph.tsx 中,RGHooks.useGraphInstance() 会获取这个实时实例,而一个只在挂载时执行的 useEffect(...) 会调用一次 graphInstance.doLayout() 来填充并排列整张图。

图配置将外层画布保持为 layoutName: 'fixed',从而把摆放控制权交给自定义实例。这组配置还设置了 170x40 的默认节点、左右方向的曲线边路由、黑色默认边颜色、沿边路径显示的边标签、10 个字符的连线标签长度上限,以及放置在右下角的横向内置工具栏。这里的 data 字段被用作传递 myLeftJsonDatamyRightJsonData 的自定义通道;它并不是内置布局的通用要求。

MyGraphInstance.doLayout() 中,代码会从 getOptions().data 读取两组数据,通过 addNodes(...)addLines(...) 注入它们,然后执行一个两阶段的摆放流程。createLayout({ layoutName: 'center' }) 会先排列左侧网络。随后,getNetworkNodesByNode(...)getNodesRectBox(...) 用于测量已摆放区域,并把测得的 maxX 用作右侧根节点的偏移量。之后再用 createLayout({ layoutName: 'tree', from: 'left', treeNodeGapH: 200, treeNodeGapV: 30 }) 排列第二个网络。最终场景会通过 moveToCenter()zoomToFit() 收尾。

这个示例使用了 hooks 和子组件,但没有使用自定义插槽或自定义事件处理器。共享的 CanvasSettingsPanel 内部通过 RGHooks.useGraphStore() 来反映当前的滚轮和拖拽模式,而 graphInstance.setOptions(...) 会在运行时更新这些模式。导入的 my-relation-graph.scss 文件只是一个没有任何声明的选择器骨架,因此可见样式主要来自图配置,以及自定义 graph instance 中对节点和连线执行的变更。

关键交互

  • 浮动辅助窗口可以通过标题栏拖动,也可以最小化或恢复。
  • 辅助窗口可以切换到一个设置覆盖层。
  • 设置覆盖层可以实时把 wheelEventActionscrollzoomnone 之间切换。
  • 同一个覆盖层也可以实时把 dragEventActionselectionmovenone 之间切换。
  • relation-graph 的内置工具栏会一直保留在画布右下角。
  • Download Image 会先为截图准备 graph DOM,导出当前视图,然后恢复图状态。

关键代码片段

下面这个片段展示了该示例如何在 provider 层替换默认 graph core:

const Demo = () => {
    return (
        <RGProvider relationGraphCore={MyGraphInstance}>
            <MyGraph />
        </RGProvider>
    );
};

下面这个片段展示了宿主图保持 fixed 模式,并通过 graphOptions.data 携带两组数据:

layout: {
    layoutName: 'fixed'
},
data: {
    myLeftJsonData,
    myRightJsonData
}

下面这个片段展示了初始化过程是一次性调用被重写的 graph-instance 布局方法:

const initializeGraph = async () => {
    // The graphInstance here is a custom instance of relationGraphCore, it overrides the doLayout method, internally reads myLeftJsonData and myRightJsonData from graphOptions.data, and renders this data
    graphInstance.doLayout();
};
useEffect(() => {
    initializeGraph();
}, []);

下面这个片段展示了自定义 graph core 如何从 options 中读取两组数据,并在布局过程中将其注入:

async doLayout(customRootNode?: string | RGNode | undefined) {
    const {myLeftJsonData, myRightJsonData} = this.getOptions().data;
    myRightJsonData.nodes.forEach(node => {
        // 为了方便关键线条端点与节点之间的间隙,让节点的视觉效果更不图虫
        node.opacity = 0.5;
        node.color = 'transparent';
    });
    this.addNodes(myRightJsonData.nodes);
    this.addLines(myRightJsonData.lines);

下面这个片段展示了针对每个子图进行的样式重设,这使左侧网络形成了不同的视觉语法:

myLeftJsonData.nodes.forEach(node => {
    // 为了方便关键线条端点与节点之间的间隙,让节点的视觉效果更不图虫
    node.opacity = 0.5;
    node.nodeShape = RGNodeShape.circle;
    node.borderWidth = 1;
    node.width = 80;
    node.height = 80;
    node.fontSize = 10;
    node.color = 'transparent';
});

下面这个片段展示了布局如何从左侧居中网络切换到右侧树形结构:

const myLeftLayout = graphInstance.createLayout({
    layoutName: 'center'
});
const leftNodes = graphInstance.getNetworkNodesByNode(leftRootNode);
myLeftLayout.placeNodes(leftNodes, leftRootNode);
const nodesRectInfo = graphInstance.getNodesRectBox(leftNodes);
rightRootNode.x = nodesRectInfo.maxX + 100; // 将右侧树的根节点放置在中心布局关系网的右侧 100位置

下面这个片段展示了共享设置面板如何在实时实例上更新图行为,而不是重新构建数据:

<SettingRow
    label="Canvas Drag Event:"
    options={[
        { label: 'Selection', value: 'selection' },
        { label: 'Move', value: 'move' },
        { label: 'none', value: 'none' },
    ]}
    value={dragMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ dragEventAction: newValue }); }}
/>

这个示例的独特之处

对比信息支持一个明确判断:这个示例的特别之处,并不在于它是唯一一个把居中网络与右侧树形结构混合在一起的示例。附近的 multiple-layout-mixing-in-single-canvasgap-of-line-and-nodecanvas-caliper 等示例,已经使用了非常接近的“中心网络 + 右侧树”组合。这里真正不同的是这种编排逻辑由谁负责。

multiple-layout-mixing-in-single-canvas 相比,它们的可见组合方式很相似,但这个示例把编排逻辑移动到了 RGProvider relationGraphCore={MyGraphInstance} 和被重写的 doLayout() 中。这个自定义 core 会从 graphOptions.data 读取 myLeftJsonDatamyRightJsonData,对它们重设样式、注入、测量左侧子图,再摆放右侧子图。当项目需要由图引擎本身而不只是 React 组件来负责加载与摆放逻辑时,这会成为一个更合适的起点。

gap-of-line-and-nodecanvas-caliper 相比,这里增加的复杂性更多是基础设施层面的,而不是展示层面的。那些示例使用相似的混合布局骨架来演示连线微调或坐标覆盖,而这个示例则用同样的骨架来演示 provider 层级的 graph-core 替换。浮动辅助窗口、图片导出以及运行时滚轮和拖拽控制都很实用,但对比信息并不支持把它们视为这个示例独有的特性。

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

这一模式非常适合那些需要让 graph instance 扮演编排器而不是被动渲染器的项目。当不同子图需要不同布局规则、当数据需要在布局阶段从自定义选项字段中注入,或当一个子图必须先被测量才能把另一个子图摆放到它旁边时,团队都可以采用同样的方法。

可扩展的场景包括把一个服务依赖集群与一个归属关系树结合起来,把一张设备拓扑与一个维护层级结合起来,或把一个焦点实体网络与一个次级分类分支结合起来。这些都是迁移目标而不是已实现的功能,但这里的核心技术是可复用的:保持宿主画布为固定布局,让自定义 graph core 吞入多组数据,并通过分阶段布局流程把它们组合起来。