score:0

So, unless I'm mis-reading what's at:

https://github.com/mbostock/d3/wiki/Force-Layout#wiki-nodes

the force layout will actually initialize (or re-initialize if you call resume/start again) the layout with node and edge information specified on the values passed to the nodes/edges functions.

I tested this by using your chart and then on layout end, resuming the force layout. It doesn't recompute the node/edge positions since they're already persisted on the dataset that was initially passed in. You can also test this by adding the x/y values to your initial data.

http://jsfiddle.net/jugglebird/Brb29/1/

force.on("end", function() {
    // At this point dataset.nodes will include layout information
    console.log("resuming");  
    force.resume(); // equivalent to force.alpha(.1);
});

score:0

It is crucial to keep in mind, that the force layout stores its results in the data itself. That way they are accessible when — within the tick handler function — adjusting the visual nodes and edges to which the data was bound.

When doing its calculations taking into account all forces and constraints the force layout will store the results into the nodes contained in the array of nodes provided to force.nodes(). At the end of each tick, when all calculations are completed, your dataset.nodes array will have been updated with each node containing the new position, velocity etc., thus representing the current state of the force layout.

There is, however, one thing missing to be able to capture the full state of the layout, which is its current value of alpha.

Saving both, dataset and alpha, by whatever means you like, you will later on be able to restore the force layout to the state at the moment these properties where captured. Depending on your needs you might use rather volatile storage like keeping local references to these properties, or JSON.stringify() them to even be able to persist them somehow.

For your own code this could be done as follows:

  1. If you need to completely remove the SVG from the DOM as is done in your callback to the first timeout, it is convenient to put the code which appends the SVG as well as the nodes and edges into a function because you need to call it twice.

    function initChart() {
      svg = d3.select("#chart")
                  .append("svg")
                  .attr("width", w)
                  .attr("height", h);
    
      //Create edges as lines
      edges = svg.selectAll("line")
          .data(dataset.edges)
          .enter()
          .append("line")
          .style("stroke", "#ccc")
          .style("stroke-width", 1);
    
      //Create nodes as circles
      nodes = svg.selectAll("circle")
          .data(dataset.nodes)
          .enter()
          .append("circle")
          .attr("r", 10)
          .style("fill", function(d, i) {
              return colors(i);
          })
          .call(force.drag);
    }
    
    initChart();              // Append the SVG with nodes and edges.
    

    If, however, it is sufficient to only set it to display:none things become more easy, because you may keep all references intact.

  2. To fully save the state of the layout you need to store the current value of alpha. Afterwards, you call force.stop() to actually immediately halt the force layout. Remember, that your dataset will already have the up-to-date values set.

    var alpha;                // This will save alpha when stopped.
    
    // Stop and remove after 1 second.
    setTimeout(function() {
      alpha = force.alpha();  // Save alpha.
      force.stop();           // Stop the force.
      svg.remove();           // Dump the SVG.
    }, 1000);
    
  3. You may at any time restore the force layout to the saved state. In your example the force layout referenced by force wasn't destroyed, so that it still has the reference to dataset containing the layout's state. But according to the API docs for force.nodes([nodes]), the values present on the nodes supplied as the parameter will also get adopted when setting up a completely new layout. You are then able to resume its execution by setting force.alpha(alpha) to the saved value. Notice, that before the restart of the force layout, the SVG is rebuild by another call to initChart().

    // Restore to paused state and restart.
    setTimeout(function() {
      initChart();            // Append the SVG with nodes and edges.
      force.alpha(alpha);     // Restart the force with alpha.
    }, 3000);
    

