Space out nodes evenly around root node in D3 force layout

Fillip Peyton picture Fillip Peyton · Apr 15, 2015 · Viewed 8.6k times · Source

I am just starting on D3, so if anyone has any general suggestions on thing I might not be doing correctly/optimally, please let me know :)

I am trying to create a Force Directed graph with the nodes spaced out evenly (or close enough) around the center root node (noted by the larger size).

Here's an example of the layout I'm trying to achieve (I understand it won't be the same every time):
enter image description here

I have the following graph:

var width = $("#theVizness").width(),
    height = $("#theVizness").height();

var color = d3.scale.ordinal().range(["#ff0000", "#fff000", "#ff4900"]);

var force = d3.layout.force()
    .charge(-120)
    .linkDistance(30)
    .size([width, height]);

var svg = d3.select("#theVizness").append("svg")
    .attr("width", width)
    .attr("height", height);

var loading = svg.append("text")
    .attr("class", "loading")
    .attr("x", width / 2)
    .attr("y", height / 2)
    .attr("dy", ".35em")
    .style("text-anchor", "middle")
    .text("Loading...");

/*
ForceDirectData.json
{
    "nodes":[
      {"name":"File1.exe","colorGroup":0},
      {"name":"File2.exe","colorGroup":0},
      {"name":"File3.exe","colorGroup":0},
      {"name":"File4.exe","colorGroup":0},
      {"name":"File5.exe","colorGroup":0},
      {"name":"File6.exe","colorGroup":0},
      {"name":"File7.exe","colorGroup":0},
      {"name":"File8.exe","colorGroup":0},
      {"name":"File8.exe","colorGroup":0},
      {"name":"File9.exe","colorGroup":0}
    ],
    "links":[
      {"source":1,"target":0,"value":10},
      {"source":2,"target":0,"value":35},
      {"source":3,"target":0,"value":50},
      {"source":4,"target":0,"value":50},
      {"source":5,"target":0,"value":65},
      {"source":6,"target":0,"value":65},
      {"source":7,"target":0,"value":81},
      {"source":8,"target":0,"value":98},
      {"source":9,"target":0,"value":100}
    ]
}
*/

d3.json("https://dl.dropboxusercontent.com/u/5772230/ForceDirectData.json", function (error, json) {
    var nodes = json.nodes;
    force.nodes(nodes)
        .links(json.links)
        .linkDistance(function (d) {
            return d.value * 1.5;
        })
        .charge(function(d){
            var charge = -500;
            
            if (d.index === 0) charge = 0;
            
            return charge;
        })
        .friction(0.4);
    
    var link = svg.selectAll(".link")
        .data(json.links)
        .enter().append("line")
        .attr("class", "link")              
        .style("stroke-width", 1);
    
        var files = svg.selectAll(".file")
        .data(json.nodes)
        .enter().append("circle")
        .attr("class", "file")
        .attr("r", 10)
        .attr("fill", function (d) {
            return color(d.colorGroup);
        });
    var totalNodes = files[0].length;
    
    files.append("title")
        .text(function (d) { return d.name; });
    
    force.start();
    for (var i = totalNodes * totalNodes; i > 0; --i) force.tick();

    
    nodes[0].x = width / 2;
    nodes[0].y = height / 2;
    
    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; });
    
    files.attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; })
        .attr("class", function(d){
            var classString = "file"
            
            if (d.index === 0) classString += " rootFile";
            
            return classString;
        })
        .attr("r", function(d){
            var radius = 10;
            
            if (d.index === 0) radius = radius * 2;
            
            return radius;
        });
    force.on("tick", function() {
    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; });

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

JSFiddle

I have already tried getting close to this with the charge() method. I thought giving every node besides the root node a higher charge would accomplish this, but it did not.

What can I do to have the child nodes evenly spaced around the root node?

Answer

VividD picture VividD · Apr 15, 2015

Yes, force layout is a perfect tool for situations like yours.

You just need to change a little initialization of the layout, like this

force.nodes(nodes)
    .links(json.links)
    .charge(function(d){
        var charge = -500;
        if (d.index === 0) charge = 10 * charge;
        return charge;
    });

and voila

enter image description here

Explanation. I had to remove settings for friction and linkDistance since they affected placement in a bad way. The charge for root node is 10 times larger so that all other nodes are dominantly pushed away from the root. Other nodes also repel each other, and the perfect symmetry is achieved at the end, as a result.

Jsfiddle is here.


I see from your code that you attempted to affect distance from the root node and other nodes by utilizing linkDistance that is dependant on data. However, it might be better (although counter-intuitive) to use linkStrength for that purpose, like this

force.nodes(nodes)
    .links(json.links)
    .linkStrength(function (d) {
        return d.value / 100.0;
    })
    .charge(function(d){
        var charge = -500;
        if (d.index === 0) charge = 10 * charge;
        return charge;
    });

but you need to experiment.


For centering and fixing the root node, you can use this

nodes[0].fixed = true;
nodes[0].x = width / 2;
nodes[0].y = height / 2;

but before initialization of layout, like in this Jsfiddle.