Getting started with D3.js force simulations

Bryony Miles
Sep 18, 2017 · 4 min read

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!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade