An introduction to d3-force for Svelte Visualizations
An introduction to `d3-force`
We begin by drawing circles, just like in our last lesson; but this time, we'll use a physics-based force layout to arrange the circles.
This means that rather than apply some fixed x and y position to each circle, we'll let a physics engine determine the position of each circle. In particular, we'll be using d3-force
which will do the following:
This module implements a velocity Verlet numerical integrator for simulating physical forces on particles. The simulation is simplified: it assumes a constant unit time step Δt = 1 for each step, and a constant unit mass m = 1 for all particles. As a result, a force F acting on a particle is equivalent to a constant acceleration a over the time interval Δt, and can be simulated simply by adding to the particle’s velocity, which is then added to the particle’s position.
Don't have any idea what's going on? No problem. D3 is going to handle a lot of the "behind the scenes" math and physics for us, and we'll convert that into points on a canvas.
A force simulation primer#
This module is somewhat advanced and it wouldn't hurt to spend a bit of time with d3-force
before diving in. Here's a great introduction from Ben Tanen.
The basic component of use here will be a force simulation. Here's an example of force simulation:
xxxxxxxxxx
import { forceSimulation, forceY, forceX, forceCollide } from "d3-force";
​
const simulation = forceSimulation(nodes)
.force("x", forceX().x(d => d.x).strength(0.02))
.force("y", forceY().y(d => d.y).strength(0.02))
Here, nodes
would be an array of objects, each containing at least an x
and y
property. (Those are needed because they are referenced in the force*
functions.)
We can apply any number of forces, where each represents a new interaction with the overall simulation. For example, if we omitted forceX
, our simulation would only attract toward the vertical axis.
We could also add others. For example, we could add a forceCollide
to make sure that the circles don't overlap. In the example below (a barebones version of our final chart), click the button to see what something like forceCollide
does:
In our chart, we're going to want three forces: forceX
, forceY
, and forceCollide
. Here's a brief description of each:
forceX
: attracts nodes towards the horizontal axis toward a given position,x
.forceY
: attracts nodes towards the vertical axis toward a given position,y
.forceCollide
: makes sure that no two nodes overlap.
Let's begin by importing our data and defining which columns we want to map to the forceX
and forceY
forces.
data.js
will look like this:
xxxxxxxxxx
export default [
{
country: "Afghanistan",
happiness: 2.404,
continent: "Asia"
},
{
country: "Lebanon",
happiness: 2.955,
continent: "Asia"
},
{
country: "Zimbabwe",
happiness: 2.995,
continent: "Africa"
},
{
country: "Rwanda",
happiness: 3.268,
continent: "Africa"
},
// ...
];
...where each country is represented by an object.
In total, there is one object for every country in the world. Each object includes its name (country
), its happiness score (happiness
), and its continent (continent
).
You can download the complete file here or see it in CSV form here.
In App.svelte
, we can import our data and console.log
to verify that everything is working.
xxxxxxxxxx
<script>
import data from "$data/data.js";
console.log(data)
</script>
Within our chart, each circle's attributes will be determined in the following way:
Each circle's radius and x position will be determined by its
happiness
scoreEach circle's color and y position will be determined by its
continent
As it relates to our force layout:
Map the
happiness
column to theforceX
force.When grouped by continent, map the
happiness
column to theforceY
force.Apply a
forceCollide
to make sure that no two circles overlap.
Let's do it!
xxxxxxxxxx
<script>
import data from "$data/data.js";
import { forceSimulation, forceX, forceY, forceCollide } from "d3-force";
​
const RADIUS = 5;
const simulation = forceSimulation(data)
.force("x", forceX().x(d => d.happiness).strength(0.8))
.force("y", forceY().y(d => d.continent).strength(0.2))
.force("collide", forceCollide().radius(RADIUS);
</script>
The strength
parameter of each force is a multiplier for the force's effect on the simulation. You can set strength
to any number between 0 and 1 to make it more or less powerful.
If you console.log
the simulation
, you'll see it's an object with multiple properties.

Some of these properties allow us to adjust how our simulation behaves. For example, we can adjust the speed at which the bubbles move using simulation.alphaDecay
. We can stop the simulation entirely using simulation.stop()
, or we can start it again using simulation.restart()
.
This lesson preview is part of the Better Data Visualizations with Svelte course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Better Data Visualizations with Svelte, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
