Skip to content Skip to sidebar Skip to footer

D3 Force Layout Where Larger Nodes Cluster In Center

I've been tinkering with a force layout that will be used for a tag cloud and each tag is represented by a whose radius is proportional to the questions with that ta

Solution 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...

functionCollide(nodes, padding) {
// Resolve collisions between nodes.var maxRadius = d3.max(nodes, function(d) {return d.radius});
  returnfunctioncollide(alpha) {
    var quadtree = d3.geom.quadtree(nodes);
    returnfunction(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

enter image description here

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 r, and the rebounds calculated according to relative "mass".

Extract

functionCollide(nodes, padding) {
    // Resolve collisions between nodes.var maxRadius = d3.max(nodes, function(d) {
        return d.q.radius
    });
    returnfunctioncollide(alpha) {
        var quadtree = d3.geom.quadtree(nodes);
        returnfunction(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(functionv(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 ordinatevar 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...

enter image description here

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 r, and the rebounds calculated according to relative "mass".

    functionCollide(nodes, padding) {
        // Resolve collisions between nodes.var maxRadius = d3.max(nodes, function(d) {
            return d.radius
        });
        returnfunctioncollide(alpha) {
            var quadtree = d3.geom.quadtree(nodes), hit = false;
            returnfunctionc(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(functionv(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 ordinatevar 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;
                });
            };
        }
    }
    

Solution 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)functiontagTick(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; });
}

functiongravity(alpha) {
  returnfunction(d) {
    var angle = Math.atan2(y - d.y, x - d.x); // angle from centervar rad = ring(d.radius); // radius of ring of attraction// closest point on ring of attractionvar 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;
  };
}

Post a Comment for "D3 Force Layout Where Larger Nodes Cluster In Center"