July 23, 2014 Simon Raper

An A to Z of extra features for the D3 force layout

Tweet about this on TwitterShare on LinkedInShare on FacebookGoogle+Share on StumbleUponEmail to someone

Since d3 can be a little inaccessible at times I thought I’d make things easier by starting with a basic skeleton force directed layout (Mike Bostock’s original example) and then giving you some blocks of code that can be plugged in to add various features that I have found useful.

The idea is that you can pick the features you want and slot in the code. In other words I’ve tried to make things sort of modular. The code I’ve taken from various places and adapted so thank you to everyone who has shared. I will try to provide the credits as far as I remember them!


Basic Skeleton

Here’s the basic skeleton based on Mike Bostock’s original example but with a bit of commentary to remind me exactly what is going on



A is for arrows

If you are dealing with a directed graph and you wish to use arrows to indicate direction your can just append this bit of code

svg.append("defs").selectAll("marker")
    .data(["suit", "licensing", "resolved"])
  .enter().append("marker")
    .attr("id", function(d) { return d; })
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 25)
    .attr("refY", 0)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("path")
    .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
    .style("stroke", "#4679BD")
    .style("opacity", "0.6");

And just modify one line of the existing code

//Create all the line svgs but without locations yet
var link = svg.selectAll(".link")
    .data(graph.links)
    .enter().append("line")
    .attr("class", "link")
    .style("marker-end",  "url(#suit)") // Modified line 
    ;

Which gives us…



B is for breaking links

It’s nice to be able to see what your graph looks like when you vary the threshold at which a connection between nodes becomes a link.

This requires us to append the following code

//adjust threshold
function threshold(thresh) {
    graph.links.splice(0, graph.links.length);
		for (var i = 0; i < graphRec.links.length; i++) {
			if (graphRec.links[i].value > thresh) {graph.links.push(graphRec.links[i]);}
		}
    restart();
}
//Restart the visualisation after any node and link changes
function restart() {
	link = link.data(graph.links);
	link.exit().remove();
	link.enter().insert("line", ".node").attr("class", "link");
	node = node.data(graph.nodes);
	node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
	force.start();
}

And add this line

var mis = document.getElementById('mis').innerHTML;
graph = JSON.parse(mis);
 graphRec=JSON.parse(JSON.stringify(graph)); //Add this line 

We get the following:



C is for collision detection

We can use collision detection to ensure that nodes do not overlap. This is sometimes useful when you have a lot of nodes or you have labels attached to them. I adapted this from this example by Mike Bostock.

We just need to append

var padding = 1, // separation between circles
    radius=8;
function collide(alpha) {
  var quadtree = d3.geom.quadtree(graph.nodes);
  return function(d) {
    var rb = 2*radius + padding,
        nx1 = d.x - rb,
        nx2 = d.x + rb,
        ny1 = d.y - rb,
        ny2 = d.y + rb;
    quadtree.visit(function(quad, x1, y1, x2, y2) {
      if (quad.point && (quad.point !== d)) {
        var x = d.x - quad.point.x,
            y = d.y - quad.point.y,
            l = Math.sqrt(x * x + y * y);
          if (l < rb) {
          l = (l - rb) / l * alpha;
          d.x -= x *= l;
          d.y -= y *= l;
          quad.point.x += x;
          quad.point.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    });
  };
}

And slot an additional line into the force tick function.

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;
    });
    node.attr("cx", function (d) {
        return d.x;
    })
        .attr("cy", function (d) {
        return d.y;
    });
     node.each(collide(0.5)); //Added 
});



F is for fisheye

This code is based on Mike Bostock's original example.

It is simply a matter of appending

var fisheye = d3.fisheye.circular()
      .radius(120);
svg.on("mousemove", function() {
      force.stop();
      fisheye.focus(d3.mouse(this));
      d3.selectAll("circle").each(function(d) { d.fisheye = fisheye(d); })
          .attr("cx", function(d) { return d.fisheye.x; })
          .attr("cy", function(d) { return d.fisheye.y; })
          .attr("r", function(d) { return d.fisheye.z * 8; });
      link.attr("x1", function(d) { return d.source.fisheye.x; })
          .attr("y1", function(d) { return d.source.fisheye.y; })
          .attr("x2", function(d) { return d.target.fisheye.x; })
          .attr("y2", function(d) { return d.target.fisheye.y; });
    });

And adding in the fisheye js

<script type='text/javascript' src="http://bost.ocks.org/mike/fisheye/fisheye.js?0.0.3"> </script>



H is for highlighting

This is about fading out nodes and links that aren't connected to the node you've doubleclicked on. I've based this on the this answer on Stackoverflow.

Here we just append the following block

//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
    linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
    linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
    return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
    if (toggle == 0) {
        //Reduce the opacity of all but the neighbouring nodes
        d = d3.select(this).node().__data__;
        node.style("opacity", function (o) {
            return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
        });
        link.style("opacity", function (o) {
            return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
        });
        //Reduce the op
        toggle = 1;
    } else {
        //Put them back to opacity=1
        node.style("opacity", 1);
        link.style("opacity", 1);
        toggle = 0;
    }
}

