D3 Force Layout,其中较大的节点聚集在中心

D3 Force Layout where larger nodes cluster in center

我一直在修补将用于标签云的强制布局,每个标签都由一个 <circle> 表示,其半径与该标签的问题成正比。我的问题是确定如何使更受欢迎的标签趋向于集群的中心,而不那么流行的标签聚集在标签云的边缘。到目前为止,我的代码如下所示:

function(err, results) {
  var nodes = results.tags;
  var width = 1020;
  var height = 800;
  var extent = d3.extent(nodes, function(tag) { return tag.questions.length; });
  var min = extent[0] || 1;
  var max = extent[1];
  var padding = 2;

  var radius = d3.scale.linear()
    .clamp(true)
    .domain(extent)
    .range([15, max * 5 / min]);

  // attempted to make gravity proportional?
  var gravity = d3.scale.pow().exponent(5)
    .domain(extent)
    .range([0, 200]);

  var minRadius = radius.range()[0];
  var maxRadius = radius.range()[1];

  var svg = d3.select('#question_force_layout').append('svg')
    .attr('width', width)
    .attr('height', height);

  var node = svg.selectAll('circle')
      .data(nodes)
    .enter().append('circle')
      .attr('class', 'tag')
      .attr('r', function(tag) { return (tag.radius = radius(tag.questions.length)); });

  var tagForce = d3.layout.force()
    .nodes(results.tags)
    .size([width, height])
    .charge(200) // seemed like an okay effect
    .gravity(function(tag) {
      return gravity(tag.questions.length);
    })
    .on('tick', tagTick)
    .start();

  function tagTick(e) {
    node
      .each(collide(.5))
      .attr('cx', function(d) { return d.x; })
      .attr('cy', function(d) { return d.y; });
  }

  function collide(alpha) {
    var quadtree = d3.geom.quadtree(nodes);

    return function(d) {
      var r = d.radius + maxRadius + padding;
      var nx1 = d.x - r;
      var nx2 = d.x + r;
      var ny1 = d.y - r;
      var ny2 = d.y + r;

      quadtree.visit(function(quad, x1, y1, x2, y2) {
        if (quad.point && (quad.point !== d)) {
          var x = d.x - quad.point.x;
          var y = d.y - quad.point.y;
          var l = Math.sqrt(x * x + y * y);
          var r = d.radius + quad.point.radius + padding;

          if (l < r) {
            l = (l - r) / l * alpha;
            d.x -= x *= l;
            d.y -= y *= l;
            quad.point.x += x;
            quad.point.y += y;
          }
        }

        return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
      });
    };
  }
}

为了让您了解正在处理的数据量,目前有 52 个问题和 42 个标签。此外,输出通常会像这样结束:

我希望较大的节点最终位于中心。

另一种可能性是给节点一些类似质量的东西,并在碰撞函数中考虑到它。然后你可以开启重力,让他们争夺位置。
This example 匆忙赶到。

这里是修改后的碰撞函数...

function Collide(nodes, padding) {
// Resolve collisions between nodes.
  var maxRadius = d3.max(nodes, function(d) {return d.radius});
  return function collide(alpha) {
    var quadtree = d3.geom.quadtree(nodes);
    return function(d) {
      var r = d.radius + maxRadius + padding,
        nx1 = d.x - r,
        nx2 = d.x + r,
        ny1 = d.y - r,
        ny2 = d.y + r;
      quadtree.visit(function(quad, x1, y1, x2, y2) {
        var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1);
        if (quad.point && (quad.point !== d) && possible) {
          var x = d.x - quad.point.x,
            y = d.y - quad.point.y,
            l = Math.sqrt(x * x + y * y),
            r = d.radius + quad.point.radius + padding,
            m = Math.pow(quad.point.radius, 3),
            mq = Math.pow(d.radius, 3),
            mT = m + mq;
          if (l < r) {
            //move the nodes away from each other along the radial (normal) vector
            //taking relative mass into consideration, the sign is already established
            //in calculating x and y and the nodes are modelled as spheres for calculating mass
            l = (r - l) / l * alpha;
            d.x += (x *= l) * m/mT;
            d.y += (y *= l) * m/mT;
            quad.point.x -= x * mq/mT;
            quad.point.y -= y * mq/mT;
          }
        }
        return !possible;
      });
    };
  }
}

具有自排序节点的强制定向图 - Position swapping

特点

  • 加速退火
    退火计算在每个 tick 进行一次,但是,直到 alpha 降至 0.05 以下,可视化仅在每 n 个 tick 更新一次(n 当前为 4)。这显着减少了达到平衡的时间(大约 2 倍)。
  • 力动力学
    力动力学是 alpha 的函数,具有两个阶段。初始阶段具有零电荷、低重力和低阻尼。这是为了最大限度地混合和分类。第二阶段具有更高的重力和大量的负电荷以及更高的阻尼,旨在清理和稳定节点的呈现。
  • 节点之间的冲突
    基于 this 示例,但增强为根据大小对节点的径向位置进行排序,较大的节点更靠近中心。每次碰撞都被用作纠正相对位置的机会。如果它们不在适当的位置,则交换碰撞节点的径向坐标(极坐标)。因此,分选效率取决于碰撞中的良好混合。为了最大化混合,节点都创建在图形中心的同一点。交换节点时,它们的速度会保持不变。这也是通过更改之前的点(p.pxp.py)来完成的。假设节点是球体,使用r3计算质量,根据相对"mass"计算回弹。

摘录

