Circle Packing with d3-force

Dataviz logo representing a CircularPacking chart.

This tutorial is a variation around the general introduction to circle packing with react and d3.js. You should probably understand the concepts described there before reading here.

Instead of relying on the pack() function of d3.js to compute the best node positions, this example suggests to rely on the d3-force plugin to apply physical forces on each node.

A code sandbox is provided for the final result, but explanations target what's different compared to a classic circular packing based on some concepts described in the network diagram section.

Useful links

Plot and code

Here is the final plot we're trying to achieve here, together with its code:🙇‍♂️

It is a circular packing chart where all circles represent an item of the dataset.

  • The circle area is proportional to the value property of the data item.
  • All circles are close to each other but do not collide. They are also attracted by the y=0 horizontal axis, what aligns them horizontally

To understand how this chart works, you need the concepts described in the Network diagram and Circle pack sections.

A circle packing chart made using the d3-force plugin, with bubbles being attracted by the y=0 baseline.

Using d3-force

This example is actually a variation of a network diagram, but with no links between nodes.

Some physical forces are applied to each node to compute their position through an iterative simulation:

d3.forceSimulation(nodes)
  .force(
    'collide',
    d3.forceCollide().radius((node) => sizeScale(node.value) + 1)
  )
  .force('charge', d3.forceManyBody().strength(80))
  .force('center', d3.forceCenter(width / 2, height / 2))
  .force('charge', d3.forceY(0).strength(0.05))

Here is a reminder:

  • collide avoids circle overlap. It uses each node radius to make sure there is no collision.
  • manyBody makes sure that nodes are attracted one to each other since the strength in use is positive.
  • forceCenter center the whole chart on the canvas.
  • forceY aligns bubble on a horizontal line.

Bubble Size

As explained in the bubble chart section, it is very important to have the bubble area being proportional to the numericvalue of the data point.

It is a very common mistake to make the radius proportional to numeric value, which leads to a misleading visualization.

Fortunately, it is very straightforward to scale the bubble appropriately thanks to the scaleSqrt() function.

const sizeScale = scaleSqrt()
  .domain([min, max])
  .range([BUBBLE_MIN_SIZE, BUBBLE_MAX_SIZE]);

Flow

Contact

👋 Hey, I'm Yan and I'm currently working on this project!

Feedback is welcome ❤️. You can fill an issue on Github, drop me a message on Twitter, or even send me an email pasting yan.holtz.data with gmail.com. You can also subscribe to the newsletter to know when I publish more content!