score:62

Accepted answer

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.

score:1

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)); });

score:3

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));
    }
}

Related Query