JavaScript is required

自定义混合子图布局

这个示例加载一份静态 relation-graph 数据,按分组元数据拆分后,在同一固定画布上组合 5 个采用不同布局策略的子图。它非常适合作为元数据驱动混合布局编排的参考,尤其是需要按组定制样式并让一个内嵌力导区域与其他布局共存的场景。

在一个固定画布上组合五种子图布局

这个示例构建了什么

这个示例构建了一个单一的 relation-graph 场景,看起来像是由五个独立子图拼贴在同一个画布上。左、右、上、下以及右下区域分别使用不同的布局策略,但它们仍然被加载到同一个图实例中,并一起显示在同一个视口里。

用户会看到按颜色区分的节点分组、明显不同的连线样式,以及悬浮在图上方的白色辅助窗口。这个窗口可以拖拽、最小化、切换为设置面板,还可以用来下载当前图像。图本身保持只读:点击节点和连线只会记录被选中的项目。这里最值得关注的是整体编排模式,而不是某一种布局本身。该示例展示了一个固定的基础画布如何同时承载多次子布局计算,其中还包括一个会通过同步力导布局持续运动的顶部子分组。

数据如何组织

数据是在 initializeGraph() 内部创建的一个内联 RGJsonData 对象。它包含一份静态的节点和连线列表,但每个节点都带有 data.myGroupId 标记,因此运行时可以在布局前将图拆分为 leftrighttopbottombottom-right 这些分区。

在调用 setJsonData(...) 之前,只做了很轻量的预处理。局部的 createLine() 辅助函数会为每条边分配稳定的 line-{n} id,而每个节点上的分组标签则成为后续决策的共享键。同一份元数据同时驱动了 MyMixLayoutManager 中的布局分区、RGSlotOnNode 中的节点渲染,以及加载完成后的多种节点和连线样式覆盖。

在业务图谱中,这种结构可以表示具有不同拓扑规则的服务域、围绕核心系统的依赖集群、多团队的归属区域、采用不同分支约定的产品家族,或是每个区域都需要不同视觉语法的混合知识图谱。

如何使用 relation-graph

index.tsx 通过 RGProvider 包装整个示例,这样 MyGraph 和悬浮工具外壳都能访问当前激活的图上下文。主 RelationGraph 实例被配置为 layoutName: 'fixed'、圆角折线、默认矩形节点以及较细的节点边框。这个固定的外层布局很关键,因为该示例并不希望由一个全局自动布局来控制整个场景。

运行时流程是命令式的。MyGraph 通过 RGHooks.useGraphInstance() 获取当前实例,显示加载状态,清空图,使用 setJsonData(...) 加载内联 JSON,执行 applyMyLayout(),将视口移动到中心,适配缩放,最后清除加载标记。

MyMixLayoutManager 才是这个示例真正的重点。它从活动实例中读取已加载的节点,按 node.data.myGroupId 过滤,给每个区域固定一个命名根节点,然后用 createLayout(...) 创建非主布局副本。左侧分组变为以 r 为根、向右展开的树布局;右侧分组变为以 a 为根、从左到右的树布局;顶部分组变为以 t 为根的力导布局;底部分组变为以 e 为根的中心布局;右下分组变为以 br-root 为根、自上而下的树布局。

该示例在数据加载后还使用了 relation-graph 的更新 API。updateNodePosition() 用于固定每个子分组根节点的锚点;updateNode() 会为圆形分组修改形状和尺寸;updateLine() 会按区域修改走线与连接点行为;getNodeOutgoingNodes() 则用于将右侧的展开占位点移动到那些拥有子节点的节点的出边一侧。

自定义渲染通过 RGSlotOnNode 完成。这个插槽读取同样的分组元数据,并将其映射为 .c-left-node.c-right-node 之类的 CSS 类,而 SCSS 覆盖则会调整选中连线样式以及各分组特有的节点外观。悬浮的 DraggableWindow 是共享的辅助脚手架,但它仍然以有意义的方式使用了 relation-graph hooks:CanvasSettingsPanel 通过 RGHooks.useGraphStore() 读取选项状态,使用 setOptions() 更新滚轮和拖拽行为,并在导出图像时调用 prepareForImageGeneration()restoreAfterImageGeneration()

关键交互

  • 辅助窗口可以通过标题栏拖拽,因此控件可以移动,而不会改变图布局。
  • 同一个窗口可以最小化,也可以切换成设置覆盖层,而无需离开图页面。
  • 设置面板可以实时将 wheelEventActionscrollzoomnone 之间切换。
  • 设置面板可以实时将 dragEventActionselectionmovenone 之间切换。
  • Download Image 会捕获准备好的图画布,并通过共享截图工具保存。
  • 顶部分组在初始化后仍会持续运动,因为它的根节点会通过定时器重新定位,并同步回已存储的力导布局副本中。

关键代码片段

下面这段展示了数据集是在代码中内联组装的,并且在图加载之前,每条连线都会通过显式 id 做标准化处理。

let lineIndex = 0;
const createLine = (line: any): JsonLine => {
    lineIndex++;
    return { id: `line-${lineIndex}`, ...line };
};