and add one line to the node set up

var node = svg.selectAll(".node")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("class", "node")
    .attr("r", 5)
    .style("fill", function (d) {
    return color(d.group);
})
    .call(force.drag)
     .on('dblclick', connectedNodes); //Added code 



L is for labels

This is a little bit more involved as we need to associate each node with a group consisting of a circle and text. Here is the section with the changes in it:

var node = svg.selectAll(".node")
    .data(graph.nodes)
    .enter().append("g")
    .attr("class", "node")
    .call(force.drag);
node.append("circle")
    .attr("r", 8)
    .style("fill", function (d) {
    return color(d.group);
})
node.append("text")
      .attr("dx", 10)
      .attr("dy", ".35em")
      .text(function(d) { return d.name })
      .style("stroke", "gray");
//Now we are giving the SVGs co-ordinates - the force layout is generating the co-ordinates which this code is using to update the attributes of the SVG elements
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;
    });
    d3.selectAll("circle").attr("cx", function (d) {
        return d.x;
    })
        .attr("cy", function (d) {
        return d.y;
    });
    d3.selectAll("text").attr("x", function (d) {
        return d.x;
    })
        .attr("y", function (d) {
        return d.y;
    });
});

Then you just need to styles the labels in the css.

.node text {
  font: 9px helvetica;
}



P is for pinning down nodes

Here we add the possibility of pinning a node a particular point. This is useful when exploring large networks. Drag a node to pin it down. Double click on a node to release it.

We need to add the following code just below the set up of the SVG

var node_drag = d3.behavior.drag()
        .on("dragstart", dragstart)
        .on("drag", dragmove)
        .on("dragend", dragend);
    function dragstart(d, i) {
        force.stop() // stops the force auto positioning before you start dragging
    }
    function dragmove(d, i) {
        d.px += d3.event.dx;
        d.py += d3.event.dy;
        d.x += d3.event.dx;
        d.y += d3.event.dy;
    }
    function dragend(d, i) {
        d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
        force.resume();
    }
    function releasenode(d) {
        d.fixed = false; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
        //force.resume();
    }

And then substitute in node_drag for force.drag.

//Do the same with the circles for the nodes - no
var node = svg.selectAll(".node")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("class", "node")
    .attr("r", 8)
    .style("fill", function (d) {
    return color(d.group);
})
.on('dblclick', releasenode)
 .call(node_drag); //Added 



S is for search

When the graph is huge it's nice to have some search functionality. We can use some jquery to create an autocompleting search box so the first thing is to add references to the jquery-ui libraries.

<script type='text/javascript' src="http://code.jquery.com/ui/1.11.0/jquery-ui.min.js"> </script>
<script type='text/javascript' src="http://code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css"> </script>

Next you need to append the following code

var optArray = [];
for (var i = 0; i < graph.nodes.length - 1; i++) {
    optArray.push(graph.nodes[i].name);
}
optArray = optArray.sort();
$(function () {
    $("#search").autocomplete({
        source: optArray
    });
});
function searchNode() {
    //find the node
    var selectedVal = document.getElementById('search').value;
    var node = svg.selectAll(".node");
    if (selectedVal == "none") {
        node.style("stroke", "white").style("stroke-width", "1");
    } else {
        var selected = node.filter(function (d, i) {
            return d.name != selectedVal;
        });
        selected.style("opacity", "0");
        var link = svg.selectAll(".link")
        link.style("opacity", "0");
        d3.selectAll(".node, .link").transition()
            .duration(5000)
            .style("opacity", 1);
    }
}

and then add the search box to the html

<div class="ui-widget">
   <input id="search">
    <button type="button" onclick="searchNode()">Search</button>
</div>



T is for tooltip

This involves using the tooltip library by labratrevenge. The tooltip here will give the name of the node but can easily be adapted to display any of the underlying data by modifying html attribute of the tip.

Here we add the following code:

To the css

d3-tip {
    line-height: 1;
    color: black;
}

And to the main body of javascript

//Set up tooltip
var tip = d3.tip()
    .attr('class', 'd3-tip')
    .offset([-10, 0])
    .html(function (d) {
    return  d.name + "";
})
svg.call(tip);

Plus a slight change to the node set up

//Do the same with the circles for the nodes - no
var node = svg.selectAll(".node")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("class", "node")
    .attr("r", 5)
    .style("fill", function (d) {
    return color(d.group);
})
    .call(force.drag)
 .on('mouseover', tip.show) //Added
 .on('mouseout', tip.hide); //Added 

You also need to add a line in the header to bring in the tooltip library by labratsrevenge

<script type='text/javascript' src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"> </script>

This gives us


Tagged: , , ,

About the Author

