概述
D3(或D3.js)是一个 JavaScript 库,用于使用 Web 标准可视化数据。D3 帮助您使用 SVG、Canvas 和 HTML 使数据栩栩如生。D3 将强大的可视化和交互技术与数据驱动的 DOM 操作方法相结合,为您提供现代浏览器的全部功能,并为您的数据设计合适的可视化界面的自由。
力导向布局
D3力导向概述
力导向布局(force-directed layout)是一种基于物理学原理的图形布局算法,它使用节点之间的引力和斥力来决定节点在可视化图形中的位置。在D3中,力导向布局是通过d3-force
模块实现的。
在力导向布局中,每个节点都是一个带有位置、速度和加速度的物体,节点之间的连线被看作是弹簧,节点与节点之间的引力、节点与节点之间的斥力以及每个节点的阻尼都被建模为物理量。这些物理量会在每个迭代步骤中更新,并且节点位置会根据当前速度和加速度更新。
D3力导向布局主要包括以下步骤:
- 创建一个力导向布局对象:使用
d3.forceSimulation()
函数创建一个力导向布局对象。 - 定义节点和边:使用
nodes()
和links()
函数分别定义节点和边。 - 配置节点和边的力属性:使用
force()
函数来配置节点和边的力属性,例如设置节点之间的斥力和吸引力的大小,以及阻尼大小等。 - 启动力导向布局:使用
simulation.alpha(1).restart()
函数启动力导向布局,将节点和边放置到初始位置并开始模拟力学运动。 - 监听布局的变化:使用
simulation.on()
函数来监听布局变化,例如节点和边的位置更新、力的变化等。 - 更新可视化图形:在每个布局变化事件发生时,使用D3的数据绑定和选择方法来更新可视化图形的位置和属性。
通过调整力导向布局的参数和添加其他交互性的功能,可以创建各种类型的动态可视化图形,例如网络图、关系图等。
力导向说明
力模拟
d3力模拟是利用一种叫做”velocity Verlet”的数值积分算法,在简化的物理假设(恒定时间步长、恒定粒子质量)下,通过每一步都把作用力转化为加速度,并累加到粒子的速度和位置上,从而逐步模拟出粒子在各种力作用下的运动轨迹。这种以牛顿运动定律为基础的简化物理模型,是d3力模拟能够生成逼真的力导向布局动画的原理所在。虽然它对时间和质量做了任意单位的假设,但并不影响模拟效果,因为我们更关注的是粒子运动的相对关系,而不是绝对数值。
forceSimulation-创建一个新的力模拟
forceSimulation 函数是 d3 力模拟的入口函数,用于创建一个新的力模拟实例。
`forceSimulation` 函数接受一个可选的参数 `nodes`,表示参与模拟的节点数组。如果没有传入 `nodes` 参数,模拟中的初始节点数组就是空数组 `[]`。
文档中特别强调了一点:传入的 `nodes` 数组会被 `forceSimulation` 函数修改。这是因为模拟过程中节点的位置和速度等属性会不断更新,而这些更新是直接作用于传入的 `nodes` 数组的。因此,如果你不希望原始的节点数据被修改,就需要在传入前创建一份拷贝。这一点可以通过 `simulation.nodes` 方法来实现,详见后续文档。
const simulation = d3.forceSimulation(nodes);
创建后的力模拟实例会自动开始运行。你可以通过 `simulation.on` 方法监听模拟的 `tick` 事件,在每一帧触发时执行自定义的更新逻辑。
如果你想手动控制模拟的运行,可以先调用 `simulation.stop` 方法暂停模拟,然后在需要的时候调用 `simulation.tick` 方法来推进模拟一帧。
simulation. restart()-力模拟的内部计时器重新启动
当调用 simulation.restart() 时,模拟的内部计时器会重新启动,同时模拟实例本身会被返回。这个方法通常与 simulation.alphaTarget 或 simulation.alpha 一起使用,用于在交互时”重热”模拟,或在暂停后恢复模拟。
在力模拟中,”alpha”表示模拟的能量或活跃度,其取值范围是 [0, 1]。当 alpha 为 1 时,模拟处于最活跃状态,节点会快速运动;当 alpha 逐渐衰减到 0 时,模拟逐渐冷却,节点趋于静止。你可以通过设置 alphaTarget 属性来指定模拟的目标能量水平,或直接修改 alpha 属性来即时调整当前能量水平。
一个常见的使用场景是,在交互操作(比如拖拽节点)时,先调用 simulation.alphaTarget(1) 将模拟的目标能量调高,然后调用 simulation.restart() 重启模拟,这样节点就会重新快速运动,响应交互的变化。
另一个场景是,在调用 simulation.stop() 暂停模拟后,可以通过调用 simulation.restart() 来恢复模拟的运行。
总之,restart 方法让你可以方便地控制模拟的重启和恢复,配合 alphaTarget 和 alpha 属性,可以实现在交互时重新”加热”模拟的效果,提高力导向图的交互体验。
simulation.stop()
当调用 simulation.stop() 时,如果模拟的内部计时器正在运行,它就会停止,同时模拟实例本身会被返回。如果计时器已经处于停止状态,则这个方法不会做任何事情。
stop 方法的主要作用是暂停模拟的自动运行,让你可以手动控制模拟的进程。这在某些场景下很有用,比如:
- 你希望在每一帧中插入一些自定义的逻辑,而不是让模拟连续不断地运行;
- 你需要暂停模拟一段时间,然后在满足某些条件时再恢复运行;
- 你想要完全手动地控制模拟的每一帧,而不依赖内部计时器。
在第三种情况下,你可以先调用 simulation.stop() 停止自动运行,然后根据需要反复调用 simulation.tick() 来推进模拟。每调用一次 tick 方法,模拟就会前进一帧。这种做法让你可以精确地控制模拟的进度,根据自己的逻辑在每一帧中执行任意操作。
需要注意的是,如果你在暂停模拟后又重新启动了它(比如通过 simulation.restart() 方法),那么在此期间通过 tick 方法产生的手动推进可能会被覆盖掉。因此,在手动控制模拟时,通常应该避免调用 restart 等方法。
simulation.tick(iterations)
simulation.tick(iterations) 方法用于手动推进模拟指定的 iterations 次迭代,然后返回模拟实例本身。如果没有指定 iterations 参数,默认为 1,即只推进一次迭代。
在每次迭代中,模拟会执行以下步骤:
- 根据公式 (alphaTarget – alpha) × alphaDecay 更新当前的 alpha 值;
- 调用每个已注册的力函数,并将新的 alpha 值传递给它们;
- 根据公式 velocity × velocityDecay 衰减每个节点的速度;
- 根据节点的速度更新每个节点的位置。
需要注意的是,与 restart 方法不同,手动调用 tick 方法并不会触发任何事件。力模拟的事件只在两种情况下派发:一是在模拟创建时自动启动;二是调用 restart 方法重新启动模拟。当模拟自动启动时,默认的迭代次数为 ⌈log(alphaMin) / log(1 – alphaDecay)⌉,即300次。
tick 方法通常与 stop 方法一起使用,用于计算静态的力导向布局。对于大型图形,建议在 Web Worker 中计算静态布局,以避免冻结用户界面。
simulation.nodes(nodes)
simulation.nodes(nodes) 方法有两种用法:
- 当传入 nodes 参数时,该方法将模拟的节点数组设置为指定的对象数组,必要时初始化它们的位置和速度,然后重新初始化所有绑定的力,最后返回模拟实例。这种用法通常在修改节点数组(如添加或删除节点)后调用,以通知模拟和力函数数据已发生变化。
- 当不传入 nodes 参数时,该方法返回模拟的当前节点数组,即最初传给构造函数的数组。
文档中特别强调了一点:这个方法是不纯的,因为它会修改传入的 nodes 数组,为每个节点对象添加或更新以下属性:
- index: 节点在数组中的索引(从0开始)
- x, y: 节点当前的位置坐标
- vx, vy: 节点当前的速度向量
节点的位置和速度可以在模拟运行过程中被力函数和 tick 方法进一步修改。如果节点的速度分量 vx 或 vy 为 NaN,则会被初始化为0;如果位置分量 x 或 y 为 NaN,则会按照螺旋排列的方式初始化,以确保分布的确定性和均匀性。
如果要将某个节点固定在特定位置,可以为它指定两个额外的属性:
- fx: 节点的固定 x 坐标
- fy: 节点的固定 y 坐标
在每次 tick 结束时,对于具有 fx 属性的节点,其 x 坐标会被重置为 fx 的值,vx 会被设为0;同样,对于具有 fy 属性的节点,其 y 坐标会被重置为 fy 的值,vy 会被设为0。如果要取消对一个节点的固定,可以将其 fx 和 fy 属性设为 null,或者直接删除这两个属性。
需要注意的是,如果传入 nodes 方法的数组被修改了,必须再次调用该方法,将新的(或改变的)数组传入,以通知模拟和力函数数据已发生变化。模拟不会主动创建传入数组的防御性拷贝。
simulation.alpha(alpha)
在力导向图的模拟中,alpha 可以类比于模拟退火算法中的温度。随着模拟的进行,alpha 会逐渐降低,就像是模拟在”冷却”。当 alpha 降低到 alphaMin 时,模拟就会停止。你可以通过调用 simulation.restart() 方法来重新开始模拟。
simulation.alpha(alpha) 方法有两种用法:
- 当传入 alpha 参数时,该方法会将模拟的当前 alpha 值设置为指定的数字,该数字必须在 [0, 1] 的范围内,然后返回模拟实例。这种用法允许你手动设置模拟的 alpha 值,例如在交互时提高 alpha 以使节点更快地移动。
- 当不传入 alpha 参数时,该方法会返回模拟的当前 alpha 值,默认为 1。
通过调整 alpha 值,你可以控制模拟的”温度”,影响节点运动的活跃程度。alpha 值越高,节点运动越活跃,布局变化越快;alpha 值越低,节点运动越平缓,布局变化越慢。
在模拟开始时,alpha 通常被设为一个较高的值(默认为1),以便节点能够快速移动并逐步形成合适的布局。随着模拟的进行,alpha 会以 alphaDecay 的速率不断衰减,直到达到 alphaMin 为止(默认为0.001)。这个过程模拟了退火算法中温度从高到低的变化,使得布局能够在快速变化后逐步稳定下来。
你可以根据实际需要,通过 alpha 方法来动态调整模拟的 alpha 值,或者通过 alphaMin、alphaDecay 等参数来设置 alpha 衰减的模式和阈值,从而影响力导向图的布局效果和收敛速度。
simulation.alphaMin(min)
`simulation.alphaMin(min)` 方法用于设置或获取模拟的最小 `alpha` 值。
当传入 `min` 参数时,该方法会将模拟的最小 `alpha` 值设置为指定的数字,该数字必须在 [0, 1] 的范围内,然后返回模拟实例。这允许你自定义模拟的停止条件。
当不传入 `min` 参数时,该方法会返回模拟的当前最小 `alpha` 值,默认为 0.001。
在模拟运行过程中,当前的 `alpha` 值会不断衰减。当它小于最小 `alpha` 值时,模拟的内部计时器就会停止,意味着模拟完成了。
默认情况下,`alpha` 的衰减率约为 0.0228,这对应于300次迭代。也就是说,如果使用默认的最小 `alpha` 值(0.001)和默认的衰减率,模拟将在大约300次迭代后停止。
通过调整最小 `alpha` 值,你可以控制模拟的运行时长。如果你增大最小 `alpha` 值(如0.01),模拟将更早停止,因为当前 `alpha` 值会更快到达停止条件。反之,如果你减小最小 `alpha` 值(如0.0001),模拟将运行更长时间,因为当前 `alpha` 值需要更多的迭代才能降低到停止条件。
需要注意的是,最小 `alpha` 值只是模拟停止的必要条件,而不是充分条件。在某些情况下,由于力的作用,节点可能会在 `alpha` 到达最小值之前就已经基本稳定下来。这时,你可以通过调用 `simulation.stop()` 方法来手动停止模拟,或者监听 `end` 事件来在模拟自然结束时执行一些操作。
合理地设置最小 `alpha` 值,可以帮助你在布局效果和性能之间取得平衡。
simulation.alphaDecay(decay)
`simulation.alphaDecay(decay)` 方法用于设置或获取 `alpha` 的衰减率。
当传入 `decay` 参数时,该方法会将 `alpha` 的衰减率设置为指定的数字,该数字必须在 [0, 1] 的范围内,然后返回模拟实例。这允许你自定义 `alpha` 的衰减速度。
当不传入 `decay` 参数时,该方法会返回当前的 `alpha` 衰减率,默认为 0.0228…,这个值是通过公式 1 – pow(0.001, 1 / 300) 计算得到的,其中 0.001 是默认的最小 `alpha` 值。
`alpha` 衰减率决定了当前 `alpha` 值向目标 `alpha` 值收敛的速度。由于默认的目标 `alpha` 值为零,因此默认情况下,衰减率控制了模拟冷却的速度。
– 较高的衰减率会导致模拟更快地稳定下来,但可能会使布局陷入局部最优而无法达到全局最优;
– 较低的衰减率会使模拟运行更长的时间,但通常能收敛到更好的布局。
如果你希望模拟永远以当前的 `alpha` 值运行,可以将衰减率设为0。或者,你也可以将目标 `alpha` 值设置为大于最小 `alpha` 值,以达到类似的效果。
通过调整 `alpha` 衰减率,你可以控制力导向图的收敛速度和布局质量。较高的衰减率适用于快速生成一个可接受的布局,而较低的衰减率则适用于生成一个尽可能最优的布局。
在实际使用中,你可以根据数据的规模和复杂度,以及对布局质量和性能的要求,来选择合适的 `alpha` 衰减率。通常建议从默认值开始,然后根据需要进行调整。如果模拟收敛得太慢,可以尝试提高衰减率;如果模拟得到的布局不理想,可以尝试降低衰减率。
simulation.alphaTarget(target)
`simulation.alphaTarget(target)` 方法用于设置或获取模拟的目标 `alpha` 值。
当传入 `target` 参数时,该方法会将模拟的当前目标 `alpha` 值设置为指定的数字,该数字必须在 [0, 1] 的范围内,然后返回模拟实例。这允许你指定模拟的目标能量水平。
当不传入 `target` 参数时,该方法会返回模拟的当前目标 `alpha` 值,默认为0。
在模拟的每一次迭代中,当前的 `alpha` 值都会朝着目标 `alpha` 值的方向更新。更新的速率由 `alphaDecay` 决定。
默认情况下,目标 `alpha` 值为0,这意味着模拟会逐渐”冷却”直到完全停止。然而,通过设置一个非零的目标 `alpha` 值,你可以让模拟保持在一个特定的能量水平上。这在某些交互场景下很有用,比如:
– 在用户拖拽节点时,将目标 `alpha` 值设为一个较高的值(如0.5),以保持其他节点的响应性;
– 在用户停止交互后,将目标 `alpha` 值逐渐降低到一个较小的值(如0.1),以使布局缓慢稳定。
需要注意的是,将目标 `alpha` 值设置为大于最小 `alpha` 值(默认为0.001)的数字,会使得模拟永远无法完全停止,因为当前 `alpha` 值无法降到最小值以下。这种情况下,你可以通过调用 `simulation.stop()` 方法来手动停止模拟。
通过动态调整目标 `alpha` 值,你可以实现力导向图的交互式布局,使其能够响应用户的操作而变化,同时在不同的交互阶段呈现出不同的稳定性和灵活性。这种技术常用于需要用户探索和操纵复杂关系数据的可视化应用中。
simulation.velocityDecay(decay)
`simulation.velocityDecay(decay)` 方法用于设置或获取速度衰减因子。
当传入 `decay` 参数时,该方法会将速度衰减因子设置为指定的数字,该数字必须在 [0, 1] 的范围内,然后返回模拟实例。这允许你自定义节点速度的衰减率。
当不传入 `decay` 参数时,该方法会返回当前的速度衰减因子,默认值为 0.4。
速度衰减因子类似于空气阻力。在每一次模拟迭代(tick)中,在施加了所有的力之后,每个节点的速度都会乘以 1 – decay。也就是说,decay 值越大,速度衰减得越快,节点运动得越慢;decay 值越小,速度衰减得越慢,节点运动得越快。
就像降低 alpha 衰减率一样,降低速度衰减因子可能会使模拟收敛到一个更好的解,但也有可能导致数值不稳定和振荡。这是因为:
– 较低的速度衰减因子允许节点保持更高的速度,这有助于它们摆脱局部最优,找到全局最优布局;
– 但是,如果节点速度过高,它们可能会不断越过最优位置,导致布局无法稳定。
因此,在设置速度衰减因子时,需要在收敛质量和稳定性之间进行权衡。通常建议从默认值 0.4 开始,然后根据实际效果进行调整:
– 如果布局振荡得太厉害,可以尝试提高速度衰减因子,以增强阻尼效果;
– 如果布局过于僵硬,可以尝试降低速度衰减因子,以增加灵活性。
同时还要注意,速度衰减因子与其他参数(如 alpha 衰减率、力的强度等)是相互影响的。你可能需要同时调整多个参数,以达到最佳的布局效果。在这个过程中,观察模拟的行为,理解不同参数的作用,是很有必要的。
合理地设置速度衰减因子,可以帮助你获得既稳定又优化的力导向布局。这在处理大型、复杂的关系数据时尤为重要。
simulation.force(name, force)- 为模拟添加或移除指定名称的力
`simulation.force(name, force)` 方法用于为模拟添加或移除指定名称的力。
当传入 `force` 参数时,该方法会为指定的 `name` 分配对应的力,并返回模拟实例。这允许你在模拟中组合使用多种力。
当不传入 `force` 参数时,该方法会返回指定 `name` 的力(如果存在),或者 undefined(如果不存在该力)。默认情况下,新创建的模拟是没有任何力的。
文档提供了一个创建力模拟的示例代码:
const simulation = d3.forceSimulation(nodes)
.force(“charge”, d3.forceManyBody())
.force(“link”, d3.forceLink(links))
.force(“center”, d3.forceCenter());
这个例子创建了一个新的力模拟实例,并为其添加了三个力:
1. `charge` 力,使用 `d3.forceManyBody()`,用于模拟节点之间的电荷斥力,使得节点相互分离;
2. `link` 力,使用 `d3.forceLink(links)`,用于模拟节点之间的链接引力,根据 `links` 数据将相连的节点拉近;
3. `center` 力,使用 `d3.forceCenter()`,用于模拟一个指向画布中心的引力,防止节点飘逸出视图范围。
通过组合使用不同的力,你可以创建出各种具有特定布局效果的力导向图。
如果要移除一个已添加的力,可以将该力的名称 `name` 作为第一个参数,将 `null` 作为第二个参数传给 `force` 方法。例如,移除上面例子中的 `charge` 力:
simulation.force(“charge”, null);
在实际的可视化应用中,你可能需要根据不同的数据类型、布局要求、交互需求等因素,动态地添加、移除或调整力的参数。`force` 方法提供了一种灵活的方式来管理力模拟中的各种力,使得你能够精细地控制布局的生成过程。
simulation.find(x, y, radius)- 于查找在给定搜索半径内,最接近指定位置 ⟨x,y⟩ 的节点
`simulation.find(x, y, radius)` 方法用于查找在给定搜索半径内,最接近指定位置 ⟨x,y⟩ 的节点。
– `x` 和 `y` 参数指定了搜索的目标位置,通常是基于鼠标或触摸事件的坐标。
– `radius` 参数指定了搜索的半径。在这个半径范围内的节点都会被考虑。如果不指定 `radius`,则默认为无穷大,即搜索整个画布。
该方法会返回满足条件的最近节点。如果在搜索范围内没有找到任何节点,则返回 undefined。
这个方法在交互式力导向图中非常有用,特别是在处理节点的鼠标事件时。例如,你可以使用 `find` 方法来:
1. 高亮显示鼠标悬停的节点:
canvas.on(“mousemove”, function(event) {
const node = simulation.find(event.offsetX, event.offsetY, 20);
if (node) {
node.isHovered = true;
}
});
2. 实现节点的拖拽功能:
let draggedNode = null;
canvas.on(“mousedown”, function(event) {
draggedNode = simulation.find(event.offsetX, event.offsetY, 20);
});
canvas.on(“mousemove”, function(event) {
if (draggedNode) {
draggedNode.fx = event.offsetX;
draggedNode.fy = event.offsetY;
}
});
canvas.on(“mouseup”, function(event) {
draggedNode = null;
});
3. 显示节点的详细信息:
canvas.on(“click”, function(event) {
const node = simulation.find(event.offsetX, event.offsetY, 20);
if (node) {
showNodeDetails(node);
}
});
在这些例子中,`find` 方法允许我们根据鼠标或触摸的位置,快速找到相应的节点,并对其进行操作。
需要注意的是,`find` 方法使用的是欧几里得距离来衡量节点与目标位置之间的距离。如果你的节点是非圆形的(例如矩形或更复杂的形状),可能需要使用更复杂的碰撞检测算法来替代 `find` 方法。
总的来说,`find` 方法是力导向图交互功能的重要组成部分,它提供了一种简单而高效的方式来连接用户输入和图形元素。合理地使用 `find` 方法,可以大大增强力导向布局的交互性和探索性。
simulation.randomSource(source)- 设置或获取模拟中用于生成随机数的函数
`simulation.randomSource(source)` 方法用于设置或获取模拟中用于生成随机数的函数。
当传入 `source` 参数时,该方法会将模拟的随机数生成函数设置为指定的函数,并返回模拟实例。这个函数应该返回一个在 0(包含)和 1(不包含)之间的数字。
当不传入 `source` 参数时,该方法会返回模拟当前使用的随机数生成函数。默认情况下,模拟使用一个固定种子的线性同余生成器(LCG)来生成随机数。
在力导向图的模拟过程中,随机数主要用于以下几个方面:
1. 初始化节点位置:当节点没有指定初始位置时,模拟会使用随机数为其分配一个位置。
2. 解决节点重叠:有些力(如碰撞力)在处理节点重叠时,会使用随机数来决定节点的移动方向。
3. 模拟随机扰动:有些布局算法(如模拟退火)会在模拟过程中引入随机扰动,以帮助节点跳出局部最优。
默认的随机数生成器通常能满足大多数情况下的需求。但是,在某些特殊情况下,你可能希望使用自定义的随机数生成器,例如:
– 你需要生成符合特定分布(如高斯分布)的随机数;
– 你希望在不同的模拟实例之间共享相同的随机数序列,以便生成可重复的布局;
– 你想要使用一个”更随机”的生成器(如基于密码学的生成器),以提高布局的多样性。
在这些情况下,你可以使用 `randomSource` 方法来替换默认的随机数生成器。例如:
simulation.randomSource(d3.randomNormal(0, 1));
这将使用 D3 提供的 `randomNormal` 函数来生成平均值为0,标准差为1的高斯分布随机数。
需要注意的是,更改随机数生成器会影响力模拟的整个过程,可能导致布局结果的显著变化。因此,在修改随机数生成器时,要谨慎评估其必要性和影响。
总的来说,`randomSource` 方法为力模拟提供了一种自定义随机行为的方式,使得我们能够在确定性和随机性之间进行平衡,并根据具体需求来调整布局算法。合理地使用 `randomSource` 方法,可以增强力导向布局的灵活性和适应性。
simulation.on(typenames, listener)- 为指定的事件类型和名称添加或移除事件监听器
`simulation.on(typenames, listener)` 方法用于为指定的事件类型和名称添加或移除事件监听器。
– 如果指定了 `listener` 参数,该方法会为指定的 `typenames` 设置事件监听器,并返回模拟实例。如果同一事件类型和名称已经注册了监听器,则在添加新监听器之前会先移除现有监听器。
– 如果 `listener` 参数为 `null`,则移除指定 `typenames` 的当前事件监听器(如果有)。
– 如果没有指定 `listener` 参数,该方法会返回与指定 `typenames` 匹配的第一个当前分配的监听器(如果有)。
在调度指定事件时,每个 `listener` 将以模拟实例作为 `this` 上下文被调用。
`typenames` 是一个包含一个或多个由空格分隔的 `typename` 的字符串。每个 `typename` 是一个事件类型,可以选择在其后面加一个句点(`.`)和一个名称,例如 `tick.foo` 和 `tick.bar`;这个名称允许为同一事件类型注册多个监听器。事件类型必须是以下之一:
– `tick` – 在模拟内部计时器的每个 tick 之后触发。
– `end` – 在模拟计时器因为 `alpha` < `alphaMin` 而停止时触发。
需要注意的是,当手动调用 `simulation.tick` 时,`tick` 事件不会被触发;事件只会由内部计时器调度,并且旨在用于模拟的交互式渲染。如果要影响模拟,应该注册力,而不是在 tick 事件监听器中修改节点的位置或速度。
使用 `on` 方法,你可以在模拟的不同阶段执行自定义的操作,例如:
1. 在每个 tick 事件中更新节点和链接的可视化:
“`js
simulation.on(“tick”, function() {
node.attr(“cx”, d => d.x)
.attr(“cy”, d => d.y);
link.attr(“x1”, d => d.source.x)
.attr(“y1”, d => d.source.y)
.attr(“x2”, d => d.target.x)
.attr(“y2”, d => d.target.y);
});
“`
2. 在模拟结束时执行一些清理或最终处理:
“`js
simulation.on(“end”, function() {
console.log(“Simulation ended.”);
// 执行一些清理或最终处理…
});
“`
3. 为同一事件类型注册多个监听器,以执行不同的任务:
“`js
simulation.on(“tick.updateNodes”, function() {
// 更新节点…
});
simulation.on(“tick.updateLinks”, function() {
// 更新链接…
});
“`
总的来说,`on` 方法提供了一种灵活的机制,让你能够在力模拟的关键时刻执行自定义的逻辑,以实现交互式渲染、数据处理、性能监测等目的。合理地使用 `on` 方法,可以增强力导向布局的可定制性和可扩展性,使其更好地适应你的应用需求。
Custom forces-自定义力
力是一个修改节点位置或速度的函数。它可以模拟电荷、重力等物理力,也可以实现一些几何约束,例如将节点保持在边界框内,或者将链接节点保持在固定距离内。文档给出了一个将节点吸引到原点的力的例子:
function force(alpha) {
for (let i = 0, n = nodes.length, node, k = alpha * 0.1; i < n; ++i) {
node = nodes[i];
node.vx -= node.x * k;
node.vy -= node.y * k;
}
}
力通常会读取节点的当前位置 ⟨x,y⟩,然后修改节点的速度 ⟨vx,vy⟩。力也可能”预测”节点的下一个位置 ⟨x + vx, y + vy⟩;这对于通过迭代松弛来解决几何约束是必要的。力也可以直接修改位置,这有时对于避免向模拟中添加能量很有用,例如在视口中重新定位模拟时。
文档还描述了力函数的两个主要方法:
1. `force(alpha)`: 应用此力,可以选择观察指定的 `alpha` 值。通常,力会应用于之前传递给 `force.initialize` 的节点数组,但是,有些力可能应用于节点的子集,或者有不同的行为。例如,`forceLink` 应用于每个链接的源节点和目标节点。
2. `force.initialize(nodes)`: 为该力提供节点数组和随机源。当通过 `simulation.force` 将力绑定到模拟时,以及当通过 `simulation.nodes` 改变模拟的节点时,会调用此方法。力可以在初始化期间执行必要的工作,例如评估每个节点的参数,以避免在每次应用力时重复执行工作。
综合来看,自定义力让你能够根据具体的布局需求,在标准的力的基础上,引入新的作用规则。例如,你可以创建一个力来:
– 将节点限制在特定的区域内
– 根据节点的属性(如大小或分组)对节点施加不同的力
– 模拟特殊的物理效果,如风力、磁力等
– 实现自定义的布局约束,如层次结构、对称性等
在创建自定义力时,通常需要注意以下几点:
1. 力应该是无状态的,其效果只取决于当前的节点位置和速度。
2. 力应该能够逐步应用,即在每个模拟 tick 中产生一个小的改变,而不是一次性的大变化。
3. 力不应该直接修改节点的位置,而是通过修改速度来影响位置,以保持模拟的稳定性。
4. 在 `initialize` 方法中预先计算好所需的参数和数据结构,以提高力的应用效率。
总的来说,自定义力是一个强大的工具,它允许你在 D3 力导向图的框架内,灵活地实现各种复杂的布局效果。合理地设计和使用自定义力,可以大大提升力导向布局的表现力和适用性。
中心力
中心力uniformly地移动节点,使所有节点的平均位置(如果所有节点具有相同的权重,则为质心)处于给定的位置 ⟨x,y⟩。这个力在每次应用时修改节点的位置;它不修改速度,因为这样做通常会导致节点超过并在期望的中心周围振荡。这个力有助于将节点保持在视口的中心,与位置力不同,它不会扭曲节点的相对位置。
文档介绍了中心力的几个方法:
1. `forceCenter(x, y)`: 使用指定的 x 和 y 坐标创建一个新的中心力。如果未指定 x 和 y,它们默认为 ⟨0,0⟩。
const center = d3.forceCenter(width / 2, height / 2);
2. `center.x(x)`: 如果指定了 x,则将中心位置的 x 坐标设置为指定的数字,并返回该力。如果未指定 x,则返回当前的 x 坐标,默认为零。
3. `center.y(y)`: 如果指定了 y,则将中心位置的 y 坐标设置为指定的数字,并返回该力。如果未指定 y,则返回当前的 y 坐标,默认为零。
4. `center.strength(strength)`: 如果指定了 strength,则设置中心力的强度。例如,在交互式图形中,当新节点进入或退出时,降低强度(例如0.05)可以软化节点的移动。
中心力的主要作用是将节点群体保持在视口的中心位置,而不影响它们之间的相对位置。这在以下情况下特别有用:
1. 当图形初始化时,节点可能分布在画布的任意位置。中心力可以快速将它们拉向中心,形成一个紧凑的初始布局。
2. 在交互式图形中,当添加或删除节点时,中心力可以平滑地调整布局,使其始终保持在视口中心。
3. 对于某些类型的数据(如星型网络),中心力可以突出中心节点,同时将其他节点围绕在周围。
需要注意的是,中心力的强度应该根据具体情况来调整。如果强度太高,节点可能会过于密集地聚集在中心;如果强度太低,中心的吸引效果可能不明显。通常建议从一个较小的值(如0.1)开始,然后根据实际效果进行调整。
碰撞力-半径的圆
碰撞力将节点视为具有给定半径的圆,而不是点,并防止节点重叠。更正式地说,两个节点 a 和 b 被分开,使得 a 和 b 之间的距离至少为 radius(a) + radius(b)。为了减少抖动,默认情况下这是一个”软”约束,具有可配置的强度和迭代次数。
文档介绍了碰撞力的几个方法:
1. `forceCollide(radius)`: 使用指定的半径创建一个新的圆碰撞力。如果未指定半径,则默认为所有节点的常量1。
const collide = d3.forceCollide((d) => d.r);
2. `collide.radius(radius)`: 如果指定了 radius,则将半径访问器设置为指定的数字或函数,重新评估每个节点的半径访问器,并返回该力。如果未指定 radius,则返回当前的半径访问器,默认为:
function radius() {
return 1;
}
半径访问器为模拟中的每个节点调用,传递节点及其从零开始的索引。然后将结果数字存储在内部,这样每个节点的半径只在力初始化时或使用新的 radius 调用此方法时重新计算,而不是在每次应用力时重新计算。
3. `collide.strength(strength)`: 如果指定了 strength,则将力的强度设置为 [0,1] 范围内的指定数字,并返回该力。如果未指定 strength,则返回当前的强度,默认为1。
重叠的节点通过迭代松弛来解决。对于每个节点,确定预计在下一个 tick 时会重叠的其他节点(使用预期位置 ⟨x + vx, y + vy⟩);然后修改节点的速度,将节点推出每个重叠的节点。速度的变化由力的强度阻尼,这样simultaneous overlaps的解决方案可以混合在一起以找到稳定的解决方案。
4. `collide.iterations(iterations)`: 如果指定了 iterations,则将每个应用程序的迭代次数设置为指定的数字,并返回该力。如果未指定 iterations,则返回当前的迭代次数,默认为1。增加迭代次数会大大增加约束的刚性并避免节点的部分重叠,但也会增加评估力的运行时成本。
碰撞力在防止节点重叠方面非常有用,尤其是在节点具有不同大小时。通过调整节点的半径、力的强度和迭代次数,你可以在不同的场景下找到合适的平衡,既能有效地分离节点,又不会过度影响布局的整体结构。
以下是一些使用碰撞力的技巧:
1. 将节点的半径设置为与其大小相关的值,例如根据节点的度数或权重来确定半径。这可以防止大节点被小节点覆盖。
2. 在交互式图形中,可以根据缩放级别动态调整碰撞力的半径和强度,以适应不同的视觉效果。
3. 对于高度互连的图形,增加迭代次数可以更好地解决复杂的节点重叠,但要注意性能开销。
4. 如果图形非常稀疏,可以降低碰撞力的强度或完全禁用它,以允许一定程度的节点重叠,从而获得更紧凑的布局。
总的来说,碰撞力是 D3 力导向图的重要组成部分,它提供了一种灵活而有效的方式来处理节点重叠问题。通过适当地配置碰撞力的参数,并将其与其他力组合使用,你可以创建出美观、清晰、易于理解的节点链接图。
链接力- 根据期望的链接距离将有链接的节点推到一起或分开
链接力根据期望的链接距离将有链接的节点推到一起或分开。力的强度与链接节点之间的距离与目标距离之差成正比,类似于弹簧力。
文档介绍了链接力的几个方法:
1. `forceLink(links)`:使用指定的链接和默认参数创建一个新的链接力。如果未指定 `links`,则默认为空数组。
警告:此函数是不纯的,它可能会改变传入的链接。参见 `link.links`。
“`js
const link = d3.forceLink(links).id((d) => d.id);
“`
2. `link.links(links)`:如果指定了 `links`,则设置与该力相关联的链接数组,重新计算每个链接的距离和强度参数,并返回该力。如果未指定 `links`,则返回当前的链接数组,默认为空数组。
每个链接都是具有以下属性的对象:
– `source` – 链接的源节点;参见 `simulation.nodes`
– `target` – 链接的目标节点;参见 `simulation.nodes`
– `index` – 由该方法分配的、在 `links` 中的从零开始的索引
为方便起见,链接的 `source` 和 `target` 属性可以使用数字或字符串标识符而不是对象引用来初始化;参见 `link.id`。
警告:此函数是不纯的,当链接力被初始化(或重新初始化,如当节点或链接改变时)时,它可能会改变传入的链接。任何不是对象的 `link.source` 或 `link.target` 属性都会被替换为对具有给定标识符的相应节点的对象引用。
如果指定的链接数组被修改,例如当链接被添加到模拟或从模拟中移除时,必须再次调用此方法并传入新的(或改变的)数组,以通知力有关更改;力不会制作指定数组的防御性副本。
3. `link.id(id)`:如果指定了 `id`,则将节点 id 访问器设置为指定的函数并返回该力。如果未指定 `id`,则返回当前的节点 id 访问器,默认为数字 `node.index`:
“`js
function id(d) {
return d.index;
}
“`
默认的 id 访问器允许将每个链接的 `source` 和 `target` 指定为节点数组中的从零开始的索引。例如:
“`js
const nodes = [
{“id”: “Alice”},
{“id”: “Bob”},
{“id”: “Carol”}
];
const links = [
{“source”: 0, “target”: 1}, // Alice → Bob
{“source”: 1, “target”: 2} // Bob → Carol
];
“`
现在考虑一个返回字符串的不同 id 访问器:
“`js
function id(d) {
return d.id;
}
“`
使用此访问器,您可以使用命名的源和目标:
“`js
const nodes = [
{“id”: “Alice”},
{“id”: “Bob”},
{“id”: “Carol”}
];
const links = [
{“source”: “Alice”, “target”: “Bob”},
{“source”: “Bob”, “target”: “Carol”}
];
“`
在 JSON 中表示图形时,这特别有用,因为 JSON 不允许引用。参见此示例。
每当力被初始化时,例如当节点或链接改变时,都会为每个节点调用 id 访问器,传递节点及其从零开始的索引。
4. `link.distance(distance)`:如果指定了 `distance`,则将距离访问器设置为指定的数字或函数,重新评估每个链接的距离访问器,并返回该力。如果未指定 `distance`,则返回当前的距离访问器,默认为:
“`js
function distance() {
return 30;
}
“`
为每个链接调用距离访问器,传递链接及其从零开始的索引。然后将结果数字存储在内部,这样每个链接的距离仅在力被初始化或使用新距离调用此方法时重新计算,而不是在每次应用力时重新计算。
5. `link.strength(strength)`:如果指定了 `strength`,则将强度访问器设置为指定的数字或函数,重新评估每个链接的强度访问器,并返回该力。如果未指定 `strength`,则返回当前的强度访问器,默认为:
“`js
function strength(link) {
return 1 / Math.min(count(link.source), count(link.target));
}
“`
其中 `count(node)` 是一个函数,返回以给定节点作为源或目标的链接数。之所以选择此默认值,是因为它自动降低了连接到大量连接节点的链接的强度,提高了稳定性。
为每个链接调用强度访问器,传递链接及其从零开始的索引。然后将结果数字存储在内部,这样每个链接的强度仅在力被初始化或使用新强度调用此方法时重新计算,而不是在每次应用力时重新计算。
6. `link.iterations(iterations)`:如果指定了 `iterations`,则将每个应用程序的迭代次数设置为指定的数字,并返回该力。如果未指定 `iterations`,则返回当前的迭代计数,默认为 1。增加迭代次数会大大增加约束的刚性,对于诸如格子之类的复杂结构很有用,但也会增加评估力的运行时成本。
总的来说,链接力提供了一种灵活而强大的方式来模拟节点之间的连接关系,通过调整链接距离、强度和迭代次数等参数,你可以根据具体的数据特点和布局要求,生成具有不同紧密程度和稳定性的网络结构。合理地使用链接力,可以帮助你更好地展现节点之间的关联模式和层次组织。
多体力
多体力是一种全局力,在所有节点之间相互作用,而不同于只影响相连节点的链接力。
多体力可以在强度为正值时模拟引力,在强度为负值时模拟静电斥力。它使用 Barnes-Hut 近似算法和四叉树数据结构来提高性能,可以通过 theta 参数来调整精度。
文档描述了多体力的几个方法:
1. `forceManyBody()`: 使用默认参数创建一个新的多体力。
2. `manyBody.strength(strength)`: 设置力的强度访问器为指定的数字或函数,对每个节点重新评估强度,并返回该力。正值导致吸引(如引力),负值导致排斥(如静电力)。如果没有指定,则返回当前的强度访问器,默认为一个返回-30的函数。
强度访问器为每个节点调用,传递节点和它的索引。结果数字被内部存储,只在力初始化或用新的强度调用此方法时重新计算。
3. `manyBody.theta(theta)`: 设置 Barnes-Hut 近似准则为指定的数字并返回该力。如果没有指定,则返回当前值,默认为0.9。
Barnes-Hut 近似用于加速计算,对于 n 个节点,每次应用需要 O(n log n) 的时间复杂度。一个四叉树存储当前节点位置,然后对每个节点,计算所有其他节点的合力。对于一个距离较远的节点簇,可以将其视为一个更大的单一节点来近似计算力。theta 参数决定了精度:如果四叉树单元宽度与节点到单元质心距离之比小于 theta,则将单元中所有节点视为一个节点。
4. `manyBody.distanceMin(distance)`: 设置被考虑的节点之间的最小距离。如果没有指定,则返回当前最小距离,默认为1。最小距离在附近节点之间建立了力强度的上限,以避免不稳定性,特别是当节点重合时。
5. `manyBody.distanceMax(distance)`: 设置被考虑的节点之间的最大距离。如果没有指定,则返回当前最大距离,默认为无穷大。有限的最大距离可以提高性能并产生更局部化的布局。
总之,多体力是模拟节点之间全局相互作用(如引力或静电力)的强大工具。通过调整力强度、theta、最小和最大距离等参数,你可以控制力的行为和性能,以满足特定的图形需求。合理地使用多体力,可以帮助生成具有吸引力、灵感来源于物理学的复杂网络数据布局。
位置力
这部分文档介绍了 D3 力导向图中的位置力(Position Forces),包括 x 轴位置力、y 轴位置力和径向位置力。这些力可以将节点推向给定维度上的期望位置,力的强度可配置。径向力类似,但它将节点推向给定圆上的最近点。力的强度与节点位置与目标位置之间的一维距离成正比。虽然这些力可以用来定位单个节点,但主要用于对所有(或大多数)节点施加的全局力。
1. `forceX(x)`: 创建一个沿 x 轴方向、指向给定位置 x 的新位置力。如果没有指定 x,则默认为0。
2. `x.strength(strength)`: 如果指定了 strength,则将强度访问器设置为指定的数字或函数,对每个节点重新评估强度访问器,并返回该力。强度决定了节点 x 速度的增量:(x – node.x) × strength。例如,值为0.1表示每次应用时,节点应该从当前 x 位置移动十分之一的距离到目标 x 位置。更高的值会更快地将节点移动到目标位置,但可能以牺牲其他力或约束为代价。不建议使用范围 [0,1] 之外的值。
如果没有指定 strength,则返回当前的强度访问器,默认为一个返回0.1的函数。
强度访问器为模拟中的每个节点调用,传递节点及其从零开始的索引。结果数字存储在内部,这样每个节点的强度只在初始化力或使用新的强度调用此方法时重新计算,而不是在每次应用力时重新计算。
3. `x.x(x)`: 如果指定了 x,则将 x 坐标访问器设置为指定的数字或函数,对每个节点重新评估 x 访问器,并返回该力。如果没有指定 x,则返回当前的 x 访问器,默认为一个返回0的函数。
x 访问器为模拟中的每个节点调用,传递节点及其从零开始的索引。结果数字存储在内部,这样每个节点的目标 x 坐标只在初始化力或使用新的 x 调用此方法时重新计算,而不是在每次应用力时重新计算。
4. `forceY(y)`: 创建一个沿 y 轴方向、指向给定位置 y 的新位置力。如果没有指定 y,则默认为0。
5. `y.strength(strength)`: 与 `x.strength` 类似,但作用于 y 方向。
6. `y.y(y)`: 与 `x.x` 类似,但访问 y 坐标。
7. `forceRadial(radius, x, y)`: 创建一个指向以 ⟨x,y⟩ 为中心、指定半径的圆的新位置力。如果没有指定 x 和 y,则默认为 ⟨0,0⟩。
8. `radial.strength(strength)`: 与 `x.strength` 类似,但同时作用于 x 和 y 方向,将节点推向圆上最近的点。
9. `radial.radius(radius)`: 如果指定了 radius,则将圆半径设置为指定的数字或函数,对每个节点重新评估半径访问器,并返回该力。如果没有指定 radius,则返回当前的半径访问器。
半径访问器为模拟中的每个节点调用,传递节点及其从零开始的索引。结果数字存储在内部,这样每个节点的目标半径只在初始化力或使用新的半径调用此方法时重新计算,而不是在每次应用力时重新计算。
10. `radial.x(x)`: 如果指定了 x,则将圆心的 x 坐标设置为指定的数字,并返回该力。如果没有指定 x,则返回当前圆心的 x 坐标,默认为0。
11. `radial.y(y)`: 如果指定了 y,则将圆心的 y 坐标设置为指定的数字,并返回该力。如果没有指定 y,则返回当前圆心的 y 坐标,默认为0。
总的来说,位置力提供了一种灵活的方式来控制节点在布局中的位置。通过设置 x、y 或径向目标位置,并调整相应的强度,你可以创建各种不同类型的布局,如围绕中心点的环形布局,或者将节点限制在特定的区域内。
合理地使用位置力,并将其与其他力(如链接力、多体力等)组合,可以创建出既美观又富有意义的节点链接图。同时,在使用位置力时,要注意不要过度约束节点,以免损害布局的整体灵活性和表现力。
力导向与SVG
SVG(Scalable Vector Graphics)是一种用于描述二维矢量图形的XML标记语言,它可以通过浏览器渲染为图像或动画。在D3中,SVG是默认的图形渲染引擎,因此力导向布局与SVG密切相关。
具体来说,D3力导向布局可以通过SVG元素来创建和呈现节点和连线。在创建力导向布局时,可以使用d3.forceSimulation()
函数来创建一个力模拟器,并将节点和连线的位置属性绑定到SVG元素上,例如:
const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id)) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(width / 2, height / 2)); const link = svg.append("g") .selectAll("line") .data(links) .enter().append("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .attr("stroke-width", d => Math.sqrt(d.value)); const node = svg.append("g") .selectAll("circle") .data(nodes) .enter().append("circle") .attr("r", 5) .attr("fill", d => color(d.group)) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended));
在上面的代码中,d3.forceSimulation()
函数创建了一个力模拟器,并使用forceLink()
、forceManyBody()
和forceCenter()
来定义节点之间的连线、节点的斥力和图形的中心位置。然后,使用SVG的line
元素来呈现连线,使用circle
元素来呈现节点,并将节点和连线的位置属性绑定到SVG元素上。同时,使用D3的drag()
方法为节点添加拖动交互。
通过以上代码,我们可以将D3力导向布局中的节点和连线呈现为一个SVG图形,并且可以使用D3的数据绑定和选择方法来更新图形的位置和属性,从而实现交互式和动态的数据可视化效果。
其他力导向布局库
在JavaScript中,有许多力导向布局的库可供选择。以下是一些比较流行的库:
- D3.js:D3是一个JavaScript库,它提供了各种数据可视化功能,包括力导向布局。D3的力导向布局功能非常强大,它可以创建各种类型的动态可视化图形,例如网络图、关系图等,并且具有灵活的配置选项和丰富的交互性功能。
- Sigma.js:Sigma是一个基于WebGL的图形可视化库,它提供了各种类型的图形布局算法,包括力导向布局。Sigma的力导向布局算法快速且适合大型数据集,而且可以与其他布局算法和插件结合使用。
- Cytoscape.js:Cytoscape是一个基于WebGL的图形可视化库,它专注于生物信息学和网络科学领域,提供了各种类型的图形布局算法,包括力导向布局。Cytoscape的力导向布局算法支持各种常见的布局参数和约束,例如节点间距、重力、斥力等。
- Vis.js:Vis是一个JavaScript可视化库,提供了各种类型的图形布局算法,包括力导向布局。Vis的力导向布局算法可以实现各种类型的动态可视化效果,并支持多种数据格式和交互式操作。
这些库各有特点和优劣,具体选择取决于项目需求和个人偏好。如果需要灵活的数据绑定和定制化配置,D3.js是一个不错的选择。如果需要快速绘制大型网络图,并希望使用WebGL技术提高性能,可以考虑使用Sigma.js或Cytoscape.js。如果需要简单易用的API和多种数据格式的支持,可以考虑使用Vis.js。
D3 V7
在 D3 v7 版本中,d3.event
已经被移除了,可以通过将 d3-selection
模块作为依赖项引入,并使用 d3.pointer(event)
来代替 d3.mouse()
和 d3.touches()
,使用 event.sourceEvent
来代替 d3.event
。
例如,可以像下面这样更改 dragstarted
和 dragged
函数:
function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }
然后在代码中引入 d3-selection
模块:
<script src="https://d3js.org/d3-selection.v2.min.js"></script>