d3 forceLink 链接力详解:用弹簧模型稳定力导向图布局(d3 v7)
本文围绕 d3 官方文档 link.md 讲解 d3-force 模块中最核心的布局力——链接力(link force):它如何用弹簧模型把相连节点拉向目标距离,以及 forceLink 的每个配置方法(links、id、distance、strength、iterations)的完整参数语义、默认值与可变性(mutability)陷阱。读完本文,你将能在 d3(v7.9.0,依赖 d3-force ^3.0.0)中正确地为网络图、层级图与晶格结构配置链接力,并理解参数何时重新计算、何时不重新计算。
链接力是什么:一条有目标长度的弹簧
d3-force 模块基于 velocity Verlet 数值积分器模拟作用在节点(粒子)上的物理力,可用于可视化网络、层级结构以及解决碰撞(气泡图)等场景,模块总览见 d3-force.md。链接力(link force)是该模块中负责“结构”的力:
链接力按期望的链接距离(link distance)把相连节点拉近或推远,力的大小与两节点当前距离和目标距离的差值成正比,类似弹簧力。
也就是说,每条 link 都等价于一根弹簧:节点间距偏离 distance 时产生恢复力,偏离越大力越大。这决定了链接力本身不是“把图撑开”的力——撑开由 many-body 斥力 完成,链接力负责让“该连着”的节点维持在一个合理间距上。两者配合才能形成稳定的力导向布局。
forceLink(links):创建链接力
创建方式(默认参数):
const link = d3.forceLink(links).id((d) => d.id);
forceLink(links)用指定的links数组和默认参数创建一个新链接力;- 若不提供
links,默认为空数组[]; - 注意:该函数是“不纯”的(impure),它会修改(mutate)传入的
links数组,详见下文link.links一节。
链接力最终通过 simulation.force(name, force) 挂到仿真器上,仿真器每步 tick 时以新的 alpha 调用它;完整机制见 simulation.force:
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink(links))
.force("center", d3.forceCenter());
link.links(links):设置并解析 link 数组
如果提供了 links,则设置该力关联的链接数组,并为每条 link 重新计算 distance 和 strength 参数,返回该力;如果不提供,返回当前的 links 数组(默认空数组)。
每条 link 是如下形状的对象:
source— link 的源节点(见 simulation.nodes)target— link 的目标节点index— 在links数组中的从零起始下标,由该方法自动赋值
为了方便,link 的 source/target 可以用数字或字符串标识符初始化(而不是对象引用),配合 link.id 使用。
可变性警告(原文档明确标注):
该函数是不纯的。链接力被初始化(或重新初始化,例如节点或链接发生变化时)时,它可能修改传入的
links。任何不是对象的link.source或link.target都会被替换为对应node的对象引用。
这意味着初始化后,你的原始 JSON 里的 "source": "Alice" 会变成指向节点对象本身——这也是为什么很多代码在 tick 回调里可以直接写 d.source.x、d.target.y。
另外一个重要约定:如果 links 数组本身被修改了(向仿真中增删链接),必须再次调用 link.links(新数组) 通知该力;力不会为指定数组做防御性拷贝。nodes 的修改同理,见 simulation.nodes。
link.id(id):节点标识符访问器
如果提供了 id,把节点 id 访问器设置为指定函数并返回该力;否则返回当前的 id 访问器,默认为返回数字 node.index:
function id(d) {
return d.index;
}
默认的 id 访问器允许每条 link 的 source 和 target 用 nodes 数组的从零起始下标 表示。例如:
const nodes = [
{"id": "Alice"},
{"id": "Bob"},
{"id": "Carol"}
];
const links = [
{"source": 0, "target": 1}, // Alice → Bob
{"source": 1, "target": 2} // Bob → Carol
];
再换一个返回字符串的 id 访问器:
function id(d) {
return d.id;
}
此时就可以用命名的 source 和 target:
const nodes = [
{"id": "Alice"},
{"id": "Bob"},
{"id": "Carol"}
];
const links = [
{"source": "Alice", "target": "Bob"},
{"source": "Bob", "target": "Carol"}
];
这一点在用 JSON 表示图时尤其有用,因为 JSON 不允许对象引用(这也是为什么网络图数据通常直接以这种 nodes + links 的扁平结构存储)。id 访问器在力初始化时(例如 nodes 或 links 变化时)对每个节点被调用一次,参数为节点对象及其从零起始下标。
link.distance(distance):目标距离访问器
如果提供了 distance,把距离访问器设置为指定数字或函数,对每条 link 重新求值距离访问器,并返回该力;否则返回当前距离访问器,默认为:
function distance() {
return 30;
}
距离访问器对每条 link 被调用,参数为 link 及其从零起始 index。求出的数字会被保存在内部——因此每条 link 的距离只在力被初始化、或调用该方法传入新 distance 时重新计算,而不是在每次施加力时重新计算。这是一个值得记住的性能与语义细节:写 distance(30) 之后,tick 过程中不会重复求值。
link.strength(strength):弹簧强度的默认公式
如果提供了 strength,把强度访问器设置为指定数字或函数,对每条 link 重新求值,并返回该力;否则返回当前强度访问器,默认为:
function strength(link) {
return 1 / Math.min(count(link.source), count(link.target));
}
其中 count(node) 返回以该节点作为 source 或 target 的链接数。这个默认公式的动机是:自动降低连到高度数节点(hub)的链接强度,从而提升整体稳定性——枢纽节点连接的每条边都弱一些,避免它被周围大量弱约束拉扯得剧烈抖动。
强度访问器同样对每条 link 被调用一次(参数为 link 与 index),结果保存在内部,只在力初始化或传入新 strength 时重新计算,不会在每次施加力时重复求值。
link.iterations(iterations):迭代次数与结构刚性
如果提供了 iterations,把每次施加时的迭代次数设置为指定数字并返回该力;否则返回当前迭代次数,默认为 1。
增加迭代次数会大幅提高约束的刚性(rigidity),对晶格(lattice)等复杂结构非常有用,但同时也成倍增加每次施加力时的求值成本。这个取舍在文档仓库自带的可交互示例 ExampleLinkForce.vue 中体现得很直接——它构造了一个 20×20 的节点晶格,横向、纵向相邻节点之间共生成 760 条链接,然后配置:
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);
可以看到这个示例同时动用了三个链接力参数:strength(1) 覆盖默认的按度数倒数公式、distance(10) 把目标边长压到 10px、iterations(10) 把单次施加的迭代提到 10 倍以“锁死”网格结构。这正是文档所说“迭代数提高刚性”的典型用法:默认 1 次迭代的链接力更像“软弹簧”,而 10 次迭代会让相邻节点距离非常接近设定值,即使把某个节点拖到很远的地方再松手,网格也能迅速弹回整齐的形态(示例中还配合了 d3.drag() + simulation.find() 实现拖拽,拖拽期间用 alphaTarget(0.3).restart() 重新加热仿真)。
与仿真器的协作方式
链接力不是独立运行的,它遵循 d3-force 的统一力协议(见 Custom forces 一节):
- 通过 simulation.force 绑定,或 simulation.nodes 改变节点时,力的
initialize(nodes)被调用——id 访问器在此对每个节点求值,source/target 的字符串/数字标识符在此被替换为节点对象引用,distance与strength的缓存值也在此计算; - 仿真器每步 tick 时以当前
alpha调用该力;链接力读取每条 link 两端节点的当前位置,按“距离偏差 × 强度 × alpha”的比例修改两端节点的速度。
因为 alpha 会随仿真冷却逐渐衰减(见 simulation.alpha),链接力施加的力也随之减弱,布局在大约 300 步(默认 alphaDecay 下)后趋于静止。这也是为什么交互拖拽时要重新设置 alphaTarget 来“加热”仿真。
实践要点小结
结合 link.md 与仓库示例,可以归纳出以下要点:
- 数据是扁平 JSON 结构:
nodes为对象数组,links中source/target用 id 或下标,通过.id(d => d.id)解析——这是从数据到图布局的标准路径; - 两个缓存:
distance和strength求值结果都缓存在力内部,只在初始化或重新调用访问器方法时刷新;调整布局参数后,必要时需重新调用对应方法或重新links()/nodes()触发 re-initialize; - 不纯函数:
forceLink(links)和link.links(...)都会原地修改你传入的 links 数组(source/target 被替换为对象引用、写入 index),不要假设原始数据保持不变; - 默认 strength 自带防抖:
1 / Math.min(deg(source), deg(target))自动削弱 hub 节点上的边,普通网络图通常无需自定义; - iterations 是刚性旋钮:默认 1;需要晶格、分子结构等强几何约束时提高到 5~10,代价是求值成本线性增长(参见 ExampleLinkForce.vue 中
iterations(10)的用法); - 动态增删节点/链接:修改数组后必须重新调用 simulation.nodes /
link.links(),力不做防御性拷贝。
在 d3 v7 中,d3.forceLink 由 d3-force ^3.0.0 提供,并通过 src/index.js 的 export * from "d3-force" 统一导出,安装 d3 包后即可直接使用 d3.forceLink(...);本仓库中所有 d3-force 相关文档页面(simulation、center、collide、many-body、position)均可作为配套参考,围绕链接力构建完整的力导向布局。
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