Skip to content Skip to sidebar Skip to footer

D3.js: How To Remove Nodes When Link-data Updates In A Force Layout

I'm using a force layout graph to show a network but I have problems when updating my data. I already check How to update elements of D3 force layout when the underlying data chang

Solution 1:

The general node-link graph structure can have nodes without links, just like it can have nodes with one, two or hundreds of link. Your update method replaces the links' data, but does not look through nodes data to remove those that no longer have any links attached.

They way you've currently got it set up, however, there is a fairly straightforward fix. As is, you initialize links from the dataset, and initialize nodes to be empty. Then in this section of your update method:

links.forEach(function(link)
        {
            link.source = nodes[link.source] 
                          || (nodes[link.source] = {name: link.source});

            link.target = nodes[link.target] 
                          || (nodes[link.target] = {name: link.target});
        });

You add all the nodes mentioned as either a source or target of a link to the nodes list, after first checking whether or not it's already in the list.

(If it isn't in the list, nodes[link.source] will return null, so the || OR operator will kick in and the second half of the statement is evaluated, creating the object, adding it to the nodes list, and then connecting it to the link object.)

Now, the first time your run your update method, this fills up the nodes list with data. The second time, however, the nodes list is already full and you don't do anything to take any nodes away.

The simple fix is to reset your nodes list to an empty object (nodes={};) at the start of your update method. Then, only the nodes that are in the current set of links will be added back in, so when you re-compute the data join on the circles and text all the unused nodes will be put into the .exit() selection and removed.

But, I should mention that if you're updating a lot, and only changing a few objects each time, there are other ways to do this that require more code but will be faster in update. This version recreates all the node and link data objects each time. If you've got a lot (many hundreds) of complex data nodes and are only changing a couple each update, it might be worth it to add an extra property to your node objects keeping track of how many links are attached, and only reset that at the start of your update method. Then you could use a filter to figure out which of the node objects to include in your data join.

Edited to Add:

Here is the approach I'd use for a more conservative update function (versus a complete reset of the data). It's not the only option, but it doesn't have much overhead:

First step (in update method), mark all nodes to have zero links:

d3.values(nodes).forEach(function(aNode){ aNode.linkCount = 0;}); 
   //Reset the link count for all existing nodes by//creating an array out of the nodes list, and then calling a function//on each node to set the linkCount property to zero.

Second step, change the links.forEach() method to record the number of links in each node:

links.forEach(function(link)
    {
     link.source = nodes[link.source] 
                      || (nodes[link.source] = {name: link.source, linkCount:0});
                                    //initialize new nodes with zero links

     link.source.linkCount++;
        // record this link on the source node, whether it was just initialized// or already in the list, by incrementing the linkCount property// (remember, link.source is just a reference to the node object in the // nodes array, when you change its properties you change the node itself.)

     link.target = /* and then do the same for the target node */
    });

Third step, option one, use an filter to only include nodes that have at least one link:

force
    .nodes( d3.values(nodes).filter(function(d){ return d.linkCount;}) )
      //Explanation: d3.values() turns the object-list of nodes into an array.//.filter() goes through that array and creates a new array consisting of //the nodes that return TRUE when passed to the callback function.  //The function just returns the linkCount of that node, which Javascript //interprets as false if linkCount is zero, or true otherwise..links(links)
    .start();

Note that this does not delete the unused nodes from the nodes list, it only filters them from getting passed to the layout. If you don't expect to be using those nodes again, you will need to actually remove them from the nodes list.

Third step, option two, scan through the nodes list and delete any nodes that have zero links:

d3.keys(nodes).forEach(
   //create an array of all the current keys(names) in the node list, //and then for each one:function (nodeKey) {
       if (!nodes[nodeKey].linkCount) {
         // find the node that matches that key, and check it's linkCount value// if the value is zero (false in Javascript), then the ! (NOT) operator// will reverse that to make the if-statement return true, // and the following will execute:

           delete(nodes[nodeKey]); 
             //this deletes the object AND its key from the nodes array
       }

   }//end of function

); //end of forEach method/*then add the nodes list to the force layout object as before, 
     no filter needed since the list only includes the nodes you want*/

Post a Comment for "D3.js: How To Remove Nodes When Link-data Updates In A Force Layout"