Tom Auger
Tom Auger

Reputation: 20091

"Pinning" nodes in a D3 force-directed graph

I have constructed a regular "grid" that places nodes at evenly-spaced points across the grid. Then, by randomizing the linkDistance, I'm able to "stir up" the grid so it's less regular.

I'd like to "pin" all the edge points such that they don't move - leaving only the inner points to be affected by the force layout.

My insight was that since this is a regular quad grid, any points with a weight less than 4 would be an "edge" point, and thus should be pinned.

I figure that weight is only calculated after the nodes and links have been added to the force layout, so I'm forEaching through the nodes array, after adding it to the force layout, and conditionally setting the fixed property based on weight.

Then, I'm reapplying the nodes property and starting the simulation.

No good. In the example I'm attaching ALL the points move.

force = d3.layout.force()
                .size( [w, h ] )
                .nodes( nodes )
                .links( links )
                .linkDistance( function( d ){ return Math.random() * GRID_SPACING; } )
                .linkStrength( 1 )
                .charge( 0 )
                .gravity( 0 )
                .friction( .5 )
                .on( "tick", function() {
                    d3links.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; });

                    d3nodes.attr("cx", function(d) { return d.x; })
                        .attr("cy", function(d) { return d.y; });
                } );

            // "Pin" all the edge nodes.
            nodes.forEach( function( node ){
                if ( node.weight < 4 ){
                    node.fixed = true;
                }
            } );

            force.nodes( nodes ).start();

Upvotes: 5

Views: 5978

Answers (1)

Cool Blue
Cool Blue

Reputation: 6476

Your insight is a good one! But timing is everything...
The "start" event is triggered after the weights have been initialised so this should work...

force = d3.layout.force()
    .size([w, h])
    .nodes(nodes)
    .links(links)
    .linkDistance(function (d) { return Math.random() * GRID_SPACING; })
    .linkStrength(1)
    .charge(0)
    .gravity(0)
    .friction(.5)
    .on("tick", function () {
        d3links.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; });

        d3nodes.attr("cx", function (d) { return d.x; })
                .attr("cy", function (d) { return d.y; });
    })
    .on("start", function () {
        // "Pin" all the edge nodes.
        nodes.forEach(function (node) {
            if (node.weight < 4) {
                node.fixed = true;
            }
        });
    })


force.nodes(nodes).start();

(If you want to include drag behavior then you would need to re-fix after dragend also.)

Upvotes: 2

Related Questions