Leitouran
Leitouran

Reputation: 578

Can't draw links by node property using D3 force layout

I'm trying to make a force layout on D3 for network visualization with nodes and links. I've been using v3 a little bit, but I switched to v5 in order to be able to link nodes using node attributes instead of node indexes (which required additional steps in d3 v3).

I got this code

https://jsfiddle.net/lioneluranl/4fxpp2co/1/

var linkpath = ("links.csv");
var nodepath = ("nodes.csv");
var svg = d3.select("svg");
var width = svg.attr("width");
var height = svg.attr("height");
var simulation = d3.forceSimulation();   

var nodes = [];
var links = [];

d3.csv(nodepath, function(d){
  node = {
    id: d.node,
    group: d.group,
    node: d.node
  };  

  nodes.push(node);

  d3.csv(linkpath, function(d){
    link = {
      source: d.source,
      target: d.target,
      type: d.type
    }; 

    links.push(link);

  });

}).then( function() {

  //console.log(links);
  //console.log(nodes);

  simulation
      .force("link", d3.forceLink().id(function(d) { /*console.log(d);*/ return d.id; }))
      .nodes(nodes)      
      .force("collide", d3.forceCollide().radius(10))
      .force("r", d3.forceRadial(function(d) { 
        if(d.group === "compound"){          
          return 240;
        } else { return d.group === "annotation" ? 0 : 100; }}))      

  // Create the link lines.
  var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(links)
    .enter().append("line")
    .attr("stroke", "black")
    .attr("stroke-width", 4)
    .attr("class", function(d) { return "link " + d.type; });

  // Create the node circles.
 var node = svg.append("g")
      .attr("class", "node")
      .selectAll("circle")
      .data(nodes)
      .enter().append("circle")
      .attr("r", 8)
      .attr("class", function (d){ 

        return d.group;

      });

 simulation.on("tick", ticked);
 simulation.force("link").links(links);

function ticked() {
  node
      .attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return 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; });
}


}); 

Which has been adapted from this

https://bl.ocks.org/mbostock/cd98bf52e9067e26945edd95e8cf6ef9

I have no problems drawing the nodes, but I'm unable to draw the links. Related documentation indicates that, as I think I'm doing, node attributes must be passed to links on force simulation, but I get this error:

TypeError: can't assign to property "vx" on "PF05257": not an object

Also, when doing this, the nodes won't behave as expected on the layout (the radial forces set won't work, see pictures attached), suggesting this link-by-node-attribute is messing with my simulation.

Not broken layout, but without adding the forceLinks Layout broken after trying to add links

CSVs contain the following data:

nodes.csv:

node,group
C236103,compound
C327961,compound
C337527,compound
C376038,compound
C543486,compound
T24871,target
T27222,target
T33516,target
T33937,target
OG5_135897,annotation
PF01529,annotation
PF05257,annotation
PF11669,annotation
...

links.csv

source,target,type
T24871,PF05257,annotation
T27222,PF05257,annotation
T33516,PF01529,annotation
T33516,PF05257,annotation
T33516,PF11669,annotation
T33937,PF05257,annotation
T24871,C561727,bioactivity
T24871,C337527,bioactivity
T24871,C585910,bioactivity
...

Just for reference and data integrity sanity check, I got this working on d3 v3. enter image description here

Any ideas?

Upvotes: 2

Views: 807

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

Here is a constructive criticism: indent your code properly1.

The first time I read it I missed the problem, because of the incorrect indentation. But then, with the correct indentation, the issue is clear, have a look at this:

d3.csv(nodepath, function(d) {
    node = {
        id: d.node,
        group: d.group,
        node: d.node
    };

    nodes.push(node);

    d3.csv(linkpath, function(d) {
        link = {
            source: d.source,
            target: d.target,
            type: d.type
        };

        links.push(link);

    });
})

You cannot nest d3.csv like this. The way your code is right now the second d3.csv is part of the row function of the first d3.csv, and that obviously won't work.

The correct approach here would be nesting the promises (which is an anti-pattern for some) or, even better, using Promise.all (since there is no d3.queue in v5):

var promises = [d3.csv("nodes.csv"), d3.csv("links.csv")];

Promise.all(promises).then(function(data) {
    var links = data[1];
    var nodes = data[0];
    //rest of the code here
});

As an additional tip, you don't need to push the objects to arrays in the outer scope: just deal with the parameter inside then.

Also, you don't need the row function for both CSVs, since your row functions are not doing anything right now (besides duplicating node as id they are simply returning the same object that you would have without them).

Here is a bl.ocks with your code and data, using Promise.all:

https://bl.ocks.org/GerardoFurtado/30cb90cc9eb4f239f59b323bbdfe4293/3049fc77b8461232b6b149f39066ec39e0d111c1


1 Most text editors, like Sublime Text, have plugins for indentation. You can also find several good tools online, like this one for instance.

Upvotes: 3

Related Questions