JavaScript is required

中心布局与树布局组合

这个示例构建了一个只读的 `relation-graph` 查看器,在同一张画布中放置两个彼此断开的子网络。其中一个子图被绘制为以中心为焦点的聚簇,另一个则被绘制为从左到右展开的树,并放置在该聚簇的右侧。

在一个固定画布上组合 centertree 布局

这个示例构建了什么

这个示例构建了一个只读的 relation-graph 查看器,在同一张画布中放置两个彼此断开的子网络。其中一个子图被绘制为以中心为焦点的聚簇,另一个则被绘制为从左到右展开的树,并放置在该聚簇的右侧。

用户会在同一个视口中看到两个明显不同的视觉区域:左侧是带有直线连线的半透明圆形节点,右侧是带有黑色曲线连线的较宽矩形节点。他们可以平移和缩放图谱,使用右下角内置工具栏,拖动或最小化浮动辅助窗口,在其设置面板中切换画布交互模式,并将当前视图导出为图片。这个示例的核心价值在于布局编排:一个固定宿主画布、两次独立的布局执行,以及一次基于边界范围的交接。

数据如何组织

数据直接在 MyGraph.tsx 中声明为两个内联的 RGJsonData 对象:treeJsonData 的根节点为 acenterJsonData 的根节点为 2。该示例不会对一个合并后的结构调用 setJsonData(...)。相反,它通过分别调用 addNodes(...)addLines(...),将每个组件插入到同一个图实例中。

两个数据集都会在布局前进行预处理。树形节点会设置 opacity = 0.5color = 'transparent',以便更容易观察连线几何形态。中心聚簇节点则会进行更明显的样式重设,变成 80x80 的圆形并使用更小的文字,而中心聚簇的连线会切换为附着在边框上的直线连接器。正是这种预处理,让这两个子网络读起来像两个不同的布局区域,而不是一个偶然断开的图。

在真实应用中,这种结构可以表示一个中心设备集群加上一棵依赖树、一个核心服务地图加上下游归属分支,或者一个焦点实体加上一个次级层级结构。示例中的标签只是占位符,但这种数据模型可以直接迁移到业务 ID、名称和关系标签上。

relation-graph 的使用方式

index.tsx 使用 RGProvider 包裹该示例,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 获取实时图实例。图谱以 layoutName: 'fixed' 启动,这是这里最关键的架构选择:宿主画布保持固定,这样示例就可以让每个连通组件分别执行自己的内置布局,而不是把整个场景交给一个全局布局来处理。

图谱选项还定义了共享的查看器行为:默认节点大小为 170x40,默认采用从左到右的曲线连线路由,路径上的线标签默认启用且限制为 10 个字符,连线颜色为黑色,并在右下角放置内置工具栏。这个示例中没有自定义节点、连线或画布插槽。视觉区分来自于插入前对节点和连线属性的修改,而不是来自自定义渲染模板。

在挂载时,initializeGraph() 会先插入树形组件,再重设样式并插入中心组件,然后调用 doLayoutAll()。这个布局过程会先为中心聚簇创建一个 center 布局器,使用 getNetworkNodesByNode(...) 扩展出整簇节点,对其进行放置,再通过 getNodesRectBox(...) 测量它的边界,并将树根节点移动到 maxX + 100。只有在这一步交接完成后,它才会为第二个组件创建一个起点在左侧的 tree 布局器。组合后的整体场景最后再通过 moveToCenter()zoomToFit() 完成收尾。

浮动的 DraggableWindow 是一个在多个 demo 中复用的本地辅助组件,但在这里它仍然很重要,因为它以一种很实用的方式暴露了图实例 API。CanvasSettingsPanel 通过 RGHooks.useGraphStore() 读取当前选项状态,在运行时通过 setOptions(...) 切换 wheelEventActiondragEventAction,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 封装截图导出。样式部分保持得很克制:my-relation-graph.scss 基本上让大多数选择器保持空白,只有一个已选中连线标签颜色的覆盖,因此这个示例的大部分外观都由图谱选项和按子图进行的属性修改来驱动。

关键交互

  • 图谱的行为是查看器而不是编辑器:用户查看的是一个已经准备好的混合布局场景,而不是创建节点或重新连接节点。
  • 浮动辅助窗口可以拖动、最小化,并通过齿轮按钮切换到设置覆盖层。
  • 设置覆盖层可以实时把 wheelEventActionscrollzoomnone 之间切换。
  • 同一个面板也可以实时把 dragEventActionselectionmovenone 之间切换。
  • relation-graph 的内置工具栏始终可在画布右下角使用。
  • Download Image 会先为捕获准备图谱 DOM,导出当前视图,然后再恢复图谱状态。

