d3-force 位置力详解:forceX、forceY 与 forceRadial 的原理、参数与实战
本文基于 d3 官方文档 位置力参考,系统讲解 d3-force 模块中的三类位置力(x 力、y 力、径向力)的创建方式、strength / x / y / radius 等访问器 API 的确切语义与默认值,并结合本仓库中的仿真示例与模拟器文档,说明这些力如何嵌入 velocity Verlet 积分循环、如何与 alpha 冷却机制协同工作,以及在实际布局中该如何取值调参。
位置力是什么:沿指定维度把节点推向目标位置
在 d3-force 的力导向仿真中,位置力(Position forces)由三个力构成:
- x 力(
forceX):沿 x 轴把节点推向给定的目标位置; - y 力(
forceY):沿 y 轴把节点推向给定的目标位置; - 径向力(
forceRadial):与二者类似,但把节点推向以某点为圆心、给定半径的圆上距离节点最近的点。
三者的共同特征是:力的强度与“节点当前位置到目标位置的一维距离成正比——离目标越远,推力越大,接近目标后推力逐渐减弱,因此节点会平滑地趋近目标而不是生硬地跳过去。文档同时强调,虽然这些力也可以用来定位个别节点,但其主要用途是作为作用于所有(或大多数)节点的全局力,例如把整张图拉向画布中心。
从 CHANGES.md 的历史记录可以看出:d3-force v3 重构时引入了独立的 x / y 位置力,取代了旧版 d3.layout.force 中的 gravity(重力)配置,并且力参数普遍支持按节点或按链接单独配置。也就是说,位置力是 d3 力仿真“可组合、可扩展”设计的一部分:仿真器本身不再内置任何力,你在 仿真 上通过 simulation.force(name, force) 自由挂载。
forceX(x):创建 x 方向的位置力
const x = d3.forceX(width / 2);
forceX(*x*) 创建一个沿 x 轴指向给定位置 x 的新位置力。若未指定 x,目标位置默认为 0。典型用法是把图整体拉向画布水平中心 width / 2。
x.strength(strength):力的强度
若指定 strength,则设置强度访问器(number 或 function),对每个节点重新求值后返回该力;若不指定,则返回当前强度访问器。
strength 决定每次施加力时为节点 x 速度增加的量,计算式为:
Δvx = (*x* - node.x) × strength
例如取 0.1 表示每次施加时,节点向其目标 x 位置移动“剩余距离的十分之一”。值越大,节点越快到达目标位置,但通常以牺牲其他力或约束的效果为代价(比如把斥力、链接力压过)。官方明确建议:不建议使用 [0,1] 范围之外的值——小于 0 会反转方向(变成排斥),大于 1 会导致速度超调、数值不稳定。
默认强度访问器为:
function strength() {
return 0.1;
}
访问器会被用于仿真中的每个节点(参见 仿真节点),调用时传入 node 与其零基 index。返回的数字会被内部缓存,即每个节点的强度只在两种时机重新计算:
- 该力被
initialize(绑定到新仿真或节点数组变更时); - 调用
x.strength(新值)时。
而不是在每次 tick 施加力时都重新求值——这对性能敏感的大图布局很重要。
x.x(x):目标 x 坐标访问器
若指定 x,设置 x 坐标访问器(number 或 function),对每个节点重新求值后返回该力;若不指定,返回当前 x 访问器,默认为:
function x() {
return 0;
}
与 strength 相同,x 访问器接收 node 与零基 index,结果数字被内部缓存,目标坐标只在力初始化或调用 x.x(新值) 时重新计算,而非每次施加力时。利用“可按节点取不同目标值”这一点,可以做出分层布局:例如把不同组的节点推向不同的纵列位置:
const x = d3.forceX()
.x((d) => d.column * 160) // 每个节点的目标 x 由其所属列决定
.strength(0.1);
forceY(y):创建 y 方向的位置力
forceY 与 forceX 完全对称:
const y = d3.forceY(height / 2);
forceY(*y*) 创建沿 y 轴指向给定位置 y 的位置力,未指定时默认为 0。
y.strength(strength)
指定时设置强度访问器并返回该力;未指定时返回当前访问器。strength 决定为节点 y 速度增加的量:
Δvy = (*y* - node.y) × strength
同样地,0.1 表示每次移动剩余距离的十分之一;高值收敛更快但可能压过其他力;不建议使用 [0,1] 范围之外的值。默认访问器:
function strength() {
return 0.1;
}
访问器对每个节点求值,传入 node 与零基 index,结果被内部缓存,仅在力初始化或设置新 strength 时重算。
y.y(y)
指定时设置 y 坐标访问器并返回该力;未指定时返回当前访问器,默认为:
function y() {
return 0;
}
语义与 x.x 一致:访问器按节点求值、结果缓存、仅在初始化或重新设置时更新。实践中 forceX() + forceY() 成对使用、均指向画布中心,是 d3 网络图最经典的“软居中”手段。
forceRadial(radius, x, y):径向位置力
const radial = d3.forceRadial(r, width / 2, height / 2);
forceRadial(*radius*, *x*, *y*) 创建一个朝向指定 radius、以 ⟨x, y⟩ 为圆心的圆的位置力。x 与 y 未指定时默认圆心为 ⟨0,0⟩。与一维位置力不同,径向力的目标是“圆上离节点最近的点”,因此节点最终会散布在这个圆环上,适合做同心环图、旭日式布局或按层级半径分层的网络图。
radial.strength(strength)
指定时设置强度访问器并返回该力;未指定时返回当前访问器。strength 决定为节点 x 与 y 速度增加的量:0.1 表示每次施加时,节点向圆上最近点移动剩余距离的十分之一。高值收敛更快但通常以牺牲其他力或约束为代价;不建议使用 [0,1] 范围之外的值。默认访问器:
function strength() {
return 0.1;
}
访问器对每个节点求值(传入 node 与零基 index),结果内部缓存,仅在力初始化或设置新 strength 时重算。
radial.radius(radius)
指定时把圆的半径设置为给定的 number 或 function,对每个节点重新求值后返回该力;未指定时返回当前 radius 访问器。与 strength、坐标访问器同理,radius 访问器对每个节点求值(传入 node 与零基 index),结果被缓存,目标半径仅在力初始化或调用 radial.radius(新值) 时重新计算。按节点返回不同半径(如按层级 depth 取值)即可实现多层同心环。
radial.x(x) 与 radial.y(y)
radial.x(*x*):指定时把圆心 x 坐标设置为给定的 number 并返回该力;未指定时返回当前圆心 x 坐标,默认为0。radial.y(*y*):指定时把圆心 y 坐标设置为给定的 number 并返回该力;未指定时返回当前圆心 y 坐标,默认为0。
注意二者与 x.x() / y.y() 的语义差异:径向力的圆心坐标是普通数值,不是按节点求值的访问器——圆心对全体节点是同一个点,而 forceRadial 中按节点变化的量只有 radius 和 strength。
位置力如何嵌入仿真的每个 tick
理解参数行为的前提是理解力的执行时机。按 仿真正文 的描述,d3-force 实现了一个 velocity Verlet 数值积分器,假设恒定单位时间步 Δt = 1 与单位质量 m = 1,因此力等价于对该时间段的恒定加速度,直接累加到粒子速度上再累加到位置。
每次 simulation.tick 的流程是:
- 当前
alpha增加(alphaTarget - alpha) × alphaDecay(默认alphaDecay ≈ 0.0228,对应 300 次迭代后冷却到alphaMin = 0.001); - 依次调用每个已注册的力,并传入新的
alpha——位置力就在此刻把Δv = (目标 - 当前位置) × strength计入节点速度; - 每个节点速度按
velocity × velocityDecay(默认0.4)衰减; - 节点位置增加速度。
由此可以推断位置力调参的两个联动关系:
- alpha 冷却决定“收手”时机:力每次 tick 都收到当前
alpha,仿真逐渐冷却、alpha降到alphaMin时定时器停止。位置力越晚被加入或strength越大,越可能在冷却前改变最终布局; - 与
velocityDecay的摩擦竞争:默认 0.4 的速度衰减相当于空气阻力。位置力strength高、摩擦低时节点运动更活跃,适合交互中持续重加热;反之则布局更快静止。
另外,节点的固定位置 fx / fy(见 simulation.nodes)在每个 tick 力施加完成后被强制重置,因此被 fx/fy 钉住的节点不会被位置力移动——拖拽交互中正是利用了这一点。
仓库中的真实用法
本仓库 VitePress 文档站自带了两个直接使用位置力的示例组件,展示了不同的取值策略:
默认强度居中——力导向图主页示例 ExampleDisjointForce.vue 用默认参数成对挂载 x、y 力,把整张图拉向坐标原点附近:
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id((d) => d.id))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
这里 forceX() / forceY() 的目标坐标与强度全部取默认值(目标 0、strength 0.1),SVG 的 viewBox 以 [-width/2, -height/2, width, height] 定义、原点居中,因此节点自然聚集在画布中央。
弱强度“围栏”——碰撞检测示例 ExampleCollideForce.vue 把强度调低到 0.01:
simulation = d3.forceSimulation(nodes)
.velocityDecay(0.1) // low friction
.force("x", d3.forceX().strength(0.01))
.force("y", d3.forceY().strength(0.01))
.force("collide", d3.forceCollide().radius((d) => d.r + 1).iterations(4))
.force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 3))
.on("tick", ticked);
该示例把 velocityDecay 降到 0.1 以降低摩擦、让碰撞分离更明显,同时把 x/y 力降到 0.01,只起“防止节点逃逸出画布”的软约束作用,而不是主导布局——这正是文档所说“高值会牺牲其他力效果”的逆向应用:当布局应由碰撞、斥力等主导时,位置力就应弱化为边界约束。
参数速查与调参建议
| 力 | 创建签名 | 默认目标 | strength 默认 | 可访问器化的参数 |
|---|---|---|---|---|
| x 力 | d3.forceX(x) |
x = 0 |
0.1 |
strength、x |
| y 力 | d3.forceY(y) |
y = 0 |
0.1 |
strength、y |
| 径向力 | d3.forceRadial(radius, x, y) |
圆心 ⟨0,0⟩ | 0.1 |
strength、radius(x、y 为普通数值) |
调参要点(均以 位置力文档 的表述为准):
- strength 保持在 [0,1] 内。
0.1是“每次靠近剩余距离 10%”的默认折中;0.01量级适合做围栏/软约束;接近1时节点几乎瞬间吸附到目标,会压过斥力与链接力,还可能振荡。 - 坐标/半径/强度都可以按节点取值。传入函数即可在节点级别差异化,例如按
group、depth指定不同的目标列、目标半径或强度;记住访问器结果被缓存,只在initialize或 setter 调用时重算——若依赖运行时变化的外部状态,需重新调用 setter 触发重算。 - x/y 力是“软”居中,不同于
forceCenter的刚性平移;fx/fy钉住的节点不受其影响。 - 径向力的圆心是全局唯一数值,按节点变化的量只有
radius与strength,设计多层环时靠radial.radius(d => ...)区分层级。
完整的 API 索引可在 docs/api.md 的 d3-force 一节中找到:d3.forceX、x.strength、x.x、d3.forceY、y.strength、y.y、d3.forceRadial、radial.strength、radial.radius、radial.x、radial.y 十一条均对应 docs/d3-force/position.md 中的同名小节;与仿真器的关系参见 docs/d3-force.md 与 docs/d3-force/simulation.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00