Shengus
Shengus

Reputation: 51

How to dynamically update a d3.js force layout graph?

I have a force layout graph that works as expected. It reads a graph from a JSON on disk and displays it. I now have the server pushing data to the client page using socket io. This data is a JSON containing the node and link data.

How do I have the force layout refresh and update the layout when it receives this new JSON from the server?

var width = 1900, 
height = 1100;

var color = d3.scale.category20();

var force = d3.layout.force() 
    .charge(-120)         
    .linkDistance(30)
    .size([width, height]);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);


var networkData = {};
var socket = io.connect('http://localhost:3000');

socket.on("networkData", function (data) {
    networkData = data; //This is the data I want to use
});


d3.json("data.json", function(error, graph) { //"data.json" is the data 
    if (error) throw error;                   // currently being used
                                              // want to use "networkData"
force
  .nodes(graph.nodes)
  .links(graph.links)
  .start();

 var link = svg.selectAll(".link")
  .data(graph.links)        
  .enter().append("line")
  .attr("class", "link")
  .style("stroke-width", function(d) { return Math.sqrt(d.value); });

 var node = svg.selectAll(".node") 
  .data(graph.nodes)
  .enter().append("circle")
  .attr("class", "node")
  .attr("r", 5)
  .style("fill", "orange")
  .call(force.drag);

 node.append("title")
  .text(function(d) { return d.name; });

 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("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
 });
});

EDIT

I have updated the code as Guillermo Garcia suggested as follows:

socket.on("networkData", function (data) {
    force.stop()
    render(data);
    force
        .nodes(data.nodes)
        .links(data.links)
        .start();             
});

function render(data) {

    var link = svg.selectAll(".link") 
      .data(data.links)     
      .enter().append("line")
      .attr("class", "link")
      .style("stroke-width", function(d) { return Math.sqrt(d.value); });

   var node = svg.selectAll(".node") 
      .data(data.nodes)
      .enter().append("circle")
      .attr("class", "node")
      .attr("r", 5)
      .style("fill", "orange")
      .call(force.drag);

   node.append("title")
      .text(function(d) { return d.name; });

 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("cx", function(d) { return d.x; })
     .attr("cy", function(d) { return d.y; });
 });
}

However, the graph only works for the first networkData JSON sent from the server. I also have to manually refresh the page in order for this data to display.

Why is the data not working for the > 2nd data files? How can I have the page dynamically update? (i.e. not have to manually refresh)

Upvotes: 1

Views: 1821

Answers (2)

kwoxer
kwoxer

Reputation: 3833

Maybe this might be a bit harder to implement for your case, but I'm sure that my code can give you at least some ideas. So these are the functions I basically use to clean up the presentation and add new nodes from the database dynamically. You can see the result here. These lines also check if there are duplicates and filtering them out. If you need some more definition or variables, let me know.

        cleanPresentation: function () {
            svg.remove();
            nodeCircles = {};
            alreadyThere = false;
        },
        getAlreadyThere: function () {
            return alreadyThere;
        },
        createGraph: function (newJSON) {
            if (alreadyThere) {
                svg.remove();
                nodeCircles = {};
            }
            this.updateForceUsingNewNodes(this.generateObjects(newJSON));
            currentJSON = newJSON;
            if (alreadyThere == false) {
                this.setbasiczoom();
            }
            alreadyThere = true;
        },
        updateGraph: function (newJSON) {
            svg.remove();
            this.findDuplicatesAndSetEmpty(newJSON);
            this.deleteEmptyObjectsInJSON(newJSON);
            currentJSON = currentJSON.concat(newJSON);
            this.updateForceUsingNewNodes(this.generateObjects(currentJSON));
        },
        findDuplicatesAndSetEmpty: function (newJSON) {
            for (var i = 0; i < currentJSON.length; i++) {
                for (var o = 0; o < newJSON.length; o++) {
                    if ((currentJSON[i].source.ID == newJSON[o].source) && (currentJSON[i].target.ID == newJSON[o].target)
                        || (currentJSON[i].source.ID == newJSON[o].target) && (currentJSON[i].target.ID == newJSON[o].source)) {
                        newJSON[o] = {};
                    }
                }
            }
        },
        deleteEmptyObjectsInJSON: function (json) {
            for (var i = 0; i < json.length; i++) {
                var y = json[i].source;
                if (y === "null" || y === null || y === "" || typeof y === "undefined") {
                    json.splice(i, 1);
                    i--;
                }
            }
        },
        updateGraphByRemoveElement: function (clickedNode, index) {
            svg.remove();
            var json4Splicing = currentJSON;
            for (var i = 0; i < json4Splicing.length; i++) {
                if (json4Splicing[i].source.ID == clickedNode.ID) {
                    json4Splicing[i] = {};
                } else if (json4Splicing[i].target.ID == clickedNode.ID) {
                    json4Splicing[i] = {};
                }
            }
            familytree.deleteEmptyObjectsInJSON(json4Splicing);
            familytree.deleteNode(force.nodes(), clickedNode);
            currentJSON = json4Splicing;
            familytree.updateForceRemoveElement(familytree.generateObjects(currentJSON));
        },
        deleteNode: function (allNodes, clickedNode) {
            allNodes.forEach(function (node) {
                if (node == clickedNode) {
                    force.links().forEach(function (link) {
                        if (node.ID == link.source.ID) {
                            link.target.linkCount--;
                        }
                        if (node.ID == link.target.ID) {
                            link.source.linkCount--;
                        }
                    });
                    node.linkCount = 0;
                }
            });
        },
        generateObjects: function (json) {
            json.forEach(function (link) {
                if (typeof(link.source) == "string") {
                    link.source = nodeCircles[link.source] || (nodeCircles[link.source] = {name: link.sourceName, significance: link.sourceSign, uniquename: link.sourceUName, ID: link.source, class: link.sourceClass, relation: link.relation, race: link.sourceRace, linkCount: 0});
                    link.source.linkCount++;
                }
                if (typeof(link.target) == "string") {
                    link.target = nodeCircles[link.target] || (nodeCircles[link.target] = {name: link.targetName, significance: link.targetSign, uniquename: link.targetUName, ID: link.target, class: link.targetClass, relation: link.relation, race: link.targetRace, linkCount: 0});
                    link.target.linkCount++;
                }
            });
            return json;
        },
        updateForceRemoveElement: function (links) {
            force.nodes(d3.values(nodeCircles).filter(function (d) {
                return d.linkCount;
            }));
            force.links(d3.values(links));
            familytree.initializeGraph();
        },
        updateForceUsingNewNodes: function (links) {
            force.nodes(d3.values(nodeCircles).filter(function (d) {
                return d.linkCount;
            }));
            force.links(d3.values(links));
            this.initializeGraph();
        }

Upvotes: 1

Guillermo
Guillermo

Reputation: 1044

Have you tried creating a render function and call it in the socket.io event?

The render function should have all the the code from var link = svg.selectAll(".link") to force.on("tick", function() {

 var link = svg.selectAll(".link")
 .data(graph.links)    

 ...
 ...

 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("cx", function(d) { return d.x; })
     .attr("cy", function(d) { return d.y; });
 });

Good luck!

Upvotes: 1

Related Questions