Have a look at the full snippet for a demonstration. I have shortened the timeouts to emphasize the effect.

        //Width and height
        var w = 800;
        var h = 600;

        //Original data
        var dataset = {
            nodes: [
                { name: "Adam" },
                { name: "Bob" },
                { name: "Carrie" },
                { name: "Donovan" },
                { name: "Edward" },
                { name: "Felicity" },
                { name: "George" },
                { name: "Hannah" },
                { name: "Iris" },
                { name: "Jerry" }
            ],
            edges: [
                { source: 0, target: 1 },
                { source: 0, target: 2 },
                { source: 0, target: 3 },
                { source: 0, target: 4 },
                { source: 1, target: 5 },
                { source: 2, target: 5 },
                { source: 2, target: 5 },
                { source: 3, target: 4 },
                { source: 5, target: 8 },
                { source: 5, target: 9 },
                { source: 6, target: 7 },
                { source: 7, target: 8 },
                { source: 8, target: 9 }
            ]
        };

        //Initialize a default force layout, using the nodes and edges in dataset
        var force = d3.layout.force()
          .nodes(dataset.nodes)
          .links(dataset.edges)
          .size([w, h])
          .linkDistance([100])
          .charge([-100])
          .start()
          .on("tick", function() {
            edges.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; });
 
            nodes.attr("cx", function(d) { return d.x; })
                 .attr("cy", function(d) { return d.y; });
          });

        var colors = d3.scale.category10();

        //Create SVG element
        var svg,
            edges,
            nodes;
            
        function initChart() {
          svg = d3.select("#chart")
                      .append("svg")
                      .attr("width", w)
                      .attr("height", h);
  
          //Create edges as lines
          edges = svg.selectAll("line")
              .data(dataset.edges)
              .enter()
              .append("line")
              .style("stroke", "#ccc")
              .style("stroke-width", 1);
  
          //Create nodes as circles
          nodes = svg.selectAll("circle")
              .data(dataset.nodes)
              .enter()
              .append("circle")
              .attr("r", 10)
              .style("fill", function(d, i) {
                  return colors(i);
              })
              .call(force.drag);
        }
        
        initChart();              // Append the SVG with nodes and edges.

        var alpha;                // This will save alpha when stopped.

        // Stop and remove after 1 second.
        setTimeout(function() {
          alpha = force.alpha();  // Save alpha.
          force.stop();           // Stop the force.
          svg.remove();           // Dump the SVG.
        }, 1000);
        
        // Restore to paused state and restart.
        setTimeout(function() {
          initChart();            // Append the SVG with nodes and edges.
          force.alpha(alpha);     // Restart the force with alpha.
        }, 3000);
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>D3: Force layout</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
  <style type="text/css">
    /* No style rules here yet */
  </style>
</head>

<body>
  <div data-role="content" id="p1content">
    <div id="chart"></div>
  </div>
</body>

</html>

score:6

EDIT: FULL solution now!

Furthermore, this approach will work in a wide variety of scenarios -- both to stop and restart a layout on a single page, and also to save and re-load a layout on a different page.

First, save the original JSON graph at the end of the layout process, which you can listen for using:

force.on('tick', function(){
    ...
}).on('end', function(){
    // Run this when the layout has finished!
});

Saving now is valuable because x,y coordinates (and some other things) have been added to each node and edge by d3, during the layout (but keep changing, until it comes to a halt). Being JSON, the graph is easy to serialize, stick in localStorage, pull out and parse again:

localStorage.setItem(JSON.stringify(graph));
...
JSON.parse(localStorage.getItem('graph'));

Once you've pulled it out of storage though, you don't just want a JSON object, you want to turn that saved object back into an svg, and ideally, using the apparatus already available with d3.layout.force for simplicity. And in fact, you can do this -- with a few small changes.

If you stick the saved graph right back in, i.e. just run

force
  .nodes(graph.nodes)
  .links(graph.links)
  .start();

with the saved graph, you'll get two weird behaviors.

Weird Behavior 1, and Solution

Based on the good documentation, Including x and y coordinates in the starting graph overrides the random initialization of the layout process -- but only the initialization. So you'll get the nodes where they should be, but then they'll float off into a uniformly distributed circle, as the layout ticks. To keep this from happening, use:

  for(n in graph.nodes){
    graph.nodes[n].fixed = 1
  }

before running force.start().

Weird Behavior 2, and Solution Now your nodes and edges will both be where you want them to be, but your edges will -- shrink?

Something similar has happened, but unfortunately, you can't use exactly the same solution. The edge lengths were saved in the JSON object, and were used in the initialization of the layout, but then the layout imposes a default length (20) on them all, unless you, first, save the edge lengths in the JSON graph --

.on('end', function() {

    links = svg.selectAll(".link")[0]
    for(i in graph.links){
      graph.links[i].length = links[i].getAttribute('length')
    }
    localStorage.setItem('graph', JSON.stringify(graph));

});

and then, before force.start() --

force.linkDistance(function (d) { return d.length })

(documentation for which can be found here) and finally, your graph will look like it's supposed to.

In summary, if you make sure your JSON graph 1) has x,y coordinates on the nodes, 2) has nodes set to fixed=1, and 3) force has linkDistance set before .start(), then you can just run exactly the same layout process as if you were initializing from scratch, and you'll get back your saved graph.


Related Query

More Query from same tag