D3 Force Layout, where larger nodes are grouped in the center

I do the syntax that will be used for the 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 the more popular tag trend towards the center of the cluster, and the less popular tags gather 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 dx; }) .attr('cy', function(d) { return dy; }); } function collide(alpha) { var quadtree = d3.geom.quadtree(nodes); return function(d) { var r = d.radius + maxRadius + padding; var nx1 = dx - r; var nx2 = dx + r; var ny1 = dy - r; var ny2 = dy + r; quadtree.visit(function(quad, x1, y1, x2, y2) { if (quad.point && (quad.point !== d)) { var x = dx - quad.point.x; var y = dy - 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; dx -= x *= l; dy -= 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 processed, there are currently 52 questions and 42 tags. Also, the output usually ends like this:

ebe0a3705e.png

I would like the large nodes to be in the center.

0
source share
2 answers

Another possibility is to give the nodes something like mass and take it into account in the collision function. You can then turn on gravity and let them fight for position.
This example gets there after a small flurry.

Here is a 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 = dx - r, nx2 = dx + r, ny1 = dy - r, ny2 = dy + 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 = dx - quad.point.x, y = dy - 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; dx += (x *= l) * m/mT; dy += (y *= l) * m/mT; quad.point.x -= x * mq/mT; quad.point.y -= y * mq/mT; } } return !possible; }); }; } } 

Forced schedule with custom sorting nodes - Position swap

enter image description here

Features

  • Accelerated annealing
    Annealing is released every time, but until alpha drops below 0.05, only every nth tick is updated visas (n is currently 4). This significantly reduces the time to reach equilibrium (about 2 times).
  • Power dynamics
    Power dynamics is a two-phase alpha function. The initial phase has zero charge, low gravity and low attenuation. This is done for maximum mixing and sorting. The second phase has higher gravity and a large negative charge and significantly higher damping, which is designed to clean and stabilize the representation of nodes.
  • Collisions of nodes
    Based on this example, but extended to sort the radial position of nodes based on size, with larger nodes closer to the center. Each collision is used as an opportunity to correct relative positions. If they are out of position, then the radial ordinates of the colliding nodes (in polar coordinates) are interchanged. Therefore, sorting efficiency depends on good mixing in collisions. To maximize mixing, all nodes are created at the same point in the center of the graph. When nodes are swapped, their speeds are maintained. This is done by changing the previous points ( p.px and p.py ). The mass is calculated under the condition that the nodes are spheres using r 3 and the selections are calculated by relative "mass".

Eject

 function Collide(nodes, padding) { // Resolve collisions between nodes. var maxRadius = d3.max(nodes, function(d) { return dqradius }); return function collide(alpha) { var quadtree = d3.geom.quadtree(nodes); return function(d) { var r = d.radius + maxRadius + padding, nx1 = dx - r, nx2 = dx + r, ny1 = dy - r, ny2 = dy + 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 = dx - quad.point.x, y = dy - 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 = dr / quad.point.r, farther = rad > 1; if(bigger && farther || !bigger && !farther) { var d_r = dr; dr = 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 dx += (x *= l); dy += (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; }); }; } } 

Exchange position plus momentum: Exchange position + momentum

It's a little faster, but also more organic looking ...

enter image description here

Additional functions

  • Conflict Sort Events
    When nodes are replaced, the speed of the larger node is maintained, and the smaller node is accelerated. In this way, sorting efficiency is improved as smaller nodes are removed from the collision point. The mass is calculated under the condition that the nodes are spheres using r 3 and the selections are calculated by 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 = dx - r, nx2 = dx + r, ny1 = dy - r, ny2 = dy + 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 = dx - quad.point.x, y = dy - 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 = dr / quad.point.r, farther = rad > 1; if(bigger && farther || !bigger && !farther) { var d_r = dr; dr = 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 dx += (x *= l); dy += (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; }); }; } } 
+1
source

Here 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 dx; }) .attr('cy', function(d) { return dy; }); } function gravity(alpha) { return function(d) { var angle = Math.atan2(y - dy, x - dx); // 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 dx += (rx - dx) * alpha; dy += (ry - dy) * alpha; }; } 
0
source

All Articles