function Collide(nodes, padding) {
    // Resolve collisions between nodes.
    var maxRadius = d3.max(nodes, function(d) {
        return d.q.radius
    });
    return function collide(alpha) {
        var quadtree = d3.geom.quadtree(nodes);
        return function(d) {
            var r   = d.radius + maxRadius + padding,
                nx1 = d.x - r,
                nx2 = d.x + r,
                ny1 = d.y - r,
                ny2 = d.y + r;
            quadtree.visit(function v(quad, x1, y1, x2, y2) {
                var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1);
                if(quad.point && (quad.point !== d) && possible) {
                    var x = d.x - quad.point.x,
                        y = d.y - quad.point.y,
                        l = Math.sqrt(x * x + y * y),
                        r = d.radius + quad.point.radius + padding;
                    if(l < r) {
                        for(; Math.abs(l) == 0;) {
                            x = Math.round(Math.random() * r);
                            y = Math.round(Math.random() * r);
                            l = Math.sqrt(x * x + y * y);
                        }
                        ;
                        //move the nodes away from each other along the radial (normal) vector
                        //taking relative size into consideration, the sign is already established
                        //in calculating x and y
                        l = (r - l) / l * alpha;

                        // if the nodes are in the wrong radial order for there size, swap radius ordinate
                        var rel = d.radius / quad.point.radius, bigger = (rel > 1),
                            rad = d.r / quad.point.r, farther = rad > 1;
                        if(bigger && farther || !bigger && !farther) {
                            var d_r = d.r;
                            d.r = quad.point.r;
                            quad.point.r = d_r;
                            d_r = d.pr;
                            d.pr = quad.point.pr;
                            quad.point.pr = d_r;
                        }
                        // move nodes apart but preserve their velocity
                        d.x += (x *= l);
                        d.y += (y *= l);
                        d.px += x;
                        d.py += y;
                        quad.point.x -= x;
                        quad.point.y -= y;
                        quad.point.px -= x;
                        quad.point.py -= y;
                    }
                }
                return !possible;
            });
        };
    }
}  

位置交换加动量:Position swapping + momentum

这会快一点,但看起来也更自然...

附加功能

  • 碰撞排序事件
    交换节点时,较大节点的速度保持不变,而较小节点加速。因此,由于较小的节点从碰撞点被抛出,排序效率得到提高。假设节点是球体,使用r3计算质量,根据相对"mass"计算回弹。

    function Collide(nodes, padding) {
        // Resolve collisions between nodes.
        var maxRadius = d3.max(nodes, function(d) {
            return d.radius
        });
        return function collide(alpha) {
            var quadtree = d3.geom.quadtree(nodes), hit = false;
            return function c(d) {
                var r   = d.radius + maxRadius + padding,
                    nx1 = d.x - r,
                    nx2 = d.x + r,
                    ny1 = d.y - r,
                    ny2 = d.y + r;
                quadtree.visit(function v(quad, x1, y1, x2, y2) {
                    var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1);
                    if(quad.point && (quad.point !== d) && possible) {
                        var x  = d.x - quad.point.x,
                            y  = d.y - quad.point.y,
                            l  = (Math.sqrt(x * x + y * y)),
                            r  = (d.radius + quad.point.radius + padding),
                            mq = Math.pow(quad.point.radius, 3),
                            m  = Math.pow(d.radius, 3);
                        if(hit = (l < r)) {
                            for(; Math.abs(l) == 0;) {
                                x = Math.round(Math.random() * r);
                                y = Math.round(Math.random() * r);
                                l = Math.sqrt(x * x + y * y);
                            }
                            //move the nodes away from each other along the radial (normal) vector
                            //taking relative size into consideration, the sign is already established
                            //in calculating x and y
                            l = (r - l) / l * (1 + alpha);
    
                            // if the nodes are in the wrong radial order for there size, swap radius ordinate
                            var rel = m / mq, bigger = rel > 1,
                                rad = d.r / quad.point.r, farther = rad > 1;
                            if(bigger && farther || !bigger && !farther) {
                                var d_r = d.r;
                                d.r = quad.point.r;
                                quad.point.r = d_r;
                                d_r = d.pr;
                                d.pr = quad.point.pr;
                                quad.point.pr = d_r;
                            }
                            // move nodes apart but preserve the velocity of the biggest one
                            // and accelerate the smaller one
                            d.x += (x *= l);
                            d.y += (y *= l);
                            d.px += x * bigger || -alpha;
                            d.py += y * bigger || -alpha;
                            quad.point.x -= x;
                            quad.point.y -= y;
                            quad.point.px -= x * !bigger || -alpha;
                            quad.point.py -= y * !bigger || -alpha;
                        }
                    }
                    return !possible;
                });
            };
        }
    }
    

以下是我为使其正常工作而添加的内容:

var x = width / 2;
var y = height / 2;

var ring = d3.scale.linear()
  .clamp(true)
  .domain([35, 80]) // range of radius
  .range([Math.min(x, y) - 35, 0]);
// smallest radius attracted to edge (35 -> Math.min(x, y) - 35)
// largest radius attracted toward center (80 -> 0)

function tagTick(e) {
  node
    .each(gravity(.1 * e.alpha)) // added this line
    .each(collide(.5))
    .attr('cx', function(d) { return d.x; })
    .attr('cy', function(d) { return d.y; });
}

function gravity(alpha) {
  return function(d) {
    var angle = Math.atan2(y - d.y, x - d.x); // angle from center
    var rad = ring(d.radius); // radius of ring of attraction

    // closest point on ring of attraction
    var rx = x - Math.cos(angle) * rad;
    var ry = y - Math.sin(angle) * rad;

    // move towards point
    d.x += (rx - d.x) * alpha;
    d.y += (ry - d.y) * alpha;
  };
}