0

This is my first question to stack overflow so please bear with me if I make some newbie mistakes. I have searched a lot of questions here and have not found exactly what I'm looking for (in one case I have, but do not know how to implement it). And it seems the only people who have asked similar questions have not received any answers.

I've created a force layout with D3 and things are almost working the way I want them to. Two things that I am having trouble editing for:

1) Keep nodes from overlapping: yes, I have read and re-read Mike Bostock's code for clustered force layouts. I do not know how to implement this into my code without something going terribly wrong! I tried this code from a tutorial, but it fixed my nodes in a corner and splayed the links all over the canvas:

var padding = 1, // separation between circles
  radius=8;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
   return function(d) {
var rb = 2*radius + padding,
    nx1 = d.x - rb,
    nx2 = d.x + rb,
    ny1 = d.y - rb,
    ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
  if (quad.point && (quad.point !== d)) {
    var x = d.x - quad.point.x,
        y = d.y - quad.point.y,
        l = Math.sqrt(x * x + y * y);
      if (l < rb) {
      l = (l - rb) / 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;
});
};
}

You can see the addition to the tick function in my fiddle (linked below) commented out.

2) Wrap my text labels so they fit inside the nodes. Right now they expand to the node's full name over hover but I am going to change that into a tooltip eventually (once I get these kinks worked out I'll figure out a tooltip) - right now I just want the original, short names to wrap inside the nodes. I've looked at this answer and this answer (http://jsfiddle.net/Tmj7g/4/) but when I try to implement this into my own code, it is not responding or ends up clustering all the nodes in the top left corner (??).

Any and all input is GREATLY appreciated, and feel free to edit my fiddle here: https://jsfiddle.net/lilyelle/496c2bmr/

I also know that all of my language is not entirely consistent or the simplest way of writing the D3 code - that's because I've copied and spliced together lots of things from different sources and am still trying to figure out the best way to write this stuff for myself. Any advice in this regard is also appreciated.

Community
  • 1
  • 1
lilyelle
  • 5
  • 4

1 Answers1

0

1) Collision detection: Here's an updated, working jsFiddle, which was guided by this example from mbostock. Adding collision detection was largely a copy/paste of the important bits. Specifically, in the tick function, I added the code that loops over all those nodes and tweaks their positions if they collide:

var q = d3.geom.quadtree(nodes),
    i = 0,
    n = nodes.length;

while (++i < n) q.visit(collide(nodes[i]));

Since your jsFiddle didn't have a variable nodes set, I added it right above the last snipped:

var nodes = force.nodes()

Also, that loop requires the function collide to be defined, just as it is in Bostock's example, so I added that to your jsFiddle as well:

function collide(node) {
  var r = node.radius + 16,
      nx1 = node.x - r,
      nx2 = node.x + r,
      ny1 = node.y - r,
      ny2 = node.y + r;
  return function(quad, x1, y1, x2, y2) {
    if (quad.point && (quad.point !== node)) {
      var x = node.x - quad.point.x,
          y = node.y - quad.point.y,
          l = Math.sqrt(x * x + y * y),
          r = node.radius + quad.point.radius;
      if (l < r) {
        l = (l - r) / l * .5;
        node.x -= x *= l;
        node.y -= y *= l;
        quad.point.x += x;
        quad.point.y += y;
      }
    }
    return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
  };
}

The last necessary bit comes from the fact that detecting node collision requires knowing their size. Bostock's code accesses node.radius inside the collide function above. Your example doesn't set the nodes' radii, so in that collide function node.radius is undefined. One way to set this radius is to add it to your json, e.g.

{radius: 30, "name":"AATF", "full_name":"African Agricultural Technology Foundation", "type":1}

If all your nodes will have the same radius, that's overkill.

Another way is to replace the 2 occurrences of node.radius with a hardcoded number, like 30.

I chose to do something between those two options: assign a constant node.radius to each node by looping over the loaded json:

json.nodes.forEach(function(node) {
    node.radius = 30;
})

That's it for getting collision detection working. I used a radius of 30 because that's the radius you used for rendering these nodes, as in .attr("r", 30). That'll keep all the nodes bunched up — not overlapping but still touching each other. You can experiment with a larger value for node.radius to get some white space between them.

2) Text Wrapping: That's a tough one. There's no easy way to make the SVG <text> wrap at some width. Only regular html div/span can do that automatically, but even html elements can't wrap to fit a circle, only to a constant width.

You might be able to come up with some compromise that'll allow you to always fit some text. For example, if your data is all known ahead of time and the size of the circles is always the same fixed value, then you know ahead of time which labels can fit and which don't. The ones that don't, you can either shorten, by adding say a short_name attribute to each node in your JSON, and setting it to something that'll definitely fit. Alternatively, still if the size and labels are known in advance, you can pre-determine how to break up the labels into multiple lines and hard code that into your JSON. Then, when you render, you can render that text using multiple SVG <text> elements that you manually position as multiple lines. Finally, if nothing is known ahead of time, then you might be able to get a good solution by switching to rendering the text as absolutely positioned divs on top of (and outside of) the SVG, with a widths that match the circles' widths, so that the text would automatically wrap.

meetamit
  • 24,727
  • 9
  • 57
  • 68
  • Oh. My. Goodness. THANK YOU for this fantastic answer, and for the explanations! It makes much more sense now. I see what you mean about the text wrapping - I think it's something I'll just have to adjust manually, which now that the collision detection is working will be an easier task. – lilyelle Jun 30 '16 at 18:29
  • @lilyelle, awesome - happy to help! One thing: if my answer solves your issue, then please click the check icon to "accept" it. StackOverflow runs on points, and by you accepting my answer I get a point for it :) – meetamit Jun 30 '16 at 20:19