D3js collision stops working after a while

Viewed 127

I am trying to dynamically update nodes on a d3js force chart, but the collision stops working at some point. It seems the issue is caused by a node which gets placed exactly at the center of the svg canvas, but I can't figure out why. Collision should be able to solve this, right? Any ideas?

I'll attach a minimal reproduction

var width = 400,
  height = 200;

var svg = d3.select("svg");

svg.attr("viewBox", [-width / 2, -height / 2, width, height]);

var nodes = [{
  radius: 20
}];

var simulation = d3
  .forceSimulation(nodes)
  .force("charge", d3.forceManyBody().strength(-50))
  .force(
    "collision",
    d3.forceCollide().radius(function(d) {
      return d.radius;
    })
  )
  .on("tick", ticked);

function ticked() {
  var u = d3.select("svg").selectAll("circle").data(nodes);

  u.enter()
    .append("circle")
    .attr("r", function(d) {
      return d.radius;
    })
    .merge(u)
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    });

  u.exit().remove();
}

function update() {
  nodes.push({
    radius: Math.random() * (20 - 5) + 5
  });
  node = svg.selectAll("circle").data(nodes);

  node
    .enter()
    .append("circle")
    .attr("r", (d) => d.radius);

  simulation.nodes(nodes);
}

setInterval(update, 100);
svg {
  background: blue
}

circle {
  fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="200" />

2 Answers

Your simulation cools down and then stops, at which point the tick function is no longer called. This is the default behavior. However, you can change this by modifying the alpha decay (simulation.alphaDecay()).

The alpha decay rate determines how quickly the current alpha interpolates towards the desired target alpha; since the default target alpha is zero, by default this controls how quickly the simulation cools. Higher decay rates cause the simulation to stabilize more quickly, but risk getting stuck in a local minimum; lower values cause the simulation to take longer to run, but typically converge on a better layout. To have the simulation run forever at the current alpha, set the decay rate to zero... (docs)

The default settings for alpha, alpha decay, and alpha min allow the simulation to run about 300 iterations, over this time the simulation cools down and eventually comes to a stop.

If we set alpha decay to zero, the simulation continues forever. However you might want to dial down the forces as the simulation remains hot and/or remove nodes that are no longer visible (as the simulation still tracks them). I've done neither in my demo below:

var width = 400,
  height = 200;

var svg = d3.select("svg");

svg.attr("viewBox", [-width / 2, -height / 2, width, height]);

var nodes = [{
  radius: 20
}];

var simulation = d3
  .forceSimulation(nodes)
  .force("charge", d3.forceManyBody().strength(-50))
  .alphaDecay(0)
  .force(
    "collision",
    d3.forceCollide().radius(function(d) {
      return d.radius;
    })
  )
  .on("tick", ticked);

function ticked() {
  var u = d3.select("svg").selectAll("circle").data(nodes);

  u.enter()
    .append("circle")
    .attr("r", function(d) {
      return d.radius;
    })
    .merge(u)
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    });

  u.exit().remove();
}

function update() {
  nodes.push({
    radius: Math.random() * (20 - 5) + 5
  });
  node = svg.selectAll("circle").data(nodes);

  node
    .enter()
    .append("circle")
    .attr("r", (d) => d.radius);

  simulation.nodes(nodes);
}

setInterval(update, 100);
svg {
  background: blue
}

circle {
  fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="200" />

You can also manipulate the alpha value directly with simulation.alpha(), which is another way to reheat the simulation if you want to do so when a new node is entered, for example.

I forgot to restart the simulation after the nodes array is updated:

simulation.nodes(nodes).restart();
Related