D3js force layout destroy and reset

Jason picture Jason · Jan 24, 2014 · Viewed 14.6k times · Source

Based on two D3 examples: Force layout (http://bl.ocks.org/mbostock/1095795) and clustered force layout (http://bl.ocks.org/mbostock/1748247), I managed to built a force layout with a few independent point of gravity to control nodes position on top of the links between nodes.

// Set up map
function map_init(){

    force = d3.layout.force()
        .nodes(nodes)
        .links(links)
        .size([width, height])
        .on("tick", tick);

    svg = d3.select("#map").append("svg")
        .attr("width", width)
        .attr("height", height);

    link = $map.selectAll(".link");
    node = $map.selectAll(".node");

    d3.json("graph.json", function(error, graph) {

        // set up nodes
        for( i = 0; i < graph.nodes.length; i++ ){          
            nodes.push( graph.nodes[i] );
        }

        // position nodes to three different gravity centres based on theme
        for( i = 0; i < nodes.length; i++ ){
            if ( nodes[i].theme == "theme1" ){ 
                nodes[i].cx = 100;
                nodes[i].cy = 100; 
            } else if ( nodes[i].theme == "theme2" ){ 
                nodes[i].cx = 300;
                nodes[i].cy = 300; 
            } else if ( nodes[i].theme == "theme3" ){ 
                nodes[i].cx = 500;
                nodes[i].cy = 500; 
            }   
        }

        // link nodes of the same theme
        theme1_nodes = nodes.filter(function(d){ return (d.theme == "theme1"); });
        theme2_nodes = nodes.filter(function(d){ return (d.theme == "theme2"); });
        theme3_nodes = nodes.filter(function(d){ return (d.theme == "theme3"); });
        for (i = 0; i < theme1_nodes.length-1; i++){
            links.push({ source: theme1_nodes[i], target: theme1_nodes[i+1] });
        }
        for (i = 0; i < theme2_nodes.length-1; i++){
            links.push({ source: theme2_nodes[i], target: theme2_nodes[i+1] });
        }
        for (i = 0; i < theme3_nodes.length-1; i++){
            links.push({ source: theme3_nodes[i], target: theme3_nodes[i+1] });
        }

        start();

    }); 

}

// Start
function start() {

  link = link.data(force.links(), function(d) { return d.source.id + "-" + d.target.id; });
  link.enter()
    .insert("svg:line")
        .attr("class", "link");
  link.exit()
        .remove();

  node = node.data(force.nodes(), function(d) { return d.id; });
  var nodeEnter = node.enter()
          .append("svg:g")
            .attr("class", "node");
        .on("click", map_nodeClick);
  node.exit().remove();

  // Enter node information
  nodeEnter.each(function(d) {
        theTitle = d3.select(this).append("svg:text")
        .attr("font-family", "Helvetica")
            .attr("class", "title")
        .text( d.title );
    });

    // More content to go into each node
    // .
    // .
    // .

  force.start();

}

// Tick
function tick(e) {

    node
      .each(gravity(.2 * e.alpha))
      .attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; })
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

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

}

// Gravity
function gravity(alpha) {

  return function(d) {
    d.y += (d.cy - d.y) * alpha;
    d.x += (d.cx - d.x) * alpha;
  };

}

// Set up when page first loads
map_init();

In order to reset/restart the force layout anytime without reloading the page, I bound the following function to a reset button:

// Remove force layout and data
function map_remove(){

    node.remove();
    link.remove();
    svg.remove();
    nodes = [];
    links = [];

}

// Reset button
$('a#reset').click(function(e){

    e.preventDefault();

    map_remove();
    map_init();

});

This webpage is displayed on a device accessible by group of people. Only loaded once in the morning and stayed running on iPad Safari for 12 hours. Link between nodes ideally changes dynamically based on users input (to be implemented). Apart from the force layout there are other info on the webpage. An option to relaunch/reset the force layout without reloading the page is required.

  1. Is there a built-in method to destroy the D3 force layout and its data?
  2. Currently this works ok as no extra DOM elements were created and no errors found from inspector. However I am not sure how to check if all the D3 objects has been cleared/emptied so no duplicated data was stored/accumulated?
  3. Currently each reset for some reasons pull the nodes closer and closer to the centre of the map. Have I missed out something in the map_remove() function?
  4. Would a complete restart of the D3 force layout improve the performance of the browser at any point? i.e. clearing up the memory for painting the SVG?

Answer

Lars Kotthoff picture Lars Kotthoff · Jan 24, 2014
  1. No. You have to do this manually.
  2. You could have a look at the DOM, but it looks like you're deleting everything.
  3. I'm guessing that this happens because you're not actually deleting the nodes/links from the force layout. At some point, you've given the variables nodes and links to the force layout. Changing what those names point to (i.e. []) doesn't change the reference in the force layout. That is, the data objects are still there and referenced. There are two ways to remove them. You can either modify the nodes and links in place (e.g. with .slice()), or reset them explicitly in the force layout.

    nodes = []; links = []; force.nodes(nodes); force.links(links);

  4. Hard to say without a specific example, but the answer is most likely no. Javascript is garbage collected, so doing it manually shouldn't have an impact.