如何可视化 d3 力导向图布局中的节点组
How to visualize groups of nodes in a d3 force-directed graph layout
我正在寻找一种方法来将组插入到我的力导向图形可视化中。到目前为止,我已经找到了三个相关示例:
Cola.js 这将需要添加另一个库,并且可能需要改造我的代码以适应这个不同的库。
This block,很难理清。
This slide 来自 mbostock 的幻灯片,这不是我想要的,但在正确的道路上......
我最喜欢的是一种添加非常接近第一个结构的东西的简单方法 link,但没有太多开销。
现在我有一个非常标准的设置:
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style(...
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
我原本希望从 cola.js 中获取 d3 代码并对其进行处理,但该库似乎相当复杂,因此不会太容易。我希望在直接 d3 中获得类似这样的东西并不难:
谢谢!
我比建议的图片更关注标题 "visualize groups of nodes",但我认为调整我的答案以显示图像中的边界框并不难
可能有一些仅限 d3 的解决方案,几乎所有这些解决方案几乎都需要手动调整节点位置以保持节点正确分组。最终结果不会是 force-layout 的典型结果,因为 links 和节点位置必须被操纵以显示除了连接之外的分组 - 因此,最终结果将是每个力量之间的妥协 -节点电荷、长度强度和长度以及组。
实现目标的最简单方法可能是:
- 削弱link强度当links link不同组
- 在每个刻度上,计算每个组的质心
- 调整每个节点的位置使其更接近组的质心
- 使用 voronoi 图显示分组
对于此处的示例,我将使用 Mike 的规范 force layout。
削弱links当linkslink不同组
使用 linked 示例,当 link 目标和 link 源具有不同的组时,我们可以抑制 link 强度。指定的强度可能需要根据部队布局的性质进行更改 - 更多 inter-connected 组可能需要具有较弱的组间 link 强度。
要根据我们是否有组间 link 来更改 link 强度,我们可以使用:
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 图
这允许最简单的节点分组 - 它确保组壳之间没有重叠。我没有内置任何验证来确保一个节点或一组节点不会与其组的其余部分隔离 - 根据可视化的复杂性,您可能需要这样做。
我最初的想法是使用隐藏的 canvas 来计算壳是否重叠,但是如果使用 Voronoi,您可能可以计算每个组是否使用相邻单元合并。在 non-consolidated 个组的情况下,您可以对杂散节点 使用更强的强制。
应用 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);
结果
总的来说,这在分组阶段看起来像这样:
当可视化最终停止时:
如果第一个图像更可取,则移除随着 alpha 冷却 minDistance
发生变化的部分。
这里a block使用上述方法。
进一步修改
我们可以使用另一个力图来定位每个组的理想质心,而不是使用每个组节点的质心。该力图每个组都有一个节点,每个组之间 links 的强度将对应于组节点之间 links 的数量。使用此力图,我们可以将原始节点强制朝向我们理想化的质心 - 第二力布局的节点。
这种方法在某些情况下可能具有优势,例如通过更大的数量将组分开。这种方法可能会给你类似的东西:
我在这里包含了一个例子,但希望代码有足够的注释以便理解,而不会像上面的代码那样分解。
voronoi 很简单,但并不总是最美观的,您可以使用裁剪路径将多边形裁剪成某种椭圆形,或者使用渐变叠加在多边形到达边缘时淡出。根据图的复杂性,一种可能的选择是使用最小凸多边形,尽管这不适用于节点少于三个的组。边界框在大多数情况下可能不起作用,除非你真的保持强制 fac或高(例如:始终保持 minDistance
非常低)。权衡总是你想展示更多:连接或分组。
我正在寻找一种方法来将组插入到我的力导向图形可视化中。到目前为止,我已经找到了三个相关示例:
Cola.js 这将需要添加另一个库,并且可能需要改造我的代码以适应这个不同的库。
This block,很难理清。
This slide 来自 mbostock 的幻灯片,这不是我想要的,但在正确的道路上......
我最喜欢的是一种添加非常接近第一个结构的东西的简单方法 link,但没有太多开销。
现在我有一个非常标准的设置:
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style(...
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
我原本希望从 cola.js 中获取 d3 代码并对其进行处理,但该库似乎相当复杂,因此不会太容易。我希望在直接 d3 中获得类似这样的东西并不难:
谢谢!
我比建议的图片更关注标题 "visualize groups of nodes",但我认为调整我的答案以显示图像中的边界框并不难
可能有一些仅限 d3 的解决方案,几乎所有这些解决方案几乎都需要手动调整节点位置以保持节点正确分组。最终结果不会是 force-layout 的典型结果,因为 links 和节点位置必须被操纵以显示除了连接之外的分组 - 因此,最终结果将是每个力量之间的妥协 -节点电荷、长度强度和长度以及组。
实现目标的最简单方法可能是:
- 削弱link强度当links link不同组
- 在每个刻度上,计算每个组的质心
- 调整每个节点的位置使其更接近组的质心
- 使用 voronoi 图显示分组
对于此处的示例,我将使用 Mike 的规范 force layout。
削弱links当linkslink不同组
使用 linked 示例,当 link 目标和 link 源具有不同的组时,我们可以抑制 link 强度。指定的强度可能需要根据部队布局的性质进行更改 - 更多 inter-connected 组可能需要具有较弱的组间 link 强度。
要根据我们是否有组间 link 来更改 link 强度,我们可以使用:
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 图
这允许最简单的节点分组 - 它确保组壳之间没有重叠。我没有内置任何验证来确保一个节点或一组节点不会与其组的其余部分隔离 - 根据可视化的复杂性,您可能需要这样做。
我最初的想法是使用隐藏的 canvas 来计算壳是否重叠,但是如果使用 Voronoi,您可能可以计算每个组是否使用相邻单元合并。在 non-consolidated 个组的情况下,您可以对杂散节点 使用更强的强制。
应用 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);
结果
总的来说,这在分组阶段看起来像这样:
当可视化最终停止时:
如果第一个图像更可取,则移除随着 alpha 冷却 minDistance
发生变化的部分。
这里a block使用上述方法。
进一步修改
我们可以使用另一个力图来定位每个组的理想质心,而不是使用每个组节点的质心。该力图每个组都有一个节点,每个组之间 links 的强度将对应于组节点之间 links 的数量。使用此力图,我们可以将原始节点强制朝向我们理想化的质心 - 第二力布局的节点。
这种方法在某些情况下可能具有优势,例如通过更大的数量将组分开。这种方法可能会给你类似的东西:
我在这里包含了一个例子,但希望代码有足够的注释以便理解,而不会像上面的代码那样分解。
voronoi 很简单,但并不总是最美观的,您可以使用裁剪路径将多边形裁剪成某种椭圆形,或者使用渐变叠加在多边形到达边缘时淡出。根据图的复杂性,一种可能的选择是使用最小凸多边形,尽管这不适用于节点少于三个的组。边界框在大多数情况下可能不起作用,除非你真的保持强制 fac或高(例如:始终保持 minDistance
非常低)。权衡总是你想展示更多:连接或分组。