37

I am new to D3 and having trouble setting the bounds for my force directed layout. I have managed to piece together (from examples) what I would like, but I need the graph to be contained. In the tick function, a transform/translate will display my graph correctly, but when i use cx and cy with Math.max/min (See commented code), the nodes are pinned to the top left corner while the lines are contained properly.

Here is what I have below... what am I doing wrong??

var w=960, h=500, r=8,  z = d3.scale.category20();

var color = d3.scale.category20();

var force = d3.layout.force()
       .linkDistance( function(d) { return (d.value*180) } )
       .linkStrength( function(d) { return (1/(1+d.value)) } )
       .charge(-1000)
       //.gravity(.08)
       .size([w, h]);

var vis = d3.select("#chart").append("svg:svg")
       .attr("width", w)
       .attr("height", h)
       .append("svg:g")
       .attr("transform", "translate(" + w / 4 + "," + h / 3 + ")");

vis.append("svg:rect")
   .attr("width", w)
   .attr("height", h)
   .style("stroke", "#000");


d3.json("miserables.json", function(json) {

       var link = vis.selectAll("line.link")
               .data(json.links);

       link.enter().append("svg:line")
               .attr("class", "link")
               .attr("x1", function(d) { return d.source.x; })
               .attr("y1", function(d) { return d.source.y; })
               .attr("x2", function(d) { return d.source.x; })
               .attr("y2", function(d) { return d.source.y; })
               .style("stroke-width", function(d) { return (1/(1+d.value))*5 });

       var node = vis.selectAll("g.node")
               .data(json.nodes);

       var nodeEnter = node.enter().append("svg:g")
               .attr("class", "node")
               .on("mouseover", fade(.1))
               .on("mouseout", fade(1))
               .call(force.drag);

       nodeEnter.append("svg:circle")
               .attr("r", r)
               .style("fill", function(d) { return z(d.group); })
               .style("stroke", function(d) { return
d3.rgb(z(d.group)).darker(); });

       nodeEnter.append("svg:text")
               .attr("text-anchor", "middle")
               .attr("dy", ".35em")
               .text(function(d) { return d.name; });

       force
       .nodes(json.nodes)
       .links(json.links)
       .on("tick", tick)
       .start();

       function tick() {

       // This works
               node.attr("transform", function(d) { return "translate(" + d.x + ","
+ d.y + ")"; });

       // This contains the lines within the boundary, but the nodes are
stuck in the top left corner
               //node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
               //      .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h -
r, d.y)); });

       link.attr("x1", function(d) { return d.source.x; })
               .attr("y1", function(d) { return d.source.y; })
               .attr("x2", function(d) { return d.target.x; })
               .attr("y2", function(d) { return d.target.y; });
       }

       var linkedByIndex = {};

   json.links.forEach(function(d) {
       linkedByIndex[d.source.index + "," + d.target.index] = 1;
   });

       function isConnected(a, b) {
       return linkedByIndex[a.index + "," + b.index] ||
linkedByIndex[b.index + "," + a.index] || a.index == b.index;
   }

       function fade(opacity) {
       return function(d) {
           node.style("stroke-opacity", function(o) {
                       thisOpacity = isConnected(d, o) ? 1 : opacity;
                       this.setAttribute('fill-opacity', thisOpacity);
               return thisOpacity;
                       });

                       link.style("stroke-opacity", opacity).style("stroke-opacity",
function(o) {
               return o.source === d || o.target === d ? 1 : opacity;
               });
       };
       }

});
VividD
  • 10,456
  • 6
  • 64
  • 111
Michael Wilson
  • 524
  • 1
  • 4
  • 9
  • I have played with some parameters and decided that unless someone has the solution, I will just use lots of .gravity(). If the graph is very large it will still pose a problem, but otherwise should contain the nodes just fine. – Michael Wilson Mar 05 '12 at 20:43

3 Answers3

66

There's a bounding box example in my talk on force layouts. The position Verlet integration allows you to define geometric constraints (such as bounding boxes and collision detection) inside the "tick" event listener; simply move the nodes to comply with the constraint and the simulation will adapt accordingly.

That said, gravity is definitely a more flexible way to deal with this problem, since it allows users to drag the graph outside the bounding box temporarily and then the graph will recover. Depend on the size of the graph and the size of the displayed area, you should experiment with different relative strengths of gravity and charge (repulsion) to get your graph to fit.

mbostock
  • 51,423
  • 13
  • 175
  • 129
  • 19
    The key is add `node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(width - r, d.x)); }) .attr("cy", function(d) { return d.y = Math.max(r, Math.min(height - r, d.y)); });` If you use `path` instead of `line`, you will need add the boundary check for `attr('d', function () {...});``` as well. – Limin May 29 '13 at 21:56
  • @mbostock, is there a way to provide coordinates of the boundaries(move only inside defined 4 corners) – Rishi0405 Nov 22 '18 at 11:43
  • 1
    And that is, ladies and gentlemen, why we don't just post links, but actual answers: After almost 10 years, two of the three links don't work anymore. – Basti Feb 08 '22 at 17:17
6

A custom force is a possible solution too. I like this approch more since not only the displayed nodes are repositioned but the whole simulation works with the bounding force.

let simulation = d3.forceSimulation(nodes)
    ...
    .force("bounds", boxingForce);

// Custom force to put all nodes in a box
function boxingForce() {
    const radius = 500;

    for (let node of nodes) {
        // Of the positions exceed the box, set them to the boundary position.
        // You may want to include your nodes width to not overlap with the box.
        node.x = Math.max(-radius, Math.min(radius, node.x));
        node.y = Math.max(-radius, Math.min(radius, node.y));
    }
}
Bruno Zell
  • 7,761
  • 5
  • 38
  • 46
2

The commented code works on node which is, from your definition, a svg g(rouping) element and does not operate the cx/cy attributes. Select the circle element inside node to make these attributes come alive:

node.select("circle") // select the circle element in that node
    .attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
    .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });
Marc
  • 51
  • 2