Updating d3.layout.force v3 to d3.forceSimulation v7

Viewed 232

I am trying to update a force-directed graph written using d3js version 3 to d3js version 7.

The following code snippet is the working implementation using d3js v3:

var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height");

graph = {
  nodes: [],
  links: [],
}

var simulation = d3.layout.force()
  .size([width, height])
  .nodes(graph.nodes)
  .links(graph.links)
  .on("tick", function() {
    svg.selectAll('.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 })
      
    svg.selectAll('.node')
      .attr("cx", function (d) { return d.x })
      .attr("cy", function (d) { return d.y })
      .attr("transform", function (d) {
        return "translate(" + d.x + "," + d.y + ")";
      })
  });
  
function update() {
  // update links
  var link = svg.selectAll('.link').data(graph.links);
  link.enter()
    .insert('line', '.node')
    .attr('class', 'link')
    .style('stroke', '#d9d9d9');
  link
    .exit()
    .remove()
    
  // update nodes
  var node = svg.selectAll('.node').data(graph.nodes);
  var g = node.enter()
            .append('g')
            .attr('class', 'node');
  g.append('circle')
    .attr("r", 20)
    .style("fill", "#d9d9d9");
  g.append('text')
    .attr("class", "text")
    .text(function (d) { return d.name });
  node
    .exit()
    .remove();
    
  // update simulation
  simulation
    .linkDistance(100)
    .charge(-200)
    .start();
};

function addNode(node) {
  graph.nodes.push(node);
  update();
};

function connectNodes(source, target) {
  graph.links.push({
    source: source,
    target: target,
  });
  update();
};

addNode({
  id: "you",
  name: "you",
});

let index = 1;

// add a new node every three seconds and connect to 'you'
const interval = window.setInterval(() => {
  let id = Math.random().toString(36).replace('0.','');
  id = id.slice(0,4);
  addNode({
      id: id,
      name: id
  });
  
  connectNodes(0, index);
  index++;
}, 3000);

// no more than 8 nodes
setTimeout(() => {
  clearInterval(interval)
}, 3000 * 8);
<html>
<head>
  <script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
  <svg width="400" height="200"></svg>
</body>
</html>

The following code snippet my attempt of implementing the above code snippet using d3js v7:

var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height");

graph = {
  nodes: [],
  links: [],
}

var simulation = d3.forceSimulation()
  .force("center", d3.forceCenter(width / 2, height / 2).strength(0.01))
  .nodes(graph.nodes)
  .force("link", d3.forceLink(graph.links).distance(100))
  .on("tick", function() {
    svg.selectAll('.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 })
      
    svg.selectAll('.node')
      .attr("cx", function (d) { return d.x })
      .attr("cy", function (d) { return d.y })
      .attr("transform", function (d) {
        return "translate(" + d.x + "," + d.y + ")";
      })
  });
  
function update() {
  // update links
  var link = svg.selectAll('.link').data(graph.links);
  link.enter()
    .insert('line', '.node')
    .attr('class', 'link')
    .style('stroke', '#d9d9d9');
  link
    .exit()
    .remove()
    
  // update nodes
  var node = svg.selectAll('.node').data(graph.nodes);
  var g = node.enter()
            .append('g')
            .attr('class', 'node');
  g.append('circle')
    .attr("r", 20)
    .style("fill", "#d9d9d9");
  g.append('text')
    .attr("class", "text")
    .text(function (d) { return d.name });
  node
    .exit()
    .remove();
    
  // update simulation
  simulation
    .nodes(graph.nodes)
    .force("link", d3.forceLink(graph.links).distance(100))
    .force("charge", d3.forceManyBody().strength(-200))
    .restart()
};

function addNode(node) {
  graph.nodes.push(node);
  update();
};

function connectNodes(source, target) {
  graph.links.push({
    source: source,
    target: target,
  });
  update();
};

addNode({
  id: "you",
  name: "you",
});

let index = 1;

// add a new node every three seconds and connect to 'you'
const interval = window.setInterval(() => {
  let id = Math.random().toString(36).replace('0.','');
  id = id.slice(0,4);
  addNode({
      id: id,
      name: id
  });
  
  connectNodes(0, index);
  index++;
}, 3000);

// no more than 8 nodes
setTimeout(() => {
  clearInterval(interval)
}, 3000 * 8);
<html>
<head>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
  <svg width="400" height="200"></svg>
</body>
</html>

The d3js v7 code snippet does not produce the same results as d3js v3 - why is this? The exact changes I have done are seen in this diff: https://www.diffchecker.com/wdq7AFbU.

Even without adding any connections, there is a difference between the two implementations. The v3 implementation makes the "you" node fly in from random directions, whilst with the v7 implementation the "you" node always flies in from the same direction.

There also seems to be some discrepancy on how the force is being applied since the new nodes in the v7 implementation get stuck in the top-left corner.

1 Answers

I've noticed the attributes of DOMs are reflecting the status alright. It's just that the simulation just stopped prematurely.

In short, the default value of d3.force.alphaDecay is too short for the intended result; alphaDecay dictates the end of simulation. Try expand the value a little bit. The latest default value for alphaDecay is 0.001, according to d3-force github readme. In my testing session, setting the value to 1/5(0.0002) seems to be enough for the same result.

try run the code below. it works fine.

Tips

When working with DOMs and SVGs, try add matching data-ooo tag to see if the d3.selection is working properly. I've added properties of node data such as .index and .target, .source to attributes like data-index,data-id,data-target,data-source... and noticed that everything is in place.

var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height");

graph = {
  nodes: [],
  links: [],
}

var simulation = d3.forceSimulation()
  .force("center", d3.forceCenter(width / 2, height / 2).strength(0.01))
  .nodes(graph.nodes)
  .force("link", d3.forceLink(graph.links).distance(100))
  .on("tick", function() {
    svg.selectAll('.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 })
      
    svg.selectAll('.node')
      .attr("cx", function (d) { return d.x })
      .attr("cy", function (d) { return d.y })
      .attr("transform", function (d) {
        return "translate(" + d.x + "," + d.y + ")";
      })
  }).alphaDecay(0.0002) // just added alpha decay to delay end of execution
  
function update() {
  // update links
  var link = svg.selectAll('.link').data(graph.links);
  link.enter()
    .insert('line', '.node')
    .attr('class', 'link')
    .style('stroke', '#d9d9d9');
  link
    .exit()
    .remove()
    
  // update nodes
  var node = svg.selectAll('.node').data(graph.nodes);
  var g = node.enter()
            .append('g')
            .attr('class', 'node');
  g.append('circle')
    .attr("r", 20)
    .style("fill", "#d9d9d9");
  g.append('text')
    .attr("class", "text")
    .text(function (d) { return d.name });
  node
    .exit()
    .remove();
    
  // update simulation
  simulation
    .nodes(graph.nodes)
    .force("link", d3.forceLink(graph.links).distance(100))
    .force("charge", d3.forceManyBody().strength(-200))
    .restart()
};

function addNode(node) {
  graph.nodes.push(node);
  update();
};

function connectNodes(source, target) {
  graph.links.push({
    source: source,
    target: target,
  });
  update();
};

addNode({
  id: "you",
  name: "you",
});

let index = 1;

// add a new node every three seconds and connect to 'you'
const interval = window.setInterval(() => {
  let id = Math.random().toString(36).replace('0.','');
  id = id.slice(0,4);
  addNode({
      id: id,
      name: id
  });
  
  connectNodes(0, index);
  index++;
}, 3000);

// no more than 8 nodes
setTimeout(() => {
  clearInterval(interval)
}, 3000 * 8);
<html>
<head>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
  <svg width="400" height="200"></svg>
</body>
</html>

Edit: What is alpha and alphaDecay?

doing simulation.restart().alpha(0.3) seem to give the same effect which was mentioned in an answer to my earlier post. Is there any difference between the two?

d3-force github readme says alpha stands for entropy. In easy words, alpha stands for the life of simulation; alpha=1 stands for start and alpha=0 stands for the end.

https://github.com/d3/d3-force#simulation_alpha

alpha is roughly analogous to temperature in simulated annealing. It decreases over time as the simulation “cools down”. When alpha reaches alphaMin, the simulation stops

here's a simple pseudocode that illustrates the idea.

alpha = 1
alphaDecay = 0.002
function tick() {
  alpha = alpha - alphaDecay
}

loop {
  tick()
  if alpha equals to 0 then end simulation
}

the previous answer mentioned in the comment increased alpha when restart because he wanted to give simulation more time after a reset.

in my answer, I've set alphaDecay to a lower number so that the simulation can work for a longer period of time.

  • increasing alphaDecay/decreasing alpha = simulation ends quicker
  • decreasing alphaDecay/increasing alpha = simulation ends later

Edit: Changes in d3-force since D3 v4?

Also, there is still some difference between the v3 and v7 implementation; 1) the collisions in v3 is more elastic and 2) the new nodes being added come in from random directions. Do you know what could be fixed to get 1) and 2) in the v7 implementation?

please read this d3-force v1 github changelog; d3-force became a separate package since d3 v4 and this changelog explains the changes.

1. d3-force has become more accurate.

The changelog mentions many improvements:

The force simulation now uses velocity Verlet integration rather than position Verlet, tracking the nodes’ positions (node.x, node.y) and velocities (node.vx, node.vy) rather than their previous positions (node.px, node.py). The new link force replaces force.linkStrength and employs better default heuristics to improve stability.

The physics integration of d3-force has improved for better accuracy. This is why it looks different from v3 implementation.

Although it is possible to tune the simulation look like in a specific way but what does more elastic mean? Does it mean stronger reaction force? or does it mean faster animation(but in same amount of time)? it surely can be tuned in, only if the request was more detailed. And every d3 package has surprisingly simple structure and formulas. It is possible to look inside and change its inner function.

2. manipulating positions of nodes

https://github.com/d3/d3-force#simulation_nodes

manipulate .x and .y of nodes during the simulation to change their positions.

addNode({ id: /* ... */, x: 0, y: 100}) // like this

edit: there were some typos in my answer about the increase-decrease relation of alpha and time.

Related