D3 Force Layout where larger nodes cluster in center

痞子三分冷 提交于 2019-12-12 03:18:16

问题


I've been tinkering with a force layout that will be used for a tag cloud and each tag is represented by a <circle> whose radius is proportional to the questions with that tag. My question is to determine how to make more popular tags trend toward the center of the cluster and less popular tags congregate around the edges of the tag cloud. So far my code looks like this:

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;
      });
    };
  }
}

To give you an idea of the amount of data being dealt with, there are 52 questions and 42 tags currently. Also the output usually ends up something like this:

I'd like the larger nodes to end up in the center.


回答1:


Another possibility is to give the nodes something like mass and take it into account in the collision function. Then you can switch on gravity and let them fight for position.
This example gets there after a bit of a flurry.

Here is the modified collision function...

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;
      });
    };
  }
}

Force Directed Graph with self sorting nodes - Position swapping

Features

  • Accelerated annealing
    The annealing calc is done every tick but, until alpha drops below 0.05, the viz is only updated every nth tick (n is currently 4). This delivers significant reductions in the time to reach equilibrium (roughly a factor of 2).
  • Force dynamics
    The force dynamics are a function of alpha, with two phases. The initial phase has zero charge, low gravity and low damping. This is designed to maximise mixing and sorting. The second phase has a higher gravity and a large, negative charge and much higher damping, this is designed to clean up and stabilise the presentation of the nodes.
  • Collisions between nodes
    Based on this example but enhanced to sort the radial position of the nodes based on size, with larger nodes closer to the center. Every collision is used as an opportunity to correct the relative positions. If they are out of position then the radial ordinates of the colliding nodes (in polar coordinates) are swapped. The sorting efficiency is therefore reliant on good mixing in the collisions. In order to maximise the mixing, the nodes are all created at the same point in the center of the graph. When the nodes are swapped, their velocities are preserved. This is done by also changing the previous points (p.px and p.py). The mass is calculated assuming the nodes are spheres, using r3, and the rebounds calculated according to relative "mass".

Extract

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 plus momentum : Position swapping + momentum

This is a little bit faster but also more organic looking...

Additional features

  • Collisions sort events
    When the nodes are swapped, the velocity of the bigger node is preserved while the smaller node is accelerated. Thus, the sorting efficiency is enhanced because the smaller nodes are flung out from the collision point. The mass is calculated assuming the nodes are spheres, using r3, and the rebounds calculated according to relative "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;
                });
            };
        }
    }
    



回答2:


Here's what I added to make it work:

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;
  };
}


来源:https://stackoverflow.com/questions/32274775/d3-force-layout-where-larger-nodes-cluster-in-center

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!