Basic D3.js forceSimulation Structure

Introduces the simplest possible D3.js forceSimulation program structure.

Example program

The nodes, shown as circles, can be dragged.

View code

D3.js forceSimulation calculates node positions while considering spring forces and other forces. Before v3, this was defined as the force layout, but the same functions can no longer be used, so code must be rewritten for later versions. Several parameters can be configured, but this page explains the minimum demo and code needed to start using it immediately.

Example code

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v5 force simulation</title>
</head>

<body>
  <svg width="400" height="300"></svg>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    // 1. Prepare the data to draw
    var nodesData = [
      {},
      {},
      {},
      {},
      {},
      {}
    ]

    var linksData = [
      { "source": 0, "target": 1 },
      { "source": 1, "target": 4 },
      { "source": 2, "target": 3 },
      { "source": 2, "target": 5 },
      { "source": 5, "target": 1 }
    ]

    // 2. Add SVG elements
    var link = d3.select("svg")
      .selectAll("line")
      .data(linksData)
      .enter()
      .append("line")
      .attr("stroke-width", 1)
      .attr("stroke", "black");

    var node = d3.select("svg")
      .selectAll("circle")
      .data(nodesData)
      .enter()
      .append("circle")
      .attr("r", 7)
      .attr("fill", "LightSalmon")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    // 3. Configure forceSimulation
    var simulation = d3.forceSimulation()
      .force("link", d3.forceLink())
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(200, 150));

    simulation
      .nodes(nodesData)
      .on("tick", ticked);

    simulation.force("link")
      .links(linksData);

    // 4. forceSimulation drawing update function
    function ticked() {
      link
        .attr("x1", function (d) { return d.source.x; })
        .attr("y1", function (d) { return d.source.y; })
        .attr("x2", function (d) { return d.target.x; })
        .attr("y2", function (d) { return d.target.y; });
      node
        .attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; });
    }

    // 5. Drag event functions
    function dragstarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    function dragended(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
  </script>
</body>

</html>

Code explanation

1. Prepare the data to draw

var nodesData = [
  {},
  {},
  {},
  {},
  {},
  {}
]

var linksData = [
  { "source": 0, "target": 1 },
  { "source": 1, "target": 4 },
  { "source": 2, "target": 3 },
  { "source": 2, "target": 5 },
  { "source": 5, "target": 1 }
]

nodesData is the node data. If you only need to draw nodes, an array of empty objects is enough. linksData is the link data and requires the IDs of the two connected nodes, source and target.

2. Add SVG elements

var link = d3.select("svg")
  .selectAll("line")
  .data(linksData)
  .enter()
  .append("line")
  .attr("stroke-width", 1)
  .attr("stroke", "black");

var node = d3.select("svg")
  .selectAll("circle")
  .data(nodesData)
  .enter()
  .append("circle")
  .attr("r", 7)
  .attr("fill", "LightSalmon")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

Add SVG elements for links and nodes. The event functions for dragging are registered with call(), which makes the nodes draggable.

3. Configure forceSimulation

This is where forceSimulation is configured.

Enable spring force from links.

d3.forceSimulation().force("link", d3.forceLink())

Enable Coulomb force between nodes. By default, a repulsive force is configured. forceSimulation still works if this line is omitted.

.force("charge", d3.forceManyBody())

Set the center position for all nodes. It also works without this setting, but without it the layout behaves like it is in zero gravity, so elements that move outside the screen may not come back.

.force("center", d3.forceCenter(200, 150));

Register the node data array with the simulation, and register the function called on each calculation update with .on('tick', ...). Calculation results are written back into the node data array, so the results must be reflected in SVG element positions to move the SVG elements.

  simulation
    .nodes(nodesData)
    .on("tick", ticked);

Register the link data array with the simulation.

  simulation.force("link")
    .links(linksData);

4. forceSimulation drawing update function

The following function is called on each calculation update. It reflects the calculation results in the SVG element positions. The source and target values in the link data are no longer the original numbers; they become references to the node data array.

function ticked() {
  link
    .attr("x1", function(d) { return d.source.x; })
    .attr("y1", function(d) { return d.source.y; })
    .attr("x2", function(d) { return d.target.x; })
    .attr("y2", function(d) { return d.target.y; });
  node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
}

5. Drag event functions

These are the event functions used while dragging. The value stored in d is the registered node data. If fx and fy are defined on the node data, that node’s coordinates are fixed. To make the node follow the mouse while dragging, the dragged element’s position is fixed when dragging starts, the mouse coordinates (event.x, event.y) are reflected while dragging, and the fixed position is released by assigning null when dragging ends.

Also, the simulation stops over time, so if the simulation is not active when dragging starts, it is restarted. The alphaTarget set at that point is a coefficient introduced in v4 to make simulation transitions smoother. In v3, nodes appear to jump when restarted. A value from 0 to 1 can be set; lower values are smoother, but if it is set to 0, nodes do not move at all when restarted.

function dragstarted(event, d) {
  if (!event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(event, d) {
  d.fx = event.x;
  d.fy = event.y;
}

function dragended(event, d) {
  if (!event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}