D3 Force Directed Graph ajax update
I have managed to find a solution to the problem using a mixture of all the advice above, below is the code I have used
var width = $(document).width(); var height = $(document).height(); var outer = d3.select("#chart") .append("svg:svg") .attr("width", width) .attr("height", height) .attr("pointer-events", "all"); var vis = outer .append('svg:g') .call(d3.behavior.zoom().on("zoom", rescale)) .on("dblclick.zoom", null) .append('svg:g') vis.append('svg:rect') .attr('width', width) .attr('height', height) .attr('fill', 'white'); var force = d3.layout.force() .size([width, height]) .nodes([]) // initialize with a single node .linkDistance(1) .charge(-500) .on("tick", tick); nodes = force.nodes(), links = force.links(); var node = vis.selectAll(".node"), link = vis.selectAll(".link"); redraw(); setInterval(function(){ $.ajax({ url: "<?php echo $url;?>", type: "post", async: false, datatype: "json", success: function(json, textStatus, XMLHttpRequest) { var current_nodes = []; var delete_nodes = []; var json = $.parseJSON(json); $.each(json.nodes, function (i,data){ result = $.grep(nodes, function(e){ return e.object_id == data.object_id; }); if (!result.length) { nodes.push(data); } else { pos = nodes.map(function(e) { return e.object_id; }).indexOf(data.object_id); nodes[pos].colour = data.colour; } current_nodes.push(data.object_id); }); $.each(nodes,function(i,data){ if(current_nodes.indexOf(data.object_id) == -1) { delete_nodes.push(data.index); } }); $.each(delete_nodes,function(i,data){ nodes.splice(data,1); }); var nodeMap = {}; nodes.forEach(function(x) { nodeMap[x.object_id] = x; }); links = json.links.map(function(x) { return { source: nodeMap[x.source], target: nodeMap[x.target], colour: x.colour, }; }); redraw(); } }); },2000); function redraw() { node = node.data(nodes,function(d){ return d.object_id;}); node.enter().insert("circle") .attr("r", 5) node.attr("fill", function(d){return d.colour}) node.exit().remove(); link = link.data(links); link.enter().append("line") .attr("stroke-width",1) link.attr('stroke',function(d){return d.colour}); link.exit().remove(); force.start(); } function tick() { link.attr("x1", function(d) { return Math.round(d.source.x); }) .attr("y1", function(d) { return Math.round(d.source.y); }) .attr("x2", function(d) { return Math.round(d.target.x); }) .attr("y2", function(d) { return Math.round(d.target.y); }); node.attr("cx", function(d) { return Math.round(d.x); }) .attr("cy", function(d) { return Math.round(d.y); }); } function rescale() { trans=d3.event.translate; scale=d3.event.scale; vis.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")"); }
Check out this answer. You need a unique identifier for your nodes, which it appears you have.
Updating links on a force directed graph from dynamic json data
I recently tried to do the same thing, here is the solution I came up with. What I do is load a first batch of data with links.php
and then update them with newlinks.php
, both return a JSON with a list of objects with attributes sender
and receiver
. In this example newlinks returns a new sender each time and I set the receiver to be a randomly selected old node.
$.post("links.php", function(data) {// Functions as an "initializer", loads the first data// Then newlinks.php will add more data to this first batch (see below)var w = 1400, h = 1400;var svg = d3.select("#networkviz") .append("svg") .attr("width", w) .attr("height", h);var links = [];var nodes = [];var force = d3.layout.force() .nodes(nodes) .links(links) .size([w, h]) .linkDistance(50) .charge(-50) .on("tick", tick);svg.append("g").attr("class", "links");svg.append("g").attr("class", "nodes");var linkSVG = svg.select(".links").selectAll(".link"), nodeSVG = svg.select(".nodes").selectAll(".node");handleData(data);update();// This is the server callvar interval = 5; // set the frequency of server calls (in seconds)setInterval(function() { var currentDate = new Date(); var beforeDate = new Date(currentDate.setSeconds(currentDate.getSeconds()-interval)); $.post("newlinks.php", {begin: beforeDate, end: new Date()}, function(newlinks) { // newlinks.php returns a JSON file with my new transactions (the one that happened between now and 5 seconds ago) if (newlinks.length != 0) { // If nothing happened, then I don't need to do anything, the graph will stay as it was // here I decide to add any new node and never remove any of the old ones // so eventually my graph will grow extra large, but that's up to you to decide what you want to do with your nodes newlinks = JSON.parse(newlinks); // Adds a node to a randomly selected node (completely useless, but a good example) var r = getRandomInt(0, nodes.length-1); newlinks[0].receiver = nodes[r].id; handleData(newlinks); update(); } });}, interval*1000);function update() { // enter, update and exit force.start(); linkSVG = linkSVG.data(force.links(), function(d) { return d.source.id+"-"+d.target.id; }); linkSVG.enter().append("line").attr("class", "link").attr("stroke", "#ccc").attr("stroke-width", 2); linkSVG.exit().remove(); var r = d3.scale.sqrt().domain(d3.extent(force.nodes(), function(d) {return d.weight; })).range([5, 20]); var c = d3.scale.sqrt().domain(d3.extent(force.nodes(), function(d) {return d.weight; })).range([0, 270]); nodeSVG = nodeSVG.data(force.nodes(), function(d) { return d.id; }); nodeSVG.enter() .append("circle") .attr("class", "node") // Color of the nodes depends on their weight nodeSVG.attr("r", function(d) { return r(d.weight); }) .attr("fill", function(d) { return "hsl("+c(d.weight)+", 83%, 60%)"; }); nodeSVG.exit().remove(); }function handleData(data) { // This is where you create nodes and links from the data you receive // In my implementation I have a list of transactions with a sender and a receiver that I use as id // You'll have to customize that part depending on your data for (var i = 0, c = data.length; i<c; i++) { var sender = {id: data[i].sender}; var receiver = {id: data[i].receiver}; sender = addNode(sender); receiver = addNode(receiver); addLink({source: sender, target: receiver}); }}// Checks whether node already exists in nodes or notfunction addNode(node) { var i = nodes.map(function(d) { return d.id; }).indexOf(node.id); if (i == -1) { nodes.push(node); return node; } else { return nodes[i]; }}// Checks whether link already exists in links or notfunction addLink(link) { if (links.map(function(d) { return d.source.id+"-"+d.target.id; }).indexOf(link.source.id+"-"+link.target.id) == -1 && links.map(function(d) { return d.target.id+"-"+d.source.id; }).indexOf(link.source.id+"-"+link.target.id) == -1) links.push(link);}function tick() { linkSVG.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;}); nodeSVG.attr("cx", function(d) {return d.x}) .attr("cy", function(d) {return d.y});}function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;}}, "json");
This is a very specific implementation so you should fill the holes where necessary depending on your server output. But I believe the D3 backbone is correct and what you are looking for :)Here is a JSFiddle to toy with it : http://jsfiddle.net/bTyh5/2/
This code was really useful and inspired some of the parts introduced here.