如何在d3力导向图布局中可视化节点组。

8
我正在寻找一种方法将组插入我的力导向图可视化中。到目前为止,我已经找到了三个相关的示例:

最好的情况是能够以第一个链接结构非常接近的方式添加组,但不会有太多开销。

现在,我有一个相当标准的设置:

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中能够轻松地实现类似于这样的效果:

enter image description here

谢谢!


如果您正在寻找非复杂形状(边界框或凸包),则可以通过计算每个组的形状相对容易地实现 - 但重叠是可能的。有一些凸包示例可用于力导向布局:https://dev59.com/sqTja4cB1Zd3GeqPF7Aa - Andrew Reid
谢谢您回复我!我正在寻找一些不重叠的东西。您认为是否有这样的区块存在? - Alex Lenail
我不能说我见过这样的情况 - 检测重叠需要增加相当多的复杂性,然后您需要一些方法在检测到重叠时推动力布局以分离节点。这将需要稍微降低力度 - 在力度刚初始化时尝试分离节点是没有意义的。 - Andrew Reid
@AndrewReid 我想在d3中实现这个是可能的,因为至少已经有人做过了。我已经开始设置赏金,以防有人认为他们可以解决它! - Alex Lenail
我认为这并不是不可能的,但具体取决于所需结果的复杂程度。如果我的答案与预期略有不同,希望它仍然有用。不过,如果愿意将节点的位置偏向于群组而非链接,则可以使用边界框而非泰森多边形图。 - Andrew Reid
1个回答

19

我更注重标题“可视化节点组”,而不是建议的图片,但我认为调整我的答案以显示边界框不会那么难。

可能有几种仅使用d3的解决方案,所有这些解决方案几乎肯定需要手动调整节点位置以保持节点正确分组。最终结果不严格符合力布局,因为必须操纵链接和节点位置以显示分组以及连接性 - 因此,最终结果将是每个力(节点电荷、长度强度和长度以及群组)之间的妥协。

实现您的目标最简单的方法可能是:

  1. 当链接不同组时,降低链接强度
  2. 在每个tick中,计算每个组的质心
  3. 调整每个节点的位置,使其更靠近组的质心
  4. 使用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; // 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;
    }
})

使用泰森多边形图

这可使节点最容易分组-确保组壳之间没有重叠。我没有内置任何验证来确保一个节点或一组节点不与其余组隔离-根据可视化的复杂性,您可能需要此功能。

我的初步想法是使用隐藏画布计算外壳是否重叠,但使用泰森多边形图,您可以通过相邻单元格计算每个组是否合并。在出现非合并组的情况下,您可以对游离节点进行更强的强制操作

应用泰森多边形图相当简单:

  // 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()))

每个tick更新:

// update voronoi:
cell = cell.data(voronoi.polygons(simulation.nodes())).attr("d", renderCell);

结果

总的来说,在分组阶段,它看起来像这样:

enter image description here

当可视化最终停止:

enter image description here

如果第一张图片更好,那么请删除改变minDistance的部分,因为alpha冷却下降。
以下是使用上述方法的一个块
进一步修改
我们可以使用另一个力导向图来定位每个组的理想质心,而不是使用每个组节点的质心。这个力导向图将为每个组设置一个节点,每个组之间链接的强度将对应于组节点之间的链接数。使用这个力导向图,我们可以迫使原始节点朝向我们理想化的质心 - 第二个力导向布局的节点。
在某些情况下,这种方法可能具有优势,例如通过更大的距离分离组。这种方法可能会给你类似于:

enter image description here

我在这里提供了一个示例,但希望代码有足够的注释,以便不需要像上面的代码一样进行分解就可以理解。

第二个示例的块。

沃罗诺伊很容易,但不总是最美观的,您可以使用剪辑路径将多边形剪切到某种椭圆形,或者使用渐变叠加使多边形在达到边缘时淡出。一个可能可行的选项取决于图形复杂性,即使用最小凸多边形,但这对于少于三个节点的组将无法正常工作。在大多数情况下,边界框可能不起作用,除非您真的保持强制因素高(例如:整个时间保持minDistance非常低)。权衡将永远是什么更重要:连接还是分组。


顺便说一下,第二个代码块是机密的 -- 不确定这是否是有意的。 - Alex Lenail
哦,还有一个问题:如果每个组都有一个名称,并且您想在Voronoi的某个地方显示它们,该怎么办? - Alex Lenail
1
每个组只有一个质心,您应该能够获取质心,根据质心更新文本位置,并且由于它们具有标识符(将节点分组),因此您应该能够显示标识符或使用标识符查找名称。这里是一个快速演示(如果我有时间,我会写一篇文章并使它们更加公开)。此外,您应该能够获得沃罗诺伊的路径质心,但是这种放置可能有点奇怪:节点的质心和沃罗诺伊单元的质心不会重叠。 - Andrew Reid
你认为在第二个示例中,是什么原因导致节点在模拟合并时“振动”?此外,为什么要使用 var cells = g.selectAll()?你在选择什么? - Alex Lenail
g.selectAll() 和 g.selectAll(null) 是一样的 - 它给我一个空选择,其中 .data() 中的所有内容都将被输入。我对振动感到有些困惑,我不太确定,可能只是在拉点到质心或让它们保持原样之间的硬边界 - 渐变可能更好。或者,您可以运行模拟并在之后绘制它。 - Andrew Reid
我向你发起挑战,尝试让非重叠边界框方法运作起来(就像问题中可乐示例图像一样)。我的图表比典型示例要复杂一些,因此Voronoi图不太美观,因为它有许多不连续的区域(虽然我想我可以将其设置小于0.1)。我认为边界框方法可能更加优雅。想试试吗? - Alex Lenail

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接