Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3: Using force layout for word clouds

I'm working on a tag visualization where tags transition between different force-directed layouts.

I had few issues figuring out how to transition from a bubble chart to a node chart, but I'm a bit stuck as to how to get the charts to transition into a word cloud. My difficulties largely stem from my inexperience at writing custom clustering/collision detection functions.

I declare the forces as globals and then stop and start them when the user clicks a button:

var force1 = d3.layout.force()
    .size([width, height])
    .charge(0)
    .gravity(0.02)
    .on("tick", ticka);

//layout for node chart
var force2 = d3.layout.force()
    .size([width, height])
    .charge(-50)
    .gravity(0.005)
    .linkDistance(120)
    .on("tick", tickb);

//layout for bubble chart
var force3 = d3.layout.force()
    .size([width, height])
    .charge(0)
    .gravity(0.02)
    .on("tick", tickc);

Relevant node/link functions are added to the force when the function that draws the nodes is called (as data changes according to a slider value).

The code for creating node data is as follows:

nodes = splicedCounts.map(function(d, e) {
    var choice;
    var i = 0,
    r = d[1],
    d = { count: d[1],
          sentiment: d[2]/d[1],
          cluster: i,
          radius: radScale(r),
          name: d[0],
          index: e,
          x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
          y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
    };
    if (!clusters[i] || (r > clusters[i].radius))
        clusters[i] = d;
    return d;
});

In order to keep this question relatively brief, the code I use for drawing the bubble chart is derivative of this example: http://bl.ocks.org/mbostock/7881887 and the code for drawing the node chart are similarly generic (I am happy to provide this code if it would help to solve my issue).

This is where my issue comes in:

I found this nice example for collision detection between rectangles and incorporated it into my code. However, since I'm using SVG text and the font size changes on transition, I opted to estimate the text size/bounding box size based on text-length and radius.

The entire "tick" functions for the word chart are below.

function tickc(e) {
    node = nodeGroup.selectAll(".node");
    var nodeText = nodeGroup.selectAll(".node text");
    node.each(cluster(5 * e.alpha * e.alpha));
    var k = e.alpha;
    nodeText.each(function(a, i) {
        var compWidth = d3.select(this).attr("bWidth");
        var compHeight = d3.select(this).attr("bHeight");
        nodes.slice(i + 1).forEach(function(b) {
          // console.log(a);
          var lineWidthA = a["name"].length * a["radius"]/2.5;
          var lineHeightA = a["radius"]/0.9;

          var lineWidthB = b["name"].length * b["radius"]/2.5;
          var lineHeightB = b["radius"]/0.9;
          dx =  (a.x - b.x)
          dy =  (a.y - b.y)    
          adx = Math.abs(dx)
          ady = Math.abs(dy)
          mdx = (1 + 0.07) * (lineWidthA + lineWidthB)/2
          mdy = (1 + 0.07) * (lineHeightA + lineHeightB)/2
          if (adx < mdx  &&  ady < mdy) {          
            l = Math.sqrt(dx * dx + dy * dy)

            lx = (adx - mdx) / l * k
            ly = (ady - mdy) / l * k

            // choose the direction with less overlap
            if (lx > ly  &&  ly > 0)
                 lx = 0;
            else if (ly > lx  &&  lx > 0)
                 ly = 0;

            dx *= lx
            dy *= ly
            a.x -= dx
            a.y -= dy
            b.x += dx
            b.y += dy
          }
        });
  });
node.select("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
node.select("text")
    .attr("x", function(d) { return d.x; })
    .attr("y", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster2(alpha) {
  return function(d) {
    var cluster = clusters[d.cluster];
    if (cluster === d) return;
    var x = d.x - cluster.x,
    y = d.y - cluster.y,
    l = Math.sqrt(x * x + y * y),
    r = (d["name"].length * d["radius"]) + (cluster["name"].length * cluster["radius"]);

  };
}

I was unsure of how to conclude the clustering function so as to move the nodes appropriately. I tried to adapt the standard cluster function, i.e.

// Move d to be adjacent to the cluster node.
function cluster(alpha) {
  return function(d) {
    var cluster = clusters[d.cluster];
    if (cluster === d) return;
    var x = d.x - cluster.x,
        y = d.y - cluster.y,
        l = Math.sqrt(x * x + y * y),
        r = d.radius + cluster.radius;
    if (l != r) {
      l = (l - r) / l * alpha;
      d.x -= x *= l;
      d.y -= y *= l;
      cluster.x += x;
      cluster.y += y;
    }
  };
} 

to be more similar to the aforementioned rectangular cluster force layout but without luck (I'm afraid I no longer have copies of my exact attempts).

I'm afraid I can't attach images due to my lack of reputation but I can try to find a way to provide them if it would help. The overlap problem with the word cloud is minor (most words resolve into adjacent but not touching positions) but, if possible, I'd like it to resolve as perfectly as the bubble chart. I'm pretty sure that these issues arose from a.) the unfinished cluster function and b.) my hack at using text length and radius to estimate text size rather than proper bounding box coords, but I'm not sure exactly how to fix these things.

like image 588
Jess Avatar asked Aug 14 '14 15:08

Jess


1 Answers

I'd recommend using the d3-cloud package which should do a lot of what you need. If not, then at least it's a good starting point https://github.com/jasondavies/d3-cloud

The way it seems to work is by calculating a bounds for each word and then resolving collisions between those bounds. You can see that here

like image 117
Ian Avatar answered Oct 20 '22 00:10

Ian