D3's force layout uses physics based rules to position visual elements. This article shows how to use D3's force layout and how to use it to create network visualisations and circle clusters.
D3's force layout uses a physics based simulator for positioning visual elements.
Forces can be set up between elements, for example:
- all elements can be configured to repel one another
- elements can be attracted to center(s) of gravity
- linked elements can be set a fixed distance apart (e.g. for network visualisation)
- elements can be configured to avoid intersecting one another (collision detection)
The force layout allows us to position elements in a way that would be difficult to achieve using other means.
Here's an example of a force layout: we have a number of circles (each of which has a category A
, B
or C
) and we add the following forces:
- all circles attract one another (to clump circles together)
- collision detection (to stop circles overlapping)
- circles are attracted to one of three centers (
A
,B
orC
)
The force layout requires a larger amount of computation (typically requiring a few seconds of time) than other D3 layouts and and the solution is calculated in a step by step (iterative) manner. Usually the positions of the SVG/HTML elements are updated as the simulation iterates, which is why we see the circles jostling into position.
Setting up a force simulation
Broadly speaking there are 4 steps to setting up a force simulation:
- create an array of objects
- call
forceSimulation
, passing in the array of objects - add one or more force functions (e.g.
forceManyBody
,forceCenter
,forceCollide
) to the system - set up a callback function to update the element positions after each tick
Let's start with a minimal example:
var width = 300, height = 300
var nodes = [{}, {}, {}, {}, {}]
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked);
Here we've created a simple array of 5 objects and have added two force functions forceManyBody
and forceCenter
to the system. (The first of these makes the elements repel each other while the second attracts the elements towards a centre point.)
Each time the simulation iterates the function ticked
will be called. This function joins the nodes
array to circle
elements and updates their positions:
function ticked() {
var u = d3.select('svg')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 5)
.attr('cx', function(d) {
return d.x
})
.attr('cy', function(d) {
return d.y
});
}
The power and flexibility of the force simulation is centred around force functions which adjust the position and velocity of elements to achieve a number of effects such as attraction, repulstion and collision detection.
You can define your own force functions but D3 comes with a number of useful ones built in:
forceCenter
(for setting the center of gravity of the system)forceManyBody
(for making elements attract or repel one another)forceCollide
(for preventing elements overlapping)forceX
andforceY
(for attracting elements to a given point)forceLink
(for creating a fixed distance between connected elements)
Force functions are added to the simulation using .force()
where the first argument is a user defined id and the second argument the force function:
simulation.force('charge', d3.forceManyBody())
Let's look at the built-in force functions one by one.
forceCenter
forceCenter
is useful (if not essential) for centering your elements as a whole about a center point. (Without it elements might disappear off the page.)
It can either be initialised with a center position:
d3.forceCenter(100, 100)
or using the configuration functions .x()
and .y()
:
d3.forceCenter().x(100).y(100)
You can add it to the system using:
simulation.force('center', d3.forceCenter(100, 100))
See the next example (forceManyBody
) for an example implementation.
forceManyBody
forceManyBody
causes all elements to attract or repel one another. The strength of the attraction or repulsion can be set using .strength()
where a positive value will cause elements to attract one another while a negative value causes elements to repel each other. The default value is -30
.
simulation.force('charge', d3.forceManyBody().strength(-20))
When creating network diagrams we typically want the elements to repel one another but for visualisations where elements are clumped together, attractive forces are necessary.
forceCollide
forceCollide
is used to stop circular elements overlapping and is particularly useful when 'clumping' circles together.
The radius of the elements is specified by passing an accessor function into forceCollide
's .radius
method. This function's first parameter d
is the joined data from which you can derive the radius.
For example:
var numNodes = 100
var nodes = d3.range(numNodes).map(function(d) {
return {radius: Math.random() * 25}
})
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(5))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius
}))
In this example, forceManyBody
pushes all the nodes together, forceCenter
helps keep the nodes in the center of the container and forceCollide
stops the nodes intersecting.
forceX and forceY
forceX
and forceY
cause elements to be attracted towards specified position(s). We can use a single center for all elements or apply the force on a per-element basis. The strength of attraction can be configured using .strength()
.
For example suppose you have a number of elements, each of which has a property category
that has value 0
, 1
or 2
. You can add a forceX
force function to attract the elements to an x-coordinate of 100
, 300
or 500
based on the element's category:
var xCenter = [100, 300, 500];
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(5))
.force('x', d3.forceX().x(function(d) {
return xCenter[d.category];
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}));
In this example, forceManyBody
pushes all the nodes together, forceX
attracts the nodes to particular x-coordinates and forceCollide
stops the nodes intersecting.
If our data has a numeric dimension we can use forceX
or forceY
to position elements along an axis:
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(5))
.force('x', d3.forceX().x(function(d) {
return xScale(d.value);
}))
.force('y', d3.forceY().y(function(d) {
return 0;
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}));
Use the above with caution because the x position of the elements is not guaranteed to be exact.
forceLink
forceLink
pushes linked elements to be a fixed distance apart. It requires an array of links that specify which elements you'd like to link together. Each link object specifies a source and target element, where the value is the element's array index:
var links = [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 1, target: 6},
{source: 3, target: 4},
{source: 3, target: 7},
{source: 4, target: 5},
{source: 4, target: 7}
]
You can then pass your links array into the forceLink
function using .links()
:
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-100))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('link', d3.forceLink().links(links));
In this example, forceManyBody
pushes the nodes apart, forceCenter
helps keep the nodes centered with the container and forceLink
maintains a constant distance between linked nodes.
The distance and strength of the linked elements can be configured using .distance()
(default value is 30) and .strength()
.