我更多地遵循标题“可视化节点组”而不是建议的图片,但我认为调整我的答案以显示如图所示的边界框并不难
可能有一些仅限 d3 的解决方案,几乎所有这些解决方案都几乎肯定需要手动调整节点位置以保持节点正确分组。最终结果不会严格意义上是典型的力布局,因为除了连接性之外,还必须操纵链接和节点位置来显示分组 - 因此,最终结果将是每个力之间的折衷 - 节点电荷、长度强度和长度、和组。
实现目标的最简单方法可能是:
- 当链接链接不同组时削弱链接强度
- 在每个刻度上,计算每个组的质心
- 调整每个节点的位置,使其更靠近组的质心
- 使用 voronoi 图显示分组
对于我的示例,我将使用 Mike 的规范兵力布局.
当链接链接不同组时削弱链接
使用链接的示例,当链接目标和链接源具有不同的组时,我们可以减弱链接强度。指定的强度可能需要根据部队布局的性质进行更改 - 更多相互连接的群体可能需要具有较弱的群体间链接强度。
要根据是否有组间链接来更改链接强度,我们可以使用:
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; // stronger link for links within a group
}
else {
return 0.1; // weaker links for links across groups
}
}) )
.force("charge", d3.forceManyBody().strength(-20))
.force("center", d3.forceCenter(width / 2, height / 2));
在每个刻度上,计算组质心
我们想要将组节点强制在一起,为此我们需要知道组的质心。数据结构simulation.nodes()
不是最适合计算质心的,所以我们需要做一些工作:
var nodes = this.nodes();
var coords ={};
var groups = [];
// sort the nodes into 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});
})
// get the centroid of each group:
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 下降到零时,节点应该已经分组,并且强制它们的位置的需要不再那么重要:
// don't modify points close the the group centroid:
var minDistance = 10;
// modify the min distance as the force cools:
if (alpha < 0.1) {
minDistance = 10 + (1000 * (0.1-alpha))
}
// adjust each point if needed towards group centroid:
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;
}
})
使用 Voronoi 图
这允许最简单的节点分组 - 它确保组壳之间没有重叠。我没有内置任何验证来确保一个节点或一组节点不会与组中的其他节点隔离——具体取决于您可能需要的可视化的复杂性。
我最初的想法是使用隐藏的画布来计算壳是否重叠,但是使用 Voronoi,您可能可以计算每个组是否使用相邻单元格进行合并。如果存在非合并组,您可以对杂散节点使用更强的强制.
应用 voronoi 相当简单:
// append voronoi
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()))
并在每个刻度上更新:
// update voronoi:
cell = cell.data(voronoi.polygons(simulation.nodes())).attr("d", renderCell);
Results
总而言之,在分组阶段看起来是这样的:
当可视化最终停止时:
如果第一张图片更好,则删除更改的部分minDistance
当阿尔法冷却下来时。
Here's a block使用上述方法。
进一步修改
我们可以使用另一个力图来定位每个组的理想质心,而不是使用每个组节点的质心。该力图对于每个组都有一个节点,每个组之间的链接强度将对应于组的节点之间的链接数量。使用这个力图,我们可以将原始节点强制朝向我们理想化的质心 - 第二个力布局的节点。
这种方法在某些情况下可能有优势,例如通过更大的数量来分隔群体。这种方法可能会给你带来类似的结果:
我在这里包含了一个示例,但希望代码有足够的注释以便理解,而不会像上面的代码那样进行细分。
块第二个例子.
voronoi 很简单,但并不总是最美观,您可以使用剪辑路径将多边形剪辑为某种椭圆形,或者使用渐变叠加在多边形到达边缘时将其淡出。根据图的复杂性,一种可能的选择是使用最小凸多边形,尽管这对于少于三个节点的组来说效果不佳。边界框在大多数情况下可能不起作用,除非你真的保持高强制因子(例如:保持minDistance
整个时间都非常低)。权衡始终是您想展示更多内容:连接或分组。