关键代码片段

这个片段说明宿主图谱被有意保持在固定模式下,并提供了共享默认值,之后两个子图会有选择地覆盖这些默认值:

const graphOptions: RGOptions = {
    defaultNodeWidth: 170,
    defaultNodeHeight: 40,
    toolBarDirection: 'h',
    toolBarPositionH: 'right',
    toolBarPositionV: 'bottom',
    defaultLineShape: RGLineShape.StandardCurve,
    defaultJunctionPoint: RGJunctionPoint.lr,
    defaultLineTextOnPath: true,
    lineTextMaxLength: 10,
    defaultLineColor: '#000000',
    layout: { layoutName: 'fixed' }
};

这个片段说明两个数据集会在插入前先重设样式,这样每个子图就拥有不同的视觉语法:

treeJsonData.nodes.forEach(node => {
    node.opacity = 0.5;
    node.color = 'transparent';
});

centerJsonData.nodes.forEach(node => {
    node.opacity = 0.5;
    node.nodeShape = RGNodeShape.circle;
    node.width = 80;
    node.height = 80;
    node.fontSize = 10;
    node.color = 'transparent';
});

这个片段说明了边级别的属性修改,它让中心聚簇使用附着在边框上的直线连线,而不是默认的曲线路由:

centerJsonData.lines.forEach(line => {
    line.lineShape = RGLineShape.StandardStraight;
    line.fromJunctionPoint = RGJunctionPoint.border;
    line.toJunctionPoint = RGJunctionPoint.border;
});
graphInstance.addNodes(centerJsonData.nodes);
graphInstance.addLines(centerJsonData.lines);
doLayoutAll();

这个片段展示了核心放置流程:先对第一个组件做居中布局,测量它的范围,偏移第二个组件的根节点,再执行树形布局:

const myCenterLayout = graphInstance.createLayout({
    layoutName: 'center'
});
const centerNodes = graphInstance.getNetworkNodesByNode(centerRootNode);
myCenterLayout.placeNodes(centerNodes, centerRootNode);
const nodesRectInfo = graphInstance.getNodesRectBox(centerNodes);
treeRootNode.x = nodesRectInfo.maxX + 100;

const myTreeLayout = graphInstance.createLayout({
    layoutName: 'tree',
    from: 'left',
    treeNodeGapH: 200,
    treeNodeGapV: 30
});

这个片段说明浮动工具面板更新的是实时图谱选项,而不是重新构建整个场景:

<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 }); }}
/>

这个片段说明了共享的截图流程:先准备 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
});
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

对比数据支持一个很明确的结论:这是一个紧凑的参考示例,用于在同一个固定图实例中组合两个内置布局,而不引入自定义布局管理器。它加载两个断开的数据集,执行一次 center 布局和一次从左到右的 tree 布局,并通过一次基于测量边界的交接把这两次布局串起来。因此,当目标只是实现一个简单的两阶段组合时,它比范围更大的混合布局示例更适合作为起点。

gap-of-line-and-node 相比,这里并没有把同样的 center-plus-tree 场景用作运行时连线调节面板。与 canvas-caliper 相比,这个场景没有加入标尺和视口覆盖层,因此更容易直接研究它的放置流程。与 mix-layoutorganizational-chart 相比,这个示例更小,也更适合复制使用,因为它没有引入基于元数据的分组管理器、自定义节点插槽,或在展开与折叠之后的重新布局逻辑。

另一个有用的区别在于,它用极少的机制实现了很强的视觉对比。中心聚簇变成了圆形、半透明、使用直线连线,而树形部分则保持较宽的矩形节点、黑色曲线连接器,以及被截断的路径标签。最终得到的是一个混合布局场景,在不需要自定义渲染层的前提下仍然保持良好的可读性。

这种模式还适用于哪里

当一个图需要在同一张画布上同时容纳两种结构语法,但又不需要完整的混合布局框架时,这种模式非常合适。常见的迁移场景包括设备拓扑加维护树、中心服务集群加下游依赖、知识枢纽加分类分支,或者工作流入口节点加审批层级。

当团队需要把一个子图的布局一次性交接给另一个子图时,这种方式也很有用。相同的方法还可以扩展到仪表板场景中:先放置一个摘要聚簇,再在其旁边附加一个或多个方向性树形结构,同时仍然保持整体图实例简单且只读。