No Time Dad

A blog about web development written by a busy dad

Basic D3 forceSimulation TypeScript Example

My goal is to use D3 to create an svg element that contains three circles that load in the center of the svg and slowly move outwards. Which on the surface seems like an easy goal top accomplish. But as someone who’s still relatively new to D3, I found it challenging. Maybe more challenging than it should be.

I also wanted to use TypeScript instead of vanilla JavaScript to accomplish this. Which adds an extra layer of complexity in writing the code but I think it also makes the code easier to understand and maintain. Not to mention more reliable. This example will use the D3 TypeScript evironment I wrote about previously.

// index.ts
import { select } from "d3-selection"
import { forceSimulation, forceManyBody, forceCenter } from "d3-force"

type Circle = {
  x?: number
  y?: number
}

export const exampleForce = () => {
  const data: Circle[] = [{}, {}, {}]
  const height = 300
  const width = 300
  const svg = select("#myDiv")
    .append("svg")
    .attr("width", width)
    .attr("height", width)

  svg
    .selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .style("fill", "royalblue")
    .attr("r", 15)

  forceSimulation(data)
    .force("charge", forceManyBody())
    .force("center", forceCenter(width / 2, height / 2))
    .on("tick", () => {
      svg
        .selectAll("circle")
        .data(data)
        .attr("cx", d => (d.x ? Number(d.x) : 0))
        .attr("cy", d => (d.y ? Number(d.y) : 0))
    })
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="../dist/index.js"></script>
    <title>Simple TS Force Example</title>
  </head>
  <body>
    <div id="myDiv"></div>
  </body>
  <script>
    graph_tools.exampleForce()
  </script>
</html>
// package.json
...
"devDependencies": {
  ...
  "@types/d3-force": "^3.0.3",
  "@types/d3-selection": "^3.0.2",
  "typescript": "^4.6.2"
  },
  "dependencies": {
    "d3-force": "^3.0.0",
    "d3-selection": "^3.0.0"
  }

Getting started

The thing about D3 is there are some amazing example visualizations out there, and it’s easy to get overwhelmed by the code used to create them. In fact, even simple D3 examples can sometimes have complex code. Lately I’ve been trying hard to just create D3 visuals one step at a time, confirming that what I’m expecing to happen in the DOM is actually happening.

In this D3 forceSimulation example, I started by just making sure that the svg was being created inside my html element on the page. The code below should an svg inside my div with the id myDiv.

const svg = select("#myDiv")
  .append("svg")
  .attr("width", width)
  .attr("height", width)

Which should create the following html:

<div id="myDiv">
  <svg width="300" height="300"></svg>
</div>

The next thing to do is to create the circle elements inside my existing svg element. For this example I want three circles. Which is where things start to get interesting with TypeScript. Eventually I’m going to pass data to the D3 forceSimulation function, and it’ll be expecting x & y values to modify. But, when I first create the circles on the page it doesn’t matter what the x & y coordinates are because D3 is going to handle placing them in the center of the page for me.

In order to properly type my circles data, I created a new type that has optional x and y properties. This allows me to initiate three empty objects that’ll represent my circles inside the svg, and it also matches the type that the D3 forceSimulation method is expecting. If I tried to pass the data array to the forceSimulation function without a type I’d immediately get an error from the TypeScript compiler.

type Circle = {
  x?: number
  y?: number
}

Next I can add the circles to my svg element by using the svg variable and using D3’s data-join pattern. Again, I’m not adding x and y coordinates to the circle elements because I want D3 to do it for me later, so these newly added elements won’t be visable on the page itself, but I can see them in the DOM. The rendered html should look something similar to the below.

<div id="myDiv">
  <svg width="300" height="300">
    <circle r="15" style="fill: royalblue;"></circle>
    <circle r="15" style="fill: royalblue;"></circle>
    <circle r="15" style="fill: royalblue;"></circle>
  </svg>
</div>

Working with D3 force

Now that the elements are added to the DOM I can focus on letting D3 position and move the circles. The main idea being that all three circles should start in the center and move outward or repel from each other.

forceSimulation(data)
  .force("charge", forceManyBody())
  .force("center", forceCenter(width / 2, height / 2))
  .on("tick", () => {
    svg
      .selectAll("circle")
      .data(data)
      .attr("cx", d => (d.x ? Number(d.x) : 0))
      .attr("cy", d => (d.y ? Number(d.y) : 0))
  })

The forceSimulation method itself is what initializes a D3 simulation to run on the page. Here I’m calling it directly instead of assigning it to a variable, so the simulation will run on each page load.

forceSimulation(data)
  ...

Next I’ll define the force charge, which is the “gravity” that’ll be applied to each circle. I’ll apply this force with D3’s forceManyBody() function which’ll apply the gravity evenly to each circle.

forceSimulation(data)
  .force("charge", forceManyBody())
  ...

After that, I define the “center” of the D3 forceSimulation using the D3 forceCenter helper method. To that method I pass what I think is the center point of my svg element based on its size.

forceSimulation(data)
  .force("charge", forceManyBody())
  .force("center", forceCenter(width / 2, height / 2))
  ...

The last piece of the puzzle is the tick. Which is still slightly mysterious to me, but I mostly understand it as an event on a page. In this case the “tick” is the page loading. The tick could also be a click, drag, or some other manually defined event.

When the tick event happens I select all of the circle elements on the page, join my data, and add x & y coordinates to the circles. These coordinates will be updated by D3 at microsecond intervals, which is what allows them to move out from the center of the svg element.

forceSimulation(data)
  .force("charge", forceManyBody())
  .force("center", forceCenter(width / 2, height / 2))
  .on("tick", () => {
    svg
      .selectAll("circle")
      .data(data)
      .attr("cx", d => (d.x ? Number(d.x) : 0))
      .attr("cy", d => (d.y ? Number(d.y) : 0))
  })

It’s worth mentioning that since my Circle type has optional x & y properties, the TypeScript compiler requires me to check for undefined values. If the value does happen to be undefined (which it won’t) I just set the coordinate to zero, forcing the circle off the page.

Getting here

How did I figure all of this out? An enormous amount of trial and error. I think the code is nice and succinct, though. It also feels less brittle since it’s written using TypeScript. And I think it’s easier to read than a lot of the example D3 visuals I’ve seen online. Not to toot my own horn, but I just think using TypeScript helps with that a lot.

I played around with these three circles for hours before I got it to work how I expected it to. The circle elements were all of the page, sometimes now even in the svg element. The D3 forceSimulation was a huge headache to figure out, and I still am not sure if how I’ve written it is optimal. I guess that might be part of working with D3. It’s progress anyways.