首页
/ D3 forceManyBody 多体力详解:strength、theta 与 Barnes–Hut 近似的实现原理

D3 forceManyBody 多体力详解:strength、theta 与 Barnes–Hut 近似的实现原理

2026-09-04 21:07:52作者:房伟宁

多体力(many-body force)是 D3 力导向布局中最具"全局性"的一种力:它作用于所有节点之间,通过正负 strength 分别模拟引力与静电斥力,并借助四叉树与 Barnes–Hut 近似将计算复杂度从 O(n²) 降到 O(n log n)。本文基于 D3 v7(当前仓库版本 7.9.0,依赖 d3-force ^3.0.0)的官方文档与配套示例,系统讲解 forceManyBody() 的构造、四个核心参数(strength / theta / distanceMin / distanceMax)的语义与默认值,以及该力在力模拟 tick 循环中的实际工作方式。

多体力的定位:全局力 vs 局部力

多体(n-body)力在模拟中的所有节点之间相互作用:

  • strength 为正时模拟引力(gravity),节点相互吸引;
  • strength 为负时模拟静电电荷(electrostatic charge),节点相互排斥。

链接力只影响两端相连的两个节点不同,多体力是全局的:每个节点影响其他所有节点,即使它们处于互不连通的子图中。这一特性使得力导向图中"没有边的节点也会彼此推开",是布局能够自然散开、避免节点重叠堆积的关键机制。

性能方面,该实现使用四叉树Barnes–Hut 近似大幅加速:每施加一次力,用四叉树存储当前所有节点位置,再对每个节点计算其余节点对它的合力;对于距离较远的节点簇,可以把整个簇当作一个"更大"的节点来近似计算其电荷力。d3-quadtree 文档也明确提到四叉树正是用于加速 Barnes–Hut 近似这一典型场景。

forceManyBody() 构造函数

创建一个新的多体力并应用默认参数:

const manyBody = d3.forceManyBody().strength(-100);

该函数无参调用即返回默认配置;链式调用 .strength(-100) 将斥力强度提高到默认的 3 倍多。这是 D3 力导向图示例中最常见的写法之一,当前仓库的链接力示例组件正是这样使用的:

simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody().strength(-40))
    .force("link", d3.forceLink(links).strength(1).distance(10).iterations(10))
    .force("x", d3.forceX())
    .force("y", d3.forceY())
    .on("tick", ticked);

从这份真实示例可以看出一个惯例:多体力通常以 "charge" 为名注册到模拟中,与 forceLinkforceXforceY 组合使用。注册机制本身见力模拟文档simulation.force(name, force) 一节:

const simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("link", d3.forceLink(links))
    .force("center", d3.forceCenter());

manyBody.strength(strength):正为引力,负为斥力

语义:若指定 strength,则设置强度访问器(accessor)为给定的数字或函数,并对每个节点重新求值该访问器,返回该力。正值使节点相互吸引(类似重力),负值使节点相互排斥(类似静电电荷)。若未指定参数,则返回当前强度访问器,其默认值为:

function strength() {
  return -30;
}

即默认对所有节点施加 -30 的斥力

访问器的求值时机是关键细节:强度访问器会对模拟中每个节点被调用一次,参数为 node 与其从零开始的 index。求值结果会被内部缓存,因此每个节点的 strength 只在该力被初始化(initialize)、或再次以新 strength 调用本方法时才重新计算,而不会在每次施加力时重复计算

这一"初始化时预计算"策略与力模拟文档中自定义力的约定一致:force.initialize(nodes) 在力通过 simulation.force() 绑定、或节点数组变化时被调用,力可以在初始化阶段完成逐节点参数求值,避免每次施加力时重复做这些工作。

按节点差异化强度的实例:仓库中的碰撞力示例组件展示了用函数形式访问器按节点设置不同 strength 的实战写法——让第 0 个节点(中心节点)获得与画布宽度挂钩的强斥力,其余节点斥力为 0:

.force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 3))

这说明 strength(d, i) 访问器接收的 i 即文档所述"从零开始的索引",可用于对特定节点做特殊处理。

manyBody.theta(theta):Barnes–Hut 近似的精度参数

语义:若指定 theta,将 Barnes–Hut 近似判据设置为该数字并返回该力;若未指定,则返回当前值,默认 0.9

近似的具体机制(原文档逐句要点):

  1. 每次施加力时,一个四叉树存储当前节点位置,单次施加力的时间复杂度为 O(n log n)(n 为节点数);
  2. 对每个节点,计算其余所有节点对它的合力;
  3. 对于远离当前节点的节点簇,其电荷力可以近似为"把该簇视为单个更大节点";
  4. theta 决定近似精度:若四叉树单元格的宽度 w 与该节点到该单元格质心距离 l 之比 w / l 小于 theta,则将该单元格内所有节点视为单个节点处理,而非逐一计算。

由此可以推断出调参含义:

  • theta 越大(如趋近 1)→ 更多远端簇被合并近似 → 更快,但布局精度略降
  • theta 越小(如 0.5)→ 更多节点被逐一精确计算 → 更准,但开销更大

