JavaScript is required

力学布局实时参数调节

这个示例基于单一静态分支数据构建力导网络,并提供节点斥力、连线弹性和重布局时长的实时控制。它是更新当前 ForceLayout 实例并重新运行内置求解器(无需重建图数据)的紧凑参考。

Force 布局实时参数调优

这个示例构建了什么

这个示例围绕一个静态分支数据集构建了一个全屏力导向图试验场。画布显示统一的圆形节点和笔直的青绿色连线,同时一个悬浮的白色控制窗口允许用户调节节点斥力、连线弹性以及布局迭代次数。

重点并不在于自定义渲染或图编辑。真正有用的部分在于图只加载一次,然后直接原地调整正在运行的力求解器。同一个悬浮窗口还暴露了共享工作区工具,例如滚轮模式切换、拖动画布模式切换以及图片导出。

数据如何组织

图数据直接在 MyGraph.tsx 中内联组装。它使用一个 RGJsonData 对象,其中包含 rootId: 'a'、一个 rawNodes 数组和一个 rawLines 数组。在调用 setJsonData 之前,示例会把每条原始连线映射为一个带有生成 id 的新对象,因此最终传入 relation-graph 的数据集具备显式的连线标识符。

示例数据构成了一个有根的分支结构:一个根节点 a、四个顶层分支(bcde),以及每个分支下更深层的子节点组。总计来看,这个文件定义了 103 个节点和 102 条连线。在生产应用中,同样的结构可以表示组织树、依赖展开、分类体系、事故传播树,或任何仍然更适合使用力导向间距而不是严格树布局的类层级网络。

relation-graph 的使用方式

页面包裹在 RGProvider 中,而 MyGraph 通过 RGHooks.useGraphInstance() 获取作用域属于该 provider 的图实例。RelationGraph 组件接收了一组聚焦的图配置:圆形节点、60x60 的默认节点尺寸、连接到边框的直线、青色与青绿色默认配色,以及内置的 force 布局。

图实例 API 驱动了运行时行为。initializeGraph() 负责加载准备好的 RGJsonData、将图居中,并设置一个固定缩放级别。updateMyOptions() 会把 graphInstance.layoutor 视为 RGLayouts.ForceLayout,并在不重建数据集的情况下推入新的物理参数值。另一条单独的 restartForceLayout() 路径则会重写 layout.maxLayoutTimes、调用 doLayout(),然后重新居中并重新设置画布缩放,以便用户对比连续布局与固定迭代预算两种效果。

这个示例中没有节点插槽、连线插槽,也没有编辑 API。样式通过图配置以及一个本地 SCSS 文件完成,该文件会强制节点文字为白色,并定义选中连线的覆盖样式。可拖拽的辅助窗口及其设置面板来自共享的本地组件,并再次使用 relation-graph hooks 来操作画布选项和图片生成。

关键交互

  • Node Repulsion 滑块会修改当前 force 布局器上的 force_node_repulsion
  • Line Elastic 滑块会修改当前 force 布局器上的 force_line_elastic
  • 分段选择器用于在 Layout ForeverLayout Fixed Times(...) 之间切换。
  • 选择固定模式后,一个范围输入会控制 maxLayoutTimes,并触发一次新的 doLayout() 运行。
  • 悬浮辅助窗口可以拖动和最小化,因此用户可以在保持控件近旁可用的同时查看图。
  • 辅助窗口的设置视图可以修改滚轮行为、修改画布拖动行为,并下载图像快照。

节点和连线点击处理器都已存在,但在审阅过的源码中,它们只会把被点击的对象输出到控制台。在这里,它们并不驱动布局、选择或编辑行为。

关键代码片段

下面这段片段展示了该示例使用 relation-graph 内置的 force 布局,并配合最小化的视觉样式,而不是自定义节点渲染。

const graphOptions: RGOptions = {
  debug: true,
  defaultNodeBorderWidth: 0,
  defaultNodeShape: RGNodeShape.circle,
  defaultNodeWidth: 60,
  defaultNodeHeight: 60,
  defaultLineColor: 'rgba(0, 186, 189, 1)',
  defaultNodeColor: 'rgba(0, 206, 209, 1)',
  defaultLineShape: RGLineShape.StandardStraight,
  layout: { layoutName: 'force', maxLayoutTimes: Number.MAX_SAFE_INTEGER },
  defaultJunctionPoint: RGJunctionPoint.border
};

下面这段片段展示了图数据只准备一次:在把它加载到图实例之前,先为每条连线补上显式的 ID。

const linesWithIds = rawLines.map((line, index) => ({
  ...line,
  id: `l${index + 1}`
}));

const myJsonData: RGJsonData = {
  rootId: 'a',
  nodes: rawNodes,
  lines: linesWithIds
};

下面这段片段展示了一次性数据加载,以及随后显式执行的视口重置。

await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.setZoom(30);

下面这段片段展示了第一条运行时控制路径:当滑块变化时,直接更新当前激活的 ForceLayout 实例。

const forceLayout = graphInstance.layoutor as InstanceType<typeof RGLayouts.ForceLayout>;
if (forceLayout) {
  forceLayout.updateOptions(myForceLayoutOptions);
}

下面这段片段展示了第二条运行时控制路径:通过重写布局选项并重新运行求解器来切换重新布局模式。

graphInstance.updateOptions({
  layout: {
    layoutName: 'force',
    maxLayoutTimes: layoutForever ? Number.MAX_SAFE_INTEGER : maxLayoutTimes
  }
});
await graphInstance.doLayout();
graphInstance.moveToCenter();
graphInstance.setZoom(30);

下面这段片段展示了悬浮 UI 如何把连续布局与带有滑块上限的布局运行区分开来。

<SimpleUISelect currentValue={layoutForever} data={[
  { value: true, text: 'Layout Forever' },
  { value: false, text: `Layout Fixed Times(${maxLayoutTimes})` }
]} onChange={(newValue: boolean) => { setLayoutForever(newValue); }} />
{!layoutForever && <input
  type="range"
  min="10"
  max="1000"
  step="20"
  value={maxLayoutTimes}

这个示例的独特之处

这个示例的独特之处在于,它把内置的 force 布局当作一个最小化的实时基线,而不是一个庞大的控制面板。图始终停留在同一份内联数据集上,可见的力导向控制项只保留了斥力和连线弹性,而迭代控制则被放进一个独立的“连续布局 / 固定次数重新布局”切换中。

layout-force-options 相比,这个版本更适合作为起点来阅读,因为它没有加入数据集预设,也没有扩展出更宽泛的全局力参数面板。与 performance-test-force-layout 相比,它避开了性能测试脚手架,例如合成规模预设、缩略图导航以及自定义节点渲染。与 layout-force-options-pro 相比,它停留在全局求解器调优层面,而不是深入到按节点或按连线的力参数覆盖,以及运行时图变更。

另一个独特之处是它在视觉上的克制。页面保持了无边框的 60px 圆形节点、笔直的青绿色连线,以及一个可拖拽的白色辅助窗口,因此真正被突出展示的是力导向行为本身。

这种模式还适用于哪里

这种模式很适合迁移到内部工具中,在团队确定最终图设计之前先调试间距行为。例如,同样的方法可以用于依赖图、服务拓扑图、组织结构、分类浏览器等场景,在这些场景中数据保持稳定,但目标中的力导向行为仍需要持续校准。

它同样是面向操作人员工作台的实用模式。产品可以只加载一次图,让用户调整求解器强度和重新运行时长,然后在保持同一数据集不变的前提下,对比不同布局参数下的可读性、重叠减少效果以及导出质量。