JavaScript is required

力导向布局运行时参数控制

这个示例提供全屏 relation-graph 力导布局调参场,包含两组预设数据、6 个运行时力参数滑块,以及连续计算与固定迭代布局切换。它通过实时图实例重载数据、修改当前 ForceLayout,并在悬浮工具窗中暴露共享画布设置与图片导出。

使用数据集预设进行运行时 Force 布局调优

这个示例构建了什么

这个示例围绕 relation-graph 内置的 force 布局构建了一个全屏的力导向布局调试场。页面会在主画布上显示一个蓝色的圆形节点网络,并在其上方悬浮一个控制窗口。用户可以在一个紧凑的加权样例和一个更大的分支样例之间切换,调整六个全局力系数,选择让布局持续运行还是在固定迭代次数后停止,并打开共享的画布工具,例如拖拽模式设置、滚轮模式设置和图片导出。

这个示例的重点不是自定义渲染,而是内置布局器的一种运行时控制模式:保持图谱已挂载,直接修改当前启用的 force 求解器,并且只在迭代策略变化时重新执行布局。

数据是如何组织的

这个示例在 example-data.ts 中保留了两个本地 RGJsonData 预设。exampleDataSmall 是一个紧凑的类树形图,包含一个更大的根节点,并设置了 force_weight: 10000exampleDataBig 则是一个更稠密的分支图,包含更多后代节点。在加载任一预设之前,initializeGraph() 会遍历每一条连线并补充生成的 id,例如 l1l2l3,然后用 rootId: 'a' 重建整份载荷。

在生产环境图谱中,相同的结构可以表示同一领域数据的两个已保存场景:聚焦的子图与更广泛的网络、低密度客户关系视图与完整账户邻域,或者用于调试布局参数的小型测试用例与更接近真实情况的数据集。

relation-graph 是如何使用的

入口组件先用 RGProvider 包裹页面,再在 MyGraph 中渲染 RelationGraph,这样所有运行时图谱 API 都可以通过 RGHooks.useGraphInstance() 获取,而不需要依赖逐层透传 props。图谱始终使用内置的 force 布局,节点为圆形、连线为直线、连接点位于边框上,并且节点和连线都使用一致的蓝色默认样式。my-relation-graph.scss 中的样式表只覆盖了选中状态下的文本和线条颜色,并没有替换节点或连线模板。

运行时控制模式被拆成两层。第一层是直接修改当前求解器:读取 graphInstance.layoutor,将其转换为 RGLayouts.ForceLayout,然后调用 updateOptions(myForceLayoutOptions)。这让六个滑块成为实时调参控件,而不是走完整重载流程。第二层是在用户切换持续布局模式和固定迭代布局模式时,调用 graphInstance.updateOptions({ layout }),随后执行 doLayout()。这种拆分让系数调节保持轻量,同时页面仍能演示受限迭代次数下的重新布局策略。

数据集切换使用更显式的重载流程。示例会选择 smallbig,为连线数组重新生成 id,然后调用 stopAutoLayout()clearGraph()sleep(500)setJsonData(...)。加载完成后,再通过 moveToCenter() 将图谱移回中心,并把缩放重置为 30。这个示例没有定义自定义节点、连线、画布或视口插槽,而是依赖内置渲染和共享辅助组件。

悬浮工具窗口来自共享组件 DraggableWindow.tsx。这个包装组件增加了拖动、最小化/恢复、画布设置面板和截图导出功能。分段选择器来自 SimpleUISelect.tsx,而图片导出最终使用 domToImageByModernScreenshot.ts 将准备好的图谱 DOM 转换为可下载的 blob。

关键交互

  • 切换数据集预设时,图谱会从 exampleDataSmallexampleDataBig 重新加载,然后再次将视口居中并调整缩放。
  • 移动六个范围输入中的任意一个,都会原地更新当前的 ForceLayout 实例,因此用户可以在不重建组件的情况下观察当前图谱的响应。
  • 在 Layout Forever 和 Layout Fixed Times(…) 之间切换,会改变 force 求解器的运行时长。选择固定模式时,还会出现一个额外的范围输入,用于调整迭代上限。
  • 悬浮控制面板本身也可交互:可以拖动、最小化、展开,并切换到共享设置面板。
  • 共享设置面板可以改变滚轮行为、改变画布拖拽行为,并把当前图谱导出为图片。这些工具之所以可用,是因为使用了共享辅助窗口,而不是因为这个示例自己实现了专用导出流程。