默认值 0.9 即 D3 在"速度/精度"之间选定的折中点,对绝大多数力导向图无需改动。

manyBody.distanceMin(distance):最小距离与数值稳定性

语义:若指定 distance,设置该力起作用的节点间最小距离;若未指定,返回当前最小距离,默认 1

最小距离的作用是为两个邻近节点之间力的强度设定上界,避免布局不稳定。一个具体而重要的场景:当两个节点位置完全重合时,若没有最小距离钳制,斥力会趋于无穷大;而由于重合时方向未定义,此时力的方向会取随机方向(随机源由模拟提供,可参考 simulation.randomSource)。distanceMin 的存在保证了即使初始位置完全重叠的节点,也只受到有限且方向随机的"抖动式"推力,从而被推开而非发散。

manyBody.distanceMax(distance):最大距离与局部化布局

语义:若指定 distance,设置该力起作用的节点间最大距离;若未指定,返回当前最大距离,默认为无穷大(infinity)

指定一个有限的最大距离有两个收益(原文档明示):

  • 提升性能:超出该距离的节点对不再参与力计算;
  • 产生更局部化的布局(more localized layout):远处节点簇不再相互排斥,布局自然聚合成若干局部"团簇",在大型图上是控制视觉密度与计算成本的常用手段。

可以推断,distanceMaxtheta 是两种互补的加速/局部化手段:theta 是"远端簇近似",distanceMax 是"远端直接忽略"。

四个参数速查表

方法 默认值 取值类型 作用
strength(strength) -30 number | (node, index) => number 正为引力,负为斥力;仅初始化或重新设置时逐节点求值
theta(theta) 0.9 number Barnes–Hut 判据,w / l < theta 时簇被整体近似
distanceMin(distance) 1 number 最小作用距离,为近距离合力设上界,防止重合节点产生无穷大力
distanceMax(distance) Infinity number 最大作用距离,有限值可提速并得到局部化布局

在力模拟 tick 循环中的工作位置

理解多体力何时被调用,需要结合力模拟文档的 tick 机制。模拟实现的是速度 Verlet 积分器,假设恒定单位时间步 Δt = 1、恒定单位质量 m = 1,因此作用在粒子上的力 F 等价于该时间步内的恒定加速度 a,可以直接累加进速度。每个 tick(默认 300 次自然 tick 后停止)依次执行:

  1. alpha(alphaTarget - alpha) × alphaDecay 递增;
  2. 依次调用每个已注册力(含多体力),传入新的 alpha
  3. 每个节点速度乘以 1 - velocityDecay(默认衰减 0.4);
  4. 每个节点位置按新速度平移。

由此得出两条实战结论:

  • 力的大小受 alpha 调制:多体力每次施加时接收当前 alpha,模拟"降温"过程会随 alpha 衰减而减弱各力的作用,最终布局收敛静止。交互中"重新加热"(如拖拽节点时 alphaTarget(0.3).restart())即仓库示例组件中的标准做法;
  • strength 不在 tick 里重算:如前所述,节点强度在 force 初始化时缓存,simulation.nodes(newNodes)force.strength(newFn) 才会触发重新求值——这是修改节点属性后布局未更新的常见原因。

一个可直接运行的最小示例

综合以上要点,下面是一个体现全部四个参数的可复制示例(在浏览器中引入 D3 v7 后即可运行):

<svg width="600" height="600" viewBox="-300 -300 600 600"></svg>
<script>
const nodes = d3.range(200).map(() => ({}));

const simulation = d3.forceSimulation(nodes)
    // 多体斥力:全局作用,默认 -30,这里显式设为 -100
    .force("charge", d3.forceManyBody()
        .strength(-100)      // 排斥强度(可为函数:(d, i) => ...)
        .theta(0.9)           // Barnes–Hut 精度,w / l < 0.9 时近似
        .distanceMin(1)      // 最小距离:限制近距离合力上界
        .distanceMax(200)    // 最大距离:提速 + 局部化布局
    )
    .force("center", d3.forceCenter())
    .on("tick", ticked);

const node = d3.select("svg").selectAll("circle")
    .data(nodes).join("circle").attr("r", 3);

function ticked() {
  node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
}
</script>

其中 forceCenter 等其他内置力可查阅仓库文档目录下的 centercollideposition 各篇;自定义力的编写规范(force(alpha)force.initialize(nodes) 两个方法)见力模拟文档的 Custom forces 一节

小结

  • d3.forceManyBody() 创建全局多体力,是力导向图中节点"散开"的主力;
  • strength 决定引力/斥力及强度,支持 (node, index) 访问器做逐节点差异化配置,且仅在初始化或重新设置时求值缓存;
  • theta(默认 0.9)控制 Barnes–Hut 近似精度,是 O(n log n) 加速的核心参数;
  • distanceMin(默认 1)防止重合节点产生无穷大力的数值不稳定;distanceMax(默认无穷)可设为有限值以换取性能与局部化布局。

参考文档:many-body 官方文档力模拟四叉树链接力,以及示例组件 ExampleLinkForce.vueExampleCollideForce.vue

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384