cola.js home

Layout kept inside bounding box

Source

Example showing how we can setup constraints to keep the graph nodes inside a given bounding box (as per this paper). Try dragging a node outside the box: you'll see the bounding box stretch to enclose it, and then snap back on release. Toggle the 'boundary unlocked' button to stop nodes stretching the boundary.

The idea is, we create a pair of dummy nodes: one for the top left corner of the box, and one for the bottom right. Then we create constraints to keep the real nodes below and to the right of the top-left, and above and to the left of the bottom right. Here's the setup:

var pageBounds = { x: 100, y: 50, width: 700, height: 400 },
    page = svg.append('rect').attr('id', 'page').attr(pageBounds),
    nodeRadius = 10,
    realGraphNodes = graph.nodes.slice(0),
    fixedNode = {fixed: true, fixedWeight: 100},
    topLeft = { ...fixedNode, x: pageBounds.x, y: pageBounds.y },
    tlIndex = graph.nodes.push(topLeft) - 1,
    bottomRight = { ...fixedNode, x: pageBounds.x + pageBounds.width, y: pageBounds.y + pageBounds.height },
    brIndex = graph.nodes.push(bottomRight) - 1,
    constraints = [];
    for (var i = 0; i < realGraphNodes.length; i++) {
        constraints.push({ axis: 'x', type: 'separation', left: tlIndex, right: i, gap: nodeRadius });
        constraints.push({ axis: 'y', type: 'separation', left: tlIndex, right: i, gap: nodeRadius });
        constraints.push({ axis: 'x', type: 'separation', left: i, right: brIndex, gap: nodeRadius });
        constraints.push({ axis: 'y', type: 'separation', left: i, right: brIndex, gap: nodeRadius });
    }
    cola
        .nodes(graph.nodes)
        .links(graph.links)
        .constraints(constraints)
        .jaccardLinkLengths(60, 0.7)
        .handleDisconnected(false)
        .start(30);
Note that we disable handleDisconnected. The layout of disconnected components is not very smart at the moment and isn't aware of constraints connecting separate graph components (TODO: fix this!).

Then, when we create the node visuals, we bind them only to the original (non-dummy) nodes:

var node = svg.selectAll(".node")
    .data(realGraphNodes)
    .enter().append("circle")
        .attr("class", "node")
        .attr("r", nodeRadius)
        .style("fill", function (d) { return color(d.group); })
        .call(cola.drag);

To keep the visuals for the page bounds up-to-date, i.e. in case the user drags a node outside the bounds, inside the tick handler we do the following:

page.attr(pageBounds = {
    x: topLeft.x,
    y: topLeft.y,
    width: bottomRight.x - topLeft.x,
    height: bottomRight.y - topLeft.y
});

The page bounds are 'stretchy' by default because the dummy nodes for the page corners are assigned the same "weight" as a dragged node. Thus, when the solver satisfies violated separation constraints between dummy nodes and the dragged nodes it displaces them both equally. The "boundary locked" button above simply sets the fixedWeight property of the dummy nodes to a value several orders of magnitude larger than the default weight.