D3.js forceSimulation Node Interaction

Explains interaction between nodes without links, or edges, in D3.js forceSimulation.

Example program

The nodes, shown as circles, can be dragged.

View code

Example code

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v7 force simulation node detail</title>
</head>

<body>
  <svg width="800" height="550"></svg>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    // 1. Prepare the data to draw
    var nodesData = [];
    for (var i = 0; i < 50; i++) {
      nodesData.push({
        "x": 800 * Math.random(),
        "y": 600 * Math.random(),
        "r": 30 * Math.random() + 5
      });
    }

    // 2. Add SVG elements
    var node = d3.select("svg")
      .selectAll("circle")
      .data(nodesData)
      .enter()
      .append("circle")
      .attr("r", function (d) { return d.r })
      .attr("fill", "LightSalmon")
      .attr("stroke", "black")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    // 3. Configure forceSimulation
    var simulation = d3.forceSimulation()
      // .force("link", d3.forceLink()) // Not needed here
      .force("collide",
        d3.forceCollide()
          .radius(function (d) { return d.r })
          .strength(1.0)
          .iterations(16))
      .force("charge", d3.forceManyBody().strength(5))
      .force("x", d3.forceX().strength(0.1).x(400))
      .force("y", d3.forceY().strength(0.1).y(300));
    // .force("center", d3.forceCenter(300, 200)); // Not needed here

    simulation
      .nodes(nodesData)    // Register node data with the simulation
      .on("tick", ticked); // Register the function called on each calculation update

    // 4. forceSimulation drawing update function
    function ticked() {
      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>

Explanation

1. Prepare the data to draw

  var nodesData = [];
  for(var i = 0; i < 50; i++) {
    nodesData.push({
      "x": 800 * Math.random(),
      "y": 600 * Math.random(),
      "r": 30 * Math.random() + 5
    });
  }

First, prepare the node data, nodesData. forceSimulation updates the x and y coordinates in nodesData, but if x and y are defined first, they are used as the initial positions. The radius is defined as the variable r so it can vary by node.

2. Add SVG elements

var node = d3.select("svg")
  .selectAll("circle")
  .data(nodesData)
  .enter()
  .append("circle")
  .attr("r", function(d) { return d.r })
  .attr("fill", "LightSalmon")
  .attr("stroke", "black")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

Add the SVG elements for nodes. The event functions called during dragging are registered with call(...).

Each node receives a different random radius as follows.

.attr("r", function(d) { return d.r })

3. Configure forceSimulation

var simulation = d3.forceSimulation()
  // .force("link", d3.forceLink()) // Not needed here
  .force("collide",
    d3.forceCollide()
    .radius(function(d) { return d.r })
    .strength(1.0)
    .iterations(16))
  .force("charge", d3.forceManyBody().strength(5))
  .force("x", d3.forceX(400).strength(0.1))
  .force("y", d3.forceY(300).strength(0.1));
// .force("center", d3.forceCenter(400, 300)); // Not needed here

simulation
  .nodes(nodesData)    // Register node data with the simulation
  .on("tick", ticked); // Register the function called on each calculation update

The following interactions can be configured in forceSimulation.

Setting Description
"link" The force from links connecting nodes. Omitted here.
"collide" Contact repulsion between nodes.
"charge" Coulomb force, or non-contact force, between nodes.
"x", "y" Position-based field forces.
"center" Center of mass for all nodes.
"r" Radial force.

The non-link parts are described in more detail below.

“collide”: contact repulsion between nodes

.force("collide",
  d3.forceCollide()
  .radius(function(d) { return d.r })
  .strength(1.0)
  .iterations(16))
Function Description
radius Sets the radius of the nodes to simulate.
The default is 1.
Here, function(d) { return d.r; } is set so the radius r defined in nodesData is assigned.
Only the radius variable is shown as a function here, but all other parameters can also be set with functions.
strength The repulsive force between overlapping nodes.
Set it as a decimal from 0.0 to 1.0.
The default is 0.7.
iterations The number of simulation iterations.
Increasing the number of iterations makes the calculation much more stable and makes overlaps easier to avoid, but increases calculation time.
The default is 1.

“charge”: Coulomb force between nodes, a non-contact force

.force("charge", d3.forceManyBody().strength(5))
Function Description
strength Positive values attract nodes like gravity, while negative values repel nodes like static electricity.
The magnitude controls the strength of the force.
The default is -30.
theta Integer that determines approximation precision.
Calculating Coulomb force for every pair of particles is expensive, so distant groups of nodes are calculated as groups using Barnes-Hut approximation for speed.
The default is 0.9. It is not set in this example.
distanceMin Minimum distance used to calculate Coulomb force.
This avoids infinite force when two overlapping nodes have distance 0.
The default is 1. It is not set in this example.
distanceMax Sets the maximum distance between nodes.
If unspecified, returns the current maximum distance.
The default is infinity.
Specifying a maximum distance improves performance. It is not set in this example.

“x” and “y”: position-based field forces

.force("x", d3.forceX().strength(0.1).x(400))
.force("y", d3.forceY().strength(0.1).y(300))
Function Description
strength A coefficient that determines how much the node returns toward the specified position in one calculation step.
With 0.1, the node moves 10% toward the specified position in each calculation step.
A value from 0.0 to 1.0 is recommended, and the default is 0.1.
x The center x coordinate of the field force. The default is 0.
y The center y coordinate of the field force. The default is 0.

“center”: center of mass for all nodes

// .force("center", d3.forceCenter(300, 200)); // Not needed here
Function Description
d3.forceCenter(x, y) The center of mass coordinate for all nodes.
It helps keep the drawing in the center of the viewport.
Unlike other forces, it does not change the relative positions between nodes.

4. forceSimulation drawing update function

function ticked() {
  node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
}

This function is called at each simulation step. It reflects the calculation result in the SVG element position so the SVG elements move.

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;
}

These are the event functions used during dragging. If fx and fy are defined on the node data, that node’s coordinates are fixed. To link movement with the mouse while dragging, the dragged element’s position is fixed when dragging starts, 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 simulation is not active when dragging starts, it is restarted. The alphaTarget set at that time is a coefficient that makes simulation transitions smooth. A value from 0 to 1 can be set; lower values produce smoother movement.