const myJsonData: RGJsonData = {
    nodes: [

下面这段展示了同一个 myGroupId 字段如何直接写入节点数据中,并在后续被布局和渲染逻辑复用。

nodes: [
    { id: 'r', text: 'R', data: { myGroupId: 'left' } },
    { id: 'R-b', text: 'R-b', data: { myGroupId: 'left' } },
    { id: 'R-b-1', text: 'R-b-1', data: { myGroupId: 'left' } },
    // right group
    { id: 'a', text: 'a', data: { myGroupId: 'right' } },
    { id: 'c', text: 'c', data: { myGroupId: 'right' } },

下面这段展示了挂载阶段的生命周期:加载图、运行自定义布局管理器,并将最终场景适配到视口中。

graphInstance.loading('Wait...');
graphInstance.clearGraph();
await graphInstance.setJsonData(myJsonData);
await myLayout.current.applyMyLayout();

graphInstance.moveToCenter();
graphInstance.zoomToFit();
graphInstance.clearLoading();

下面这段展示了其中一个非主布局副本:先固定左侧分组的根节点,然后仅为这个子分组创建专用树布局。

const groupRootNode = this.graphInstance.getNodeById('r');
if (groupRootNode) {
    const groupRootNodeXy = {
        x: -500,
        y: 0
    };
    this.graphInstance.updateNodePosition(groupRootNode, groupRootNodeXy.x, groupRootNodeXy.y);
    const currentLayoutClone = this.graphInstance.createLayout({
        layoutName: 'tree', from: 'right'
    });
    currentLayoutClone.isMainLayouer = false;
    currentLayoutClone.layoutOptions.fixedRootNode = true;

下面这段展示了由力导驱动的顶部子分组,其中包括后续会在根节点通过定时器移动时持续同步的布局副本。

const forceLayoutOptions: RGLayoutOptions = {
    layoutName: 'force',
    force_node_repulsion: 0.2,
    force_line_elastic: 1.5,
    maxLayoutTimes: Number.MAX_SAFE_INTEGER
};

const currentLayoutClone = this.graphInstance.createLayout(forceLayoutOptions);
currentLayoutClone.isMainLayouer = false;
currentLayoutClone.requireLinks = false;
currentLayoutClone.layoutOptions.fixedRootNode = true;
currentLayoutClone.placeNodes(eGroupNodes, groupRootNode);
this.myForceLayout = currentLayoutClone;

下面这段展示了节点插槽如何利用分组元数据切换渲染出的节点类,而不是依赖单一默认节点主体。

if (node.id === 'my-root') {
    nodeClass = 'my-root-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'left') {
    nodeClass = 'c-left-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'right') {
    nodeClass = 'c-right-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'top') {
    nodeClass = 'c-top-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'bottom') {
    nodeClass = 'c-bottom-node c-valign-center';
}

下面这段展示了共享的导出流程:先准备图画布,进行截图,然后在下载完成后恢复图状态。

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

下面这段展示了 SCSS 层面对左侧和右侧两类节点家族的差异化处理。

.c-left-node {
    background-color: #df7f03;
    width: 100px;
    height: 40px;
    color: white;
}

.c-right-node {
    width: 150px;
    height: 40px;
    background-color: #f0f0f0;
}

这个示例的独特之处

对比记录清楚地给出了一个稳妥的区分点:这不只是一个恰好让两种内置布局共享同一画布的演示。它最强的差异点,是在一个 fixed 图实例之上实现了五个区域的组合式布局方案。该示例按 data.myGroupId 对一次加载的数据集进行分区,为每个区域固定一个根节点,然后在同一场景中组合树布局、力导布局和中心布局副本。

multiple-layout-mixing-in-single-canvas 相比,这个示例更强调编排规模与协同方式。它使用五个由元数据驱动的分组,而不是两个分别排列的子网络,并且将布局选择与更广泛的加载后样式规则绑定在一起。与 mix-layout-8 相比,它的范围更窄,也更适合作为参考来阅读,因为它是一个预设好的浏览场景,而不是一个支持运行时图编辑的混合布局工作区。

对比数据还支持另一个克制的判断:动画化的力导分段也是这个示例不常见的原因之一。顶部簇会保存自己的力导布局副本,通过定时器移动根节点,并将这段运动同步回子布局中,而其他区域仍保持确定性。这使得该示例成为一种很强的起点,适用于图中的某一部分需要持续运动,而整体又要维持固定混合布局构图的场景。

共享的悬浮辅助窗口本身并不是最具辨识度的部分,因为对比说明指出,这个工具外壳在其他地方也被复用了。这里真正突出的,是它把这个覆盖层与固定基础画布、五个分组子布局、按组定制的节点与连接线规则,以及嵌入在其他稳定区域之间、通过定时器同步的力导孤岛结合在了一起。

这种模式还适用于哪里

这种模式非常适合那些需要在同一张图中同时使用多种布局语法,而不是依赖单一全图算法的系统。例如,具有不同集群类型的服务版图、需要用不同形态表达入口区、内部拓扑和外部依赖的安全图谱,或者在一个画布中混合流程中心、分支树和监控岛的制造视图。

当同一个元数据键需要同时控制位置和外观时,这种方式也很有用。团队可以把同样的方法复用于产品组合地图、混合式组织结构、产品架构总览,或是知识图谱中那些每个类别都需要各自连线走向、节点轮廓和根节点摆放方式,但又必须共存于同一个 relation-graph 实例中的场景。