D3 in Depth covers versions 6 and 7 of D3

Home About Newsletter
NEWSLETTER

Get book discounts and receive D3.js related news and tips.

Subscribe

Force layout

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 or C)

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 and forceY (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 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().

BOOKS

Learn how to make a custom data visualisation using D3.js.

Find out more

Learn the fundamentals of HTML, SVG, CSS and JavaScript for building data visualisations on the web.

Find out more