我更注重标题“可视化节点组”,而不是建议的图片,但我认为调整我的答案以显示边界框不会那么难。
可能有几种仅使用d3的解决方案,所有这些解决方案几乎肯定需要手动调整节点位置以保持节点正确分组。最终结果不严格符合力布局,因为必须操纵链接和节点位置以显示分组以及连接性 - 因此,最终结果将是每个力(节点电荷、长度强度和长度以及群组)之间的妥协。
实现您的目标最简单的方法可能是:
- 当链接不同组时,降低链接强度
- 在每个tick中,计算每个组的质心
- 调整每个节点的位置,使其更靠近组的质心
- 使用voronoi图显示分组
对于我的示例,我将使用Mike的典型force layout。
当链接不同组时,降低链接强度
使用链接示例,当链接目标和链接源具有不同的组时,我们可以减弱链接强度。指定的强度可能需要根据力布局的性质进行修改 - 更多相互连接的组可能需要具有较弱的组间链接强度。
要根据是否具有组间链接更改链接强度,我们可以使用:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(function(link) {
if (link.source.group == link.source.target) {
return 1;
}
else {
return 0.1;
}
}) )
.force("charge", d3.forceManyBody().strength(-20))
.force("center", d3.forceCenter(width / 2, height / 2));
每个时刻计算群组中心
我们希望将群组节点强制聚在一起,为此,我们需要知道群组的中心点。simulation.nodes()
的数据结构不太适合计算中心点,因此我们需要做一些工作:
var nodes = this.nodes();
var coords ={};
var groups = [];
node.each(function(d) {
if (groups.indexOf(d.group) == -1 ) {
groups.push(d.group);
coords[d.group] = [];
}
coords[d.group].push({x:d.x,y:d.y});
})
var centroids = {};
for (var group in coords) {
var groupNodes = coords[group];
var n = groupNodes.length;
var cx = 0;
var tx = 0;
var cy = 0;
var ty = 0;
groupNodes.forEach(function(d) {
tx += d.x;
ty += d.y;
})
cx = tx/n;
cy = ty/n;
centroids[group] = {x: cx, y: cy}
}
将每个节点的位置调整为更接近其组质心:
我们不需要调整每个节点 - 只需要调整那些离其质心相当远的节点。对于足够远的节点,我们可以使用质心和节点当前位置的加权平均值来微调它们的位置。
我在可视化降温时修改了用于确定是否应该调整节点的最小距离。在可视化活动期间的大部分时间中,即 alpha 值很高时,优先考虑分组,因此大多数节点将被强制朝向分组质心移动。随着 alpha 值接近零,节点已经被分组,强制调整它们的位置的必要性就不那么重要了。
var minDistance = 10;
if (alpha < 0.1) {
minDistance = 10 + (1000 * (0.1-alpha))
}
node.each(function(d) {
var cx = centroids[d.group].x;
var cy = centroids[d.group].y;
var x = d.x;
var y = d.y;
var dx = cx - x;
var dy = cy - y;
var r = Math.sqrt(dx*dx+dy*dy)
if (r>minDistance) {
d.x = x * 0.9 + cx * 0.1;
d.y = y * 0.9 + cy * 0.1;
}
})
使用泰森多边形图
这可使节点最容易分组-确保组壳之间没有重叠。我没有内置任何验证来确保一个节点或一组节点不与其余组隔离-根据可视化的复杂性,您可能需要此功能。
我的初步想法是使用隐藏画布计算外壳是否重叠,但使用泰森多边形图,您可以通过相邻单元格计算每个组是否合并。在出现非合并组的情况下,您可以对游离节点进行更强的强制操作。
应用泰森多边形图相当简单:
var cells = svg.selectAll()
.data(simulation.nodes())
.enter().append("g")
.attr("fill",function(d) { return color(d.group); })
.attr("class",function(d) { return d.group })
var cell = cells.append("path")
.data(voronoi.polygons(simulation.nodes()))
每个tick更新:
cell = cell.data(voronoi.polygons(simulation.nodes())).attr("d", renderCell);
结果
总的来说,在分组阶段,它看起来像这样:
![enter image description here](https://istack.dev59.com/ayBRu.webp)
当可视化最终停止:
![enter image description here](https://istack.dev59.com/ZdfB6.webp)
如果第一张图片更好,那么请删除改变
minDistance
的部分,因为alpha冷却下降。
以下是使用上述方法的
一个块。
进一步修改
我们可以使用另一个力导向图来定位每个组的理想质心,而不是使用每个组节点的质心。这个力导向图将为每个组设置一个节点,每个组之间链接的强度将对应于组节点之间的链接数。使用这个力导向图,我们可以迫使原始节点朝向我们理想化的质心 - 第二个力导向布局的节点。
在某些情况下,这种方法可能具有优势,例如通过更大的距离分离组。这种方法可能会给你类似于:
![enter image description here](https://istack.dev59.com/jioe7.webp)
我在这里提供了一个示例,但希望代码有足够的注释,以便不需要像上面的代码一样进行分解就可以理解。
第二个示例的块。
沃罗诺伊很容易,但不总是最美观的,您可以使用剪辑路径将多边形剪切到某种椭圆形,或者使用渐变叠加使多边形在达到边缘时淡出。一个可能可行的选项取决于图形复杂性,即使用最小凸多边形,但这对于少于三个节点的组将无法正常工作。在大多数情况下,边界框可能不起作用,除非您真的保持强制因素高(例如:整个时间保持minDistance
非常低)。权衡将永远是什么更重要:连接还是分组。