关键代码片段

这段代码说明,这个示例是围绕 relation-graph 内置的 force 布局和内置默认渲染配置的,而不是依赖自定义插槽。

const graphOptions: RGOptions = {
    debug: true,
    defaultNodeBorderWidth: 0,
    defaultNodeShape: RGNodeShape.circle,
    defaultNodeWidth: 80,
    defaultNodeHeight: 80,
    defaultLineColor: 'rgb(0,139,189)',
    defaultNodeColor: 'rgb(0,139,189)',
    defaultLineShape: RGLineShape.StandardStraight,
    layout: {
        layoutName: 'force',
        maxLayoutTimes: Number.MAX_SAFE_INTEGER
    }
};

这段代码展示了显式的数据集重载路径,包括生成的连线 id,以及用于清空、重载、居中和重置缩放的 graph-instance API。

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

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: exampleData.nodes,
    lines: linesWithIds
};
graphInstance.stopAutoLayout();
graphInstance.clearGraph();
await graphInstance.sleep(500);
await graphInstance.setJsonData(myJsonData);

这段代码是运行时调参的核心技巧:页面读取当前布局器,并把新的全局力系数推送到实时的 ForceLayout 中。

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

这段代码说明,持续布局模式与固定次数布局模式的处理逻辑,与实时系数更新是分开的。

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

这段代码证明,force 控制面板暴露了六个彼此独立的全局系数,而不只是一个或两个简单滑块。

<div className="py-2">Node Repulsion: {myOptions.force_node_repulsion}</div>
<input type="range" min="0.2" max="3" step="0.1" value={myOptions.force_node_repulsion} />
<div className="py-2">Line Elastic: {myOptions.force_line_elastic}</div>
<input type="range" min="0.2" max="3" step="0.1" value={myOptions.force_line_elastic} />
<div className="py-2">maxTractionLength: {myOptions.maxTractionLength}</div>
<input type="range" min="100" max="1000" step="50" value={myOptions.maxTractionLength} />

这段代码说明,继承而来的悬浮窗口也通过 graph-instance API 暴露了画布设置和导出功能。

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

这个示例的独特之处

这个示例之所以独特,是因为它把三个在其他演示里经常分开的模式组合到了一起。第一,它通过专用面板暴露了六个全局力系数,并使用 RGLayouts.ForceLayout.updateOptions(...) 将它们应用到实时布局器上。第二,它不是把用户限制在一张静态图上,而是让这些控制项在两个预设数据集之间做对比。第三,它把实时系数修改和迭代策略触发的重新布局分离开来,因此比起更简单的 force 示例,这个示例更适合做运行时调优。

layout-force 相比,这个页面不再只是一个最小化的内置 force 基线示例,而更像一个用于比较不同预设图谱上全局 force 设置的实验室。与 performance-test-force-layout 相比,它仍然使用常规内置渲染和精心准备的本地数据集,而没有进入 performanceMode、超大规模合成图、缩略图或自定义节点插槽压测的场景。与 io-tree-layoutlayout-tree 这类布局切换示例相比,这里的核心经验是运行中 force 实例上的求解器行为,而不是朝向变化或层级专用的连线路由规则。

较小的预设还包含一个尺寸更大且带权重的根节点,这会让 force 求解器的效果在视觉上更容易观察。这个细节属于样例数据本身,而不是单独的 UI 控件。

这个模式还适用于哪里

当产品需要为内置 force 布局提供调参界面,但又不想从零开始构建自定义布局器时,这个模式就很适用。典型场景包括:内部知识图谱工具,分析人员需要稳定节点间距;调查视图,需要将稠密邻域与较小的聚焦子图做对比;以及售前演示,希望展示同一个图引擎在不同密度和不同求解时长下的表现。

它同样适用于管理或诊断类工具。支持控制台可以暴露类似的悬浮面板,用于控制拖拽行为、滚轮行为、截图导出和有限的布局参数,同时保持图谱自身为只读。这里最重要的思想,是把数据重载、实时求解器修改和重新运行策略彼此分离,使每一种交互都保持可预测。