Force directed graph: minimal working example
The goal of this post is to take what we covered earlier and make a simple working example of a force-directed graph.
We’ll start with just getting the nodes working, adding the links afterwards to complete the example. Find what we’ll be building here.
So open up a text file in your favourite editor. Fire up a local server. Find yourself a cool drink or two.
Ready? Let’s begin.
Nodes Link to heading
First thing we’ll do is set up a basic skeleton for our force directed graph. This will look familiar to you if you’ve played around with d3 before.
We also add some CSS to make our nodes and lines look nice.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
//create somewhere to put the force directed graph
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//d3 code goes here
</script>
Start your force-graph journey by creating some data for the nodes. We’re only going to go with six nodes.
var nodes_data = [
{"name": "Travis", "sex": "M"},
{"name": "Rake", "sex": "M"},
{"name": "Diana", "sex": "F"},
{"name": "Rachel", "sex": "F"},
{"name": "Shawn", "sex": "M"},
{"name": "Emerald", "sex": "F"}
]
Then set up the simulation with the nodes data.
//set up the simulation
//nodes only for now
var simulation = d3.forceSimulation()
.nodes(nodes_data);
Next we’ll add two forces to the simulation: a centering force and a charge force.
We’ll use the centering force to drive all the nodes towards the centre of the svg element. The force is implemented for us with a prepacked function called d3.forceCenter(), so we don’t really have to give this too much thought. We can just feed in the x and y coordinates of the centre of the svg element as function parameters and let it do its stuff.
The charge force treats each node as a charged particle, so that nodes will repel each other when they get too close together. The idea is that they’ll end up spaced apart naturally. Again there’s an already-written function available to implement this: d3.forceManyBody().
It’s easy to add these forces to the simulation through simulation.force(). The first argument names the force (make it whatever name you like) and the second argument specifies what force you want.
//add forces
//we're going to add a charge to each node
//also going to add a centering force
simulation
.force("charge_force", d3.forceManyBody())
.force("center_force", d3.forceCenter(width / 2, height / 2));
Next thing to do is to draw the circles inside our svg element. There’ll be one circle for each node, and by default they’ll start off all bunched up in the top left corner.
//draw circles for the nodes
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", 5)
.attr("fill", "red");
There’s just one thing missing before we’re done with this stage.
What’s lacking is a way to update the locations of the circles after every tick. You see, the simulation updates the position of nodes, but it doesn’t translate that across to svg circle representations. That’s our job.
We can solve this problem by writing some code to update circle locations in a function, and then call this function on every tick of the simulation.
Let’s write this function and call it tickActions()
.
function tickActions() {
//update circle positions to reflect node updates on each tick of the simulation
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
}
We instruct the simulation to apply our function tickActions()
on every tick through the simulation.on() method.
simulation.on("tick", tickActions );
You should be able to see something happening now on your force directed graph - six red dots in a rough pentagon shape. It’s strangely satisfying.
No links yet - we’ll add those now!
Links Link to heading
Start by specifying the link data:
//Create links data
var links_data = [
{"source": "Travis", "target": "Rake"},
{"source": "Diana", "target": "Rake"},
{"source": "Diana", "target": "Rachel"},
{"source": "Rachel", "target": "Rake"},
{"source": "Rachel", "target": "Shawn"},
{"source": "Emerald", "target": "Rachel"}
]
Next create the link force with the d3.forceLink() function. Add the link data to the link force by putting it as the argument to the function, so we’ll do that.
We also need to specify an accessor function through link.id(). Basically this is a function that tells the simulation which variable in the nodes_data
array corresponds to the text in the links_data
array.
Because our nodes each have a “name” attribute that we’re using to create the links, we use the link.id()
function to tell the accessor function to use the name
attribute to create our links for us.
For a better explanation of that read the documentation of link.id(). I tried.
Anyway, let’s create the link force:
//Create the link force
//We need the id accessor to use named sources and targets
var link_force = d3.forceLink(links_data)
.id(function(d) { return d.name; })
then add it to the simulation:
simulation.force("links",link_force)
Similar to the nodes, we need to physically draw the links on the page.
//draw lines for the links
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter().append("line")
.attr("stroke-width", 2);
We also need to update their positions in the tickActions()
function we created earlier.
Say you have a line that links together two nodes. In the tickActions()
we’d instruct one end of the line to follow one node around (the “source” node), and the other end to follow around the other node (the “target” node). The names come from our links data (stored in links_data) - whatever you called it in that applies to the tickActions()
function.
// The complete tickActions() function
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
//update link positions
//simply tells one end of the line to follow one node around
//and the other end of the line to follow the other node around
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; });
}
You’ll be able to see the complete simulation now - nodes and links. Here’s the link to what it should look like.
It’d be nice to be able to drag it round, though. You’ll learn how to do that in the next part!
Hope you found that useful! Click here to view to the rest of the force directed graph series.