Getting started with D3.js force simulations

Bryony Miles
4 min readSep 18, 2017

--

This article talks you through the decisions made developing 2 force simulations.

Family Tree
SEO Tree

They look and behave very differently but the core logic is the same.

Step 1 — The Basics

Force simulations are very different to other d3 charts , so if this is your first time I’d recommend a quick look at Shirley Wu and her tip to watch Jim Vallandingham’s Abusing the Force.

Step 2 — The Data

All network charts have two key data elements, nodes (the circles) and links (the lines — sometimes called edges).

Nodes : nodes must have unique ids:

var nodes = [{"id":'f1'},{"id":'f2'} ...... {"id":'f99'}]

then you can add as many custom variables as you like:

{"type":'person',"id":'f99',"name":"Santa's Little Helper","age": 2,"sex":'m',"image": "santa.png"}

Links: Links must have a valid node id as a source and a target

var links = [{source:'f1',target:'f2'}]

These can be text or numbers. You can add other variables if you want.

Step 3— Basic Code Structure

Assuming you’ve drawn an svg with a stored width and height there are 5 key stages. (for more info on the update process, click here)

a) the simulation.

This is basic functionality.

center — pulls all nodes to the center
charge — nodes repel from each other which prevents overlap
link — specifies that id is the link variable

//define and stop the simulation
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter(width/2, height/2))
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink().id(function(d => d.id)
simulation.stop()

b) define links group and set link properties (position comes later):

//define links group
var my_group = svg.selectAll(".link_group")
.data(links)
//exit, remove
my_group.exit().remove()
//enter
var enter = my_group.enter()
.append("g").attr("class","link_group")
//append
enter.append("line").attr("class","link_line")
//merge
my_group = my_group.merge(enter)
my_group.select("link_line")
.attr("stroke", "orange")

c) nodes

define nodes group and set properties.

//define nodes group
var my_group = svg.selectAll(".node_group")
.data(nodes)
//exit, remove
my_group.exit().remove()
//enter
var enter = my_group.enter()
.append("g").attr("class","node_group")
//append
enter.append("circle").attr("class","node_circle")
//merge
my_group = my_group.merge(enter)
my_group.select("node_circle")
.attr("fill", "orange")
.attr("r",10)

c) append the data to the simulation

simulation.nodes(nodes)
simulation.force("link").links(links)

c) and finally, define the tick functionality and restart:

simulation.on("tick",function(d){
//position links
d3.selectAll(".link_line")
.attr("x1", d => d.source.x)
.attr("x2", d => d.target.x)
.attr("y1", d => d.source.y)
.attr("y2", d => d.target.y)

//position nodes
d3.selectAll(".node_circle")
.attr("x", d => d.x)
.attr("y", d => d.y)
})
simulation.alpha(1).restart()

Step 4 — Tweaking the simulation to fit

Now you’ve got the basics when a brief comes in you need to tailor the simulation to suit.

FAMILY TREE

A node for each family member

A link for each of their family links — parent or child.

Nodes cluster around a central smaller ‘family node’.

Here’s the simulation:

var simulation = d3.forceSimulation()
.force("x",d3.forceX(width/2).strength(0.4))
.force("y",d3.forceY(height/2).strength(0.6))
.force("charge",d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(d => d.id ))
.force("collide",d3.forceCollide().radius(d => d.r * 10))

x and y — pull nodes towards the centre with y stronger so nodes fill the landscape screen better.
charge — nodes repel from each other which prevents overlap (default strength is -30 so I’ve made this a bit stronger)
link — specify that id is the link variable
collide-specify a ‘repel radius’ of 10 x node radius — to prevent overlap and leave space for label

HIERARCHICAL TREE

Three tiers of hierarchical data — Title , Category and Sub-Category .

The client wanted the tiers to fan out in the appearance of a tree.

Node behaviour depended on the hierarchical tier.

Tier 1 — Title
nodes fixed to a set position around the ‘trunk’

Tier 2 — Category

here’s the simulation:

var simulation = d3.forceSimulation()
.force('x', d3.forceX().x(d => d.x_position))
.force('y', d3.forceY().y(d => d.y_position))
.force("charge",d3.forceManyBody())
.force("link", d3.forceLink().id(d =>
d.id).distance(20))
.force("collide",d3.forceCollide().radius(d => d.r*10))

x and y — pulls all nodes to the center (gentler than force centre)
charge — nodes repel from each other which prevents overlap
link — specify that id is the link variable — distance added to keep them close
collide-specify a ‘repel radius’ of 10 x node radius — to prevent overlap and leave space for labels

Tier 3

The simulation here is the same as the Tier 2 simulation

EXCEPT

x and y are the co-ordinates of the Tier 2 level node.

var popup_simulation = d3.forceSimulation()
.force('x', d3.forceX().x(my_x))
.force('y', d3.forceY().y(my_y))
.force("charge",d3.forceManyBody())
.force("link", d3.forceLink().id(d =>
d.id).distance(70))
.force("collide",d3.forceCollide().radius(d => d.r*10))

Step 5 — Adding and Optimising

Once you’ve got the core simulation running correctly you can start:

  • adding other data specific elements — for example, images, fills, strokes, stroke width.
  • altering the individual properties to optimise performance — more information here.

Step 6 — Conclusions

This is only a start.

A quick Google or Observable search will show you many different ways people have used force simulations to fit their specific needs.

This is a great tool for starters — recommended by Mike Bostock of course!

--

--