自定义力学布局同心圆轨道
这个示例挂载自定义 `RGLayouts.ForceLayout` 子类,在力学规则驱动关系节点运动的同时,将其约束在可配置的同心圆轨道上。悬浮控制面板可实时调整斥力、连线弹性、环半径、画布行为和图片导出。
带同心圆约束的自定义力布局
这个示例构建了什么
这个示例构建了一个全屏关系图:自定义力布局会让节点持续运动,但节点只能沿可配置的同心圆环移动。画布中央展示一个头像根节点、两个固定在第一圈上的分支根节点、带明确标签的关系连线,以及与求解器轨道规则相匹配的分层圆形背景。
用户可以在图运行时重新调整节点斥力和连线弹性,拖动滑条修改圆环直径,使用内置工具栏控制实时布局,还可以打开浮动设置面板来调整画布交互并导出图片。这个示例的重点不在于带有亲属意味的样例标签,而在于它为“如何在有序的径向构图中保持力导运动可读”提供了一个具体参考。
数据是如何组织的
图数据以内联的一个 RGJsonData 对象声明,其中包含 rootId、nodes 和 lines。每个节点都会在 data 中携带展示和布局元数据,尤其是 myColor、myLevel 和 limitCircular;部分节点还会使用 force_weight、fixed、disableDrag 和 disablePointEvent 来影响求解器行为与交互。
在任何数据插入 relation-graph 之前,代码会先对这份数据集做预处理。它会把根节点放到图坐标原点,通过 getOvalPoint(...) 放置两个第一圈锚点节点,将它们标记为固定且不可拖拽,给它们的展开控件着色,并把每条连线都改写成虚线且不显示箭头的样式。circularSet 状态以像素存储圆环直径,然后 updateLayoutCircleSet() 会把这些值转换为半径,并加入一个偏移量,这样自定义布局约束的是节点中心点,而不是节点外边缘。
在真实应用中,同样的数据结构可以用来描述分层的利益相关者图、分层依赖图、围绕某个中心实体的影响力圆环,或者任何“每个节点都属于某个径向带,同时仍需要在该带内进行力导排布”的数据集。
relation-graph 是如何使用的
index.tsx 使用 RGProvider 包裹页面,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 作为控制入口。基础图配置让宿主画布保持 layoutName: 'fixed',把默认节点形状设为圆形,启用沿路径显示的连线文字,并把工具栏水平放在右下角。这个示例没有调用 setJsonData(),而是先预处理内联数据集,再通过 addNodes() 和 addLines() 加载。
relation-graph 的核心定制点是 MyForceLayout extends RGLayouts.ForceLayout。在 resetCalcNodes() 中,它会把每个可见节点复制到计算记录里,并把 data.limitCircular 带入求解器状态。在 calcNodesPosition() 中,它保留常规的节点斥力和父子连线弹性,然后在迭代进入较后阶段后,把每个非根、非固定节点重新投影回其所属圆环所配置的半径上。页面通过 setLayoutor(myLayout, true, true) 挂载这个布局器,先执行一次 placeNodes(...),之后再用 stopAutoLayout() 和 startAutoLayout() 应用实时的力参数调整。
该示例还使用 RGSlotOnNode 替换默认节点主体,提供两种自定义渲染:根节点显示为圆形头像,其他节点显示为圆形文字徽章。同心圆背景作为绝对定位的分层 DOM 渲染在 RelationGraph 内部,因此这些可视化引导区域会始终与图坐标原点对齐。my-relation-graph.scss 中的样式覆盖了画布背景、选中连线高亮、节点颜色变体以及圆形节点皮肤。共享辅助组件则补充了可拖动的浮动面板、基于拖拽的圆环尺寸编辑器、通过 graphInstance.setOptions(...) 进行的运行时画布模式切换,以及通过 prepareForImageGeneration() 和 restoreAfterImageGeneration() 实现的图片导出功能。
关键交互
- 拖动
Node Repulsion Coefficient滑块会更新已挂载的自定义布局器,并重新启动自动布局,使间距斥力立即生效。 - 拖动
Line Elastic Coefficient滑块会改变父子关系连线在求解器运行时对相关节点的拉拽强度。 - 拖动
SimpleUINumberArray条形控件会修改circularSet,从而更新MyForceLayout使用的圆环半径,而无需重建图数据。 - 浮动辅助窗口可以被拖动、最小化,并切换为设置覆盖层。
- 设置覆盖层可以调整滚轮行为、修改画布拖拽行为,并把当前场景导出为图片。
- 根节点和两个第一圈锚点节点始终保持固定,因此交互模型的重点是重新调优布局,而不是编辑图结构。
关键代码片段
这段代码表明:宿主图保持在 fixed 模式下,使用圆形节点,并在自定义布局器运行时继续保留工具栏。
const graphOptions: RGOptions = {
debug: true,
defaultLineColor: '#aaaaaa',
defaultNodeColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 0,
defaultNodeHeight: 0,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
layout: {
layoutName: 'fixed'
}
};
这段片段证明:圆环层级归属是节点数据模型的一部分,而图会把样式元数据与可见文本分开存放。
{ id: 'a', text: '', color: '#cccccc', width: 80, height: 80, force_weight: 10000, disablePointEvent: true, disableDrag: true, data: { myColor: 'root-color', myLevel: 'my-root', limitCircular: 0, img: 'https://img1.baidu.com/it/u=1728803666,3540199393&fm=253&fmt=auto&app=138&f=JPEG?w=200&h=200' } },
{ id: 'l1-1', text: '同一单位', force_weight: 100, data: { myColor: 'my-node-yellow', myLevel: 'my-level1', limitCircular: 1 } },
{ id: 'l2-1', text: '刘宁', data: { myColor: 'my-node-yellow', myLevel: 'my-level2', limitCircular: 2 } },
{ id: 'l3-1', text: '刘六六', data: { myColor: 'my-node-yellow', myLevel: 'my-level3', limitCircular: 3 } },
{ id: 'l1-2', text: '同一系统', force_weight: 100, data: { myColor: 'my-node-green', myLevel: 'my-level1', limitCircular: 1 } },
{ id: 'l4-1', text: '刘明明', data: { myColor: 'my-node-green', myLevel: 'my-level4', limitCircular: 4 } }
这段代码展示了预处理步骤:在加载图之前,它会固定第一圈锚点,并把每条关系线都改写为无箭头的虚线样式。
const leve1NodeForSystem = graphJsonData.nodes.find(nodeJson => nodeJson.id === 'l1-2');
leve1NodeForSystem.color = '#40c989';
leve1NodeForSystem.fixed = true;
let nodePoint = getOvalPoint(rootNodeJson.x, rootNodeJson.y, circularSet[0] / 2, 90);
leve1NodeForSystem.x = nodePoint.x;
leve1NodeForSystem.y = nodePoint.y;
leve1NodeForSystem.disableDrag = true;
graphJsonData.lines.forEach(line => {
line.dashType = 2;
line.showEndArrow = false;
});
这是最关键的自定义布局规则:在力导步骤执行后,每个节点都会被重新投影回其所属圆环对应的半径上。
if (__node1.limitCircular > 0) {
const rgNode = __node1.rgNode;
const nodeCenterX = __node1.x + rgNode.el_W / 2;
const nodeCenterY = __node1.y + rgNode.el_H / 2;
const nodeNewX = nodeCenterX + __node1.Fx;
const nodeNewY = nodeCenterY + __node1.Fy;
const distanceToCenter = isPointInCircleAndIntersection(
0, 0, this.levelCircleSet[__node1.limitCircular], nodeNewX, nodeNewY
);
__node1.Fx = distanceToCenter.intersection.x - nodeCenterX;
__node1.Fy = distanceToCenter.intersection.y - nodeCenterY;
}
这个处理函数展示了如何把实时物理参数调优应用到已挂载的布局器,而不是通过重建整张图来实现。
const updateLayoutOptions = async () => {
graphInstance.stopAutoLayout();
const forceLayout = graphInstance.layoutor as MyForceLayout;
if (forceLayout) {
forceLayout.updateOptions({
force_node_repulsion,
force_line_elastic
});
forceLayout.start();
}
graphInstance.startAutoLayout();
};
这段片段表明:圆环几何参数同样可以在运行时通过 React state 和专用 UI 控件进行编辑。
const updateLayoutCircleSet = async (myLayout: MyForceLayout) => {
myLayout.setLevelCircleSet(circularSet.map(v => v / 2 - 60));
};
useEffect(() => {
const myLayout = graphInstance.layoutor as MyForceLayout;
if (myLayout) {
updateLayoutCircleSet(myLayout);
}
}, [circularSet]);
这个示例的独特之处
准备好的对比数据已经清楚说明了这个示例的主要区别:它不只是另一个自定义力布局示例。相较于 customer-layout-force,它保留了明确的关系连线,为每个节点分配了 limitCircular 层级,固定了两个第一圈分支根节点,并持续把运动中的节点重新投影回轨道半径上。这里强调的是有序的径向运动,而不是在一个无边场景中的自由聚类。
这些对比数据也将它与 multiple-layout-mixing-in-single-canvas、canvas-caliper 这类偏工作区风格的邻近示例区分开来。那些示例是在场景外围增加结构,而这里的圆形本身就是求解规则的一部分,因为 circularSet 会传入 MyForceLayout.setLevelCircleSet(...)。背景层、锚点布局和约束逻辑共同强化了同一种分层阅读方式。
它那组少见的功能组合也非常聚焦:自定义的 RGLayouts.ForceLayout 子类、实时力参数调优、基于拖拽的圆环直径编辑器、固定锚点节点、深色画布、无箭头虚线关系线,以及圆形头像与徽章式节点插槽。像浮动设置面板和图片导出这样的共享辅助功能本身并不独特,但在这个示例里,它们服务的是一个“受约束的布局工程工作流”,而不是通用查看器。
这种模式还适用于哪里
这种模式可以直接迁移到那些既需要层级带状分布、又需要局部力导间距的图中。例如按影响距离组织的利益相关者图、分层风险网络、按与核心公司距离分组的供应商生态图,或者拆分为核心圈、相邻圈和外围圈的依赖关系图。
当团队希望让终端用户在不编辑图结构的前提下调节布局几何时,这种模式也很有用。可拖拽的圆环尺寸编辑器可以变成策略区域、所有权圆、影响带或服务层级的控制器,而同样的自定义布局器模式则能让节点在应用所需的任何径向模型中保持清晰可读。