Simon Raper I am an RSS accredited statistician with over 15 years’ experience working in data mining and analytics and many more in coding and software development. My specialities include machine learning, time series forecasting, Bayesian modelling, market simulation and data visualisation. I am the founder of Coppelia an analytics startup that uses agile methods to bring machine learning and other cutting edge statistical techniques to businesses that are looking to extract value from their data. My current interests are in scalable machine learning (Mahout, spark, Hadoop), interactive visualisatons (D3 and similar) and applying the methods of agile software development to analytics. I have worked for Channel 4, Mindshare, News International, Credit Suisse and AOL. I am co-author with Mark Bulling of Drunks and Lampposts - a blog on computational statistics, machine learning, data visualisation, R, python and cloud computing. It has had over 310 K visits and appeared in the online editions of The New York Times and The New Yorker. I am a regular speaker at conferences and events.

Comments (40)

    • j

      Zoom and pan is easy. Just google it.

      I would like to make the labels as hyperlinks, and also clicking on a node collapses all its children.
      Could we see these examples also?

  1. p3k

    This is fantastic! Many thanks.

    I would be interested how one would approach an additional collision detection for the labels – to prevent them from overlapping…

  2. Pingback: Neo4j | Pearltrees
  3. Sandhya

    Hi,

    its very helpfull. Thanks.

    Can we make node size according to degree of node, means node size should be according to number of links it have. More links larger the size of node.

    Also can we highlight the nodes which are neighbors to selected nodes neighbor. means up to second degree connectivity.

    Can you please help on this issue.

  4. Hector Zamora

    Hi, this is very helpful, I wonder if you can add an example for hiperlinks on labels.

  5. Abhinay

    Could you add example of multiple links between 2 nodes which can be collapsible/expandable?

    Thanks for the great document.

  6. Joe

    Thanks for the great post.

    Do you know a way of making collision detection for links in a force directed graph, so they are drawn around nodes that are in the way of the direct line between source and target, rather than being drawn through those nodes?

  7. Louis

    Great piece!
    Just stumbled upon it, but I’m going to have fun reading it in depth for sure!

  8. jaydipsinh

    great article 🙂 i’m beginner in data visualization and using d3 with canvas so can you guide how to use above features with that ?

  9. keerthika

    Hi ,

    This is a great work to learn from. I am working on the searching nodes part and get this error
    Uncaught ReferenceError: jQuery is not defined(anonymous function) @ jquery-ui.min.js:8
    jquery-ui.css:13 Uncaught SyntaxError: Unexpected token .
    searchingforcelayout.html:1362 Uncaught ReferenceError: $ is not defined

    I have no clue what it means.

    It would be of great help if you could help me out.

    Thanks & Regards,
    Keerthika

    • Simon Raper

      Hi Keerthika

      The example uses jquery so you’ll need to include it as a source.

      Cheers

      Simon

  10. keerthika

    Hi ,

    I am working on the searching nodes part and get this error
    Uncaught ReferenceError: jQuery is not defined(anonymous function) @ jquery-ui.min.js:8
    jquery-ui.css:13 Uncaught SyntaxError: Unexpected token .
    searchingforcelayout.html:1362 Uncaught ReferenceError: $ is not defined

    I have no clue what it means.

    It would be of great help if you could help me out.

    Thanks & Regards,
    Keerthika

  11. Vishal Bharti

    Any recommendation for radial layout for d3 force, having a hard time implementing it

  12. Vishal Bharti

    Please post something on Radial Layout with grouping of nodes.

  13. Joy

    I just wanted to say thank you very much. Not only are these great examples, but I love how you present it. “Start with this and then for this example, just add that”. Because of your approach, I was very easily able to map my ugly code structure to your objects “wherever he says X, but in myXObj” and whatnot. Anyway, thank you, and please consider writing a book (if you haven’t already).

    Lastly, would you mind clarifying your copyright/licensing model. I need to know if these examples can be used/distributed/etc. and what kind of attribution needs to be applied.

    Thank you!

  14. Alexandre Sieira

    This is an invaluable reference, thank you very much.

    I was wondering if you ever had to tackle the problem of doing a “fit to contents” where you adjust the scale and translate to ensure that all nodes are visible. I’ve seen plenty of examples where you use the drag and zoom behavior to allow control using the mouse, but no ensured way to guarantee all the content is visible at once.

  15. Vedran

    How can I combine Labels and pinning down the nodes. Pinning overides the Labels for some reason. My knowledge is not sufficient to combine it together.

  16. Aliah

    A lifesaver! I struggle to make force layout work, so having code snippets as a starting point to adapt is incredibly helpful. Thanks!

  17. 6uillaum3

    Hi,

    I just noticed that when I use the code to change the threshold the stroke-width of the links(edges) are not stable. It is present in the example above as well.

    For example, the link between a specific node A and B can have different width if I change the threshold.

    Is it normal ? Is there a way to make sure the links width always match the data ?

    For one of the network I am building I really need the links width to be consistent when changing the threshold 🙁

    Regards.

  18. Angeline Shalini

    Awesome features to go with the force directed graph. I hope to use the search feature in my code. Thanks a lot!!

Leave a Reply

Your email address will not be published. Required fields are marked *

Machine Learning and Analytics based in London, UK