Dynamically updating nodes, links in a force layout diagram

3,134 views
Skip to first unread message

Michail Vogiatzis

unread,
Dec 18, 2012, 8:59:55 AM12/18/12
to d3...@googlegroups.com
Hello,

Inspired by (http://bl.ocks.org/1062288), I have used d3 to create a nice force layout from a json file which works fine. However, I want to update it in real-time so I do an AJAX call to a server and I get the new json in my hands which I pass it to my update(json) method using JSON.parse to parse it. I use setInterval to set this update every 5 seconds. 

The problem is that when I update my layout with the new data, the whole diagram behaves as its the first time of run, moving very fast until it "calms down" normally after a 0.5 sec or so. This isn't elegant, especially now that most of the times the data I receive are the same as before.

How should I proceed?
Any help is welcome!

Below is my update method:

function update(newJson) {
console.log('entered update');
  var nodes = flatten(newJson),
      links = d3.layout.tree().links(nodes);

  // Restart the force layout.
  force
      .nodes(nodes)
      .links(links)
      .start();
     
var link = svg.selectAll("line.link")
      .data(links, function(d) { return d.target.id; });

    link.enter().insert("svg:line", ".node")
      .attr("class", "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; });
  link.exit().remove();

  var node = svg.selectAll(".node")
      .data(nodes, function(d) { return d.id; });

//remove previous text
var text = svg.selectAll("text");
text.remove();

     node.enter().append("g")
      .attr("class", "node")
.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
      .on("click", click)
      .call(force.drag);

     node.append("svg:circle")
      .attr("class", "node")
      .attr("cx", 0) 
      .attr("cy", 0) 
      .attr("r", circleRadius)
      .style("fill", color);

      //on hover display user info
     node.append("svg:title")
      .text(hover);

   var text = node.append("text")
      .attr("dx", 12)
      .attr("dy", ".35em")
      .attr("class", textClass)
      .text(textNode);

node.exit().remove();

  force.on("tick", function() {
    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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
  }
 );

}

Many thanks,
Michael

Mike Bostock

unread,
Dec 18, 2012, 11:55:19 AM12/18/12
to d3...@googlegroups.com
A few things to try:

* Don't replace node objects that stay the same across updates (for
example, use a data-join with a key function [1]). If you *do* replace
node objects, you must copy the `x`, `y`, `px` and `py` attributes
from the old node to the replacement node for the position to be
preserved across the update. Otherwise the nodes will be initialized
in a new random position and the graph will destabilize!

* If you don't want to reheat the graph completely, you could set
force.alpha to a value smaller than .1 after calling force.start.
Better yet, use d3.timer to keep the graph hot all the time (via
force.resume), and then the effect of reheating the graph on
force.start will have less of an effect.

This is a simple example of adding and removing a node & associated
links from a force layout:

http://bl.ocks.org/1095795

Mike

[1] http://bost.ocks.org/mike/constancy/

Michail Vogiatzis

unread,
Dec 21, 2012, 6:16:03 AM12/21/12
to d3...@googlegroups.com
Thank you Mike, your suggestions were very helpful.

I now don't replace the nodes but add / remove nodes accordingly.
When I receive the new json, I construct again the new nodes-tree using the 'recurse' method and I assign my own ids depending on the node info (I make sure that for the existing nodes the ids remain the same). Some nodes don't have 'id' json element so I assign node.id to a unique string that they have. I compare the new nodes-tree with the old one and if I find any differences I push/splice to the old nodes-tree which I always use to create the links after. Nodes are added/removed perfectly but the problem I have has to do with links and its the following.

As I don't externally specify how nodes will be connected, I assumed this can be created each time using d3.layout.tree().links(new_nodes_tree). This is what I use to create the links in the first place. To my surprise, when I try to use the same method for the new_nodes_tree, the new links are not connected with the old tree but only with each other. I observed that even when I find the new_node into the new_node_tree, its weight is 0. Should that be zero? Is there any other method of assigning links automatically?


Thanks,
Michael

Michail Vogiatzis

unread,
Dec 21, 2012, 7:34:05 AM12/21/12
to d3...@googlegroups.com
EDIT: Whenever I refer to the 'nodes-tree' I mean the flattened nodes array.

Sorry, my mistake.
Michael

Michail Vogiatzis

unread,
Dec 21, 2012, 10:43:39 AM12/21/12
to d3...@googlegroups.com
It seems that I managed to create the missing links the hard way. Since I don't provide the links explicitly in the json, I noticed that d3.layout.tree().nodes(nodes) creates a node array, same as the 'recurse' method, but also assigns 'parent' object to the children. Having this information, I can push the new links as such: {source: new_node, target: new_node.parent}. Now works as a charm. But I still haven't understood completely why d3.layout.tree().links(new_nodes_array) doesn't create the correct links between the new nodes and the old ones.

Perhaps I should specify the links explicitly in the json, as my code would be more clear and easy to understand.

Cheers,
Michael

Matan Safriel

unread,
Jul 31, 2015, 4:46:28 AM7/31/15
to d3-js, michaelv...@gmail.com
This can work "within" the d3 api rather than coercing it from the outside like that..
Binding the data with a key function, like Mike Bostock suggested, and correctly keeping the selection variable between calls, makes it simply work, at least in my case.
Reply all
Reply to author
Forward
0 new messages