Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Saving and reloading a force layout using d3.js

I am trying to find the correct method to be able to save out a force diagram node layout positions once settled, then later, to reload that layout and start again from the same settled state.

I am trying to do this by cloning the DOM elements containing the diagram, removing it and then reloading it.

This I can do, in part as indicated below:-

_clone = $('#chart').clone(true,true);
$('#chart').remove();

Selects the containing div, clones it and removes it, then later

var _target = $('#p1content');
_target.append(_clone);

Selects the div that used to hold the chart and reloads it. Reloaded diagram is fixed.

I don't know how to reconnect the force to allow manipulation to carry on. Is this possible? I want to preserve the settled position of the nodes.

Another possibility, could I reload the node positions and start the force with a low alpha?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>D3: Force layout</title>
    <script src="./jquery-2.0.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="../d3.v3.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>
    <script type="text/javascript">

        //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();

        var colors = d3.scale.category10();

        //Create SVG element
        var svg = d3.select("#chart")
                    .append("svg")
                    .attr("width", w)
                    .attr("height", h);

        //Create edges as lines
        var edges = svg.selectAll("line")
            .data(dataset.edges)
            .enter()
            .append("line")
            .style("stroke", "#ccc")
            .style("stroke-width", 1);

        //Create nodes as circles
        var nodes = svg.selectAll("circle")
            .data(dataset.nodes)
            .enter()
            .append("circle")
            .attr("r", 10)
            .style("fill", function(d, i) {
                return colors(i);
            })
            .call(force.drag);

        //Every time the simulation "ticks", this will be called
        force.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; });

        });

// After 5 secs clone and remove DOM elements
        setTimeout(function() {
                        _clone = $('#chart').clone(true,true);
                        $('#chart').remove();
        }, 5000);
//After 10 secs reload DOM
        setTimeout(function() {
                        var _target = $('#p1content');
                        _target.append(_clone);

// WHAT NEEDS TO GO HERE TO RECOUPLE THE FORCE?                     

         }, 10000);

    </script>
</body>
</html>

Added this where I put // WHAT NEEDS TO GO HERE TO RECOUPLE THE FORCE?
This seems to work picking up the existing elements restored and recouples the Force where it left off passing the force nodes etc into the Timeout function

force = d3.layout.force()
    .nodes(dataset.nodes)
    .links(dataset.edges)
    .size([w, h])
    .linkDistance([100])
    .charge([-100])
    .start();

colors = d3.scale.category10();

//Create SVG element
svg = d3.select("#chart");

//Create edges as lines
edges = svg.selectAll("line")
    .data(dataset.edges);

//Create nodes as circles
nodes = svg.selectAll("circle")
    .data(dataset.nodes)
    .call(force.drag);

//Every time the simulation "ticks", this will be called
force.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; });

});
like image 993
Dean Avatar asked Aug 13 '13 10:08

Dean


1 Answers

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.

like image 110
one_observation Avatar answered Nov 08 '22 17:11

one_observation