Search code examples
javascriptd3.jsd3-force-directed

How to label a force directed Graph on d3?


I am creating a force-directed graph with d3.js and can't get passed the point of labelling the nodes with text. I have tried countless answers here on StackOverflow and online tutorials, yet I believe that the problems lie in my fundamental Javascript understanding. enter image description here

I have tried different combinations of .attr/.append/.text to get the text from the source and target to appear, but nothing ever happens.

This is the area in question:

node.append("title")
    .text(function (d) {return d.target});

node.append("text")
    .attr("dy", -3)
    .text(function (d) {return d.source})
    .attr("class", "font");

This is a simplified excerpt of the style:

<style>

.node {
    fill: #ccc; /* Fill of the circles*/
    stroke: #ffffff;
    stroke-width: 2px;
}

.font {
    font: 10px;
    font-family: sans-serif;

}

.link {
    stroke: #777; /* Colour of the lines*/
    stroke-width: 2px;
}
</style>

This is a simplified excerpt of the script:

var width = 640,
    height = 480;

    var links = [
    //this is an array
    {source: "Germany", target: "name1"},
    {source: "Germany", target: "name2"},
    {source: "Nigeria", target: "name3"},
    {source: "Environment", target: "name4"},

    ]; 

    //setting up the nodes:

    var nodes = {};


    links.forEach(function(link){
        link.source = nodes[link.source] || 
            (nodes[link.source] = {name: link.source});
        link.target = nodes[link.target] ||
            (nodes[link.target] = {name: link.target});    
    });


//add svg to the body, this is where the actual d3 starts

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

var force = d3.layout.force() //Here we specify the paramaters
    .size([width,height])
    .nodes(d3.values(nodes)) //this is where we pass the nodes of our dataset
    .links(links) // source of links
    .on("tick", tick) //on click of the nodes
    .linkDistance(300) //How far apart the nodes are
    .start(); //Start to render

//add link and nodes
var link = svg.selectAll(".link")
    .data(links) //get the data
    .enter().append('line') //binds the data in the links array to the svg 
    .attr("class", "link") //css styling

var node = svg.selectAll(".node")
    .data(force.nodes()) //way to reference the nodes in the force layout
    .enter().append("circle") 
    .attr("class", "node") //attribute CSS styling
    .attr("r", width * 0.03); //radius of the circle

//text element
node.append("title")
    .text(function (d) {return d.target});

node.append("text")
    .attr("dy", -3)
    .text(function (d) {return d.source})
    .attr("class", "font");

//creating the tick function from the force variable
//the "e" paramater can be used for positioning

function tick(e) {
    node.attr("cx", function(d) {return d.x;}) 
        .attr("cy", function(d) {return d.y;})
        .call(force.drag); //the relative location will activate a drag once the node is clicked

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

    }
</script>

I'm currently not getting any error messages which is making it hard for me to debug the file. Any help is appreciated. Thank you!


Solution

  • There are two problems in your code.

    The first problem is your node selection:

    var node = svg.selectAll(".node")
        .data(force.nodes())
        .enter().append("circle") 
        //etc...
    

    As you can see, this is a selection of circles. Later, when you try...

    node.append("text")
    

    ... it won't work because you cannot append a <text> element to a <circle> element.

    The most common solution is making node a group (<g>) selection, to which you append both circles and texts.

    The second problem is the data for the nodes. You have this in your texts:

    node.append("text")
        .text(function (d) {return d.source})
    

    However, there is no property named source in the data. The only property you have is name.

    Here is your code with those changes:

    var width = 640,
      height = 480;
    
    var links = [
      //this is an array
      {
        source: "Germany",
        target: "name1"
      },
      {
        source: "Germany",
        target: "name2"
      },
      {
        source: "Nigeria",
        target: "name3"
      },
      {
        source: "Environment",
        target: "name4"
      },
    
    ];
    
    //setting up the nodes:
    
    var nodes = {};
    
    
    links.forEach(function(link) {
      link.source = nodes[link.source] ||
        (nodes[link.source] = {
          name: link.source
        });
      link.target = nodes[link.target] ||
        (nodes[link.target] = {
          name: link.target
        });
    });
    
    
    //add svg to the body, this is where the actual d3 starts
    
    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);
    
    var force = d3.layout.force() //Here we specify the paramaters
      .size([width, height])
      .nodes(d3.values(nodes)) //this is where we pass the nodes of our dataset
      .links(links) // source of links
      .on("tick", tick) //on click of the nodes
      .linkDistance(300) //How far apart the nodes are
      .start(); //Start to render
    
    //add link and nodes
    var link = svg.selectAll(".link")
      .data(links) //get the data
      .enter().append('line') //binds the data in the links array to the svg 
      .attr("class", "link") //css styling
    
    var node = svg.selectAll(".node")
      .data(force.nodes()) //way to reference the nodes in the force layout
      .enter().append("g");
    
    node.append("circle")
      .attr("class", "node")
      .attr("r", width * 0.03); //radius of the circle
    
    node.append("text")
      .attr("dy", -3)
      .text(function(d) {
        return d.name
      })
      .attr("class", "font");
    
    //creating the tick function from the force variable
    //the "e" paramater can be used for positioning
    
    function tick(e) {
      node.attr("transform", function(d) {
          return "translate(" + [d.x, d.y] + ")"
        })
        .call(force.drag); //the relative location will activate a drag once the node is clicked
    
      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;
        })
    
    }
    .node {
      fill: #ccc;
      /* Fill of the circles*/
      stroke: #ffffff;
      stroke-width: 2px;
    }
    
    .font {
      font: 10px;
      font-family: sans-serif;
    
    }
    
    .link {
      stroke: #777;
      /* Colour of the lines*/
      stroke-width: 2px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>