Reputation: 412
I am using d3.js and jquery with a PHP back-end (based on yii framework) to create a dynamic force directed graph to represent the current state of hosts and services on the network that we are monitoring using Nagios.
The graph shows root -> hostgroups -> hosts -> services. I have created a server side function to return a JSON object in the following format
{
"nodes": [
{
"name": "MaaS",
"object_id": 0
},
{
"name": "Convergence",
"object_id": "531",
"colour": "#999900"
},
{
"name": "maas-servers",
"object_id": "719",
"colour": "#999900"
},
{
"name": "hrg-cube",
"object_id": "400",
"colour": "#660033"
}
],
"links": [
{
"source": 0,
"target": "531"
},
{
"source": 0,
"target": "719"
},
{
"source": "719",
"target": "400"
}
]
}
The nodes contain an object id which is used in the links and colour for displaying the state of the node (OK = green, WARNING = yellow, etc) The links has the source object ids and target object ids for the nodes. The nodes and links may change as new hosts are added or removed from the monitoring system
I have the following code which setups the initial SVG and then every 10 seconds
Force is started
$.ajaxSetup({ cache: false }); width = 960, height = 500; node = []; link = []; force = d3.layout.force() .charge(-1000) .linkDistance(1) .size([width, height]);
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
setInterval(function(){
$.ajax({
url: "<?php echo $url;?>",
type: "post",
async: false,
datatype: "json",
success: function(json, textStatus, XMLHttpRequest)
{
json = $.parseJSON(json);
var nodeMap = {};
json.nodes.forEach(function(x) { nodeMap[x.object_id] = x; });
json.links = json.links.map(function(x) {
return {
source: nodeMap[x.source],
target: nodeMap[x.target],
};
});
link = svg.selectAll("line")
.data(json.links);
node = svg.selectAll("circle")
.data(json.nodes,function(d){return d.object_id})
link.enter().append("line").attr("stroke-width",1).attr('stroke','#999');
link.exit().remove();
node.enter().append("circle").attr("r",5);
node.exit().remove();
node.attr("fill",function(d){return d.colour});
node.append("title")
.text(function(d) { return d.name; });
node.call(force.drag);
force
.nodes(node.data())
.links(link.data())
.start()
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 = Math.max(5, Math.min(width - 5, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(5, Math.min(height - 5, d.y)); });
});
}
});
},10000);
An example of the output can be seen at Network Visualization
All of the above works correctly with the exception that every time the code loops it causes the visualization to restart and the nodes all bounce about until they settle. What I need is for any current items to stay as they are but any new nodes and links are added to the visualisation and are clickable and draggable, etc.
If anyone can help I would be eternally grateful.
Upvotes: 12
Views: 8028
Reputation: 412
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 + ")");
}
Upvotes: 6
Reputation: 1163
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 call
var 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 not
function 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 not
function 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.
Upvotes: 2
Reputation: 204
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
Upvotes: 2
Reputation: 3245
You don't actually need to pass anything back to the server, as long as, server-side, you are able to tell what new nodes
and links
are being generated. Then, instead of reloading your whole d3 script, you load it once, and in the force.on("tick", function())
, you make your 10 sec timeout AJAX call to go get from the server the new data
you want to append, be it nodes
or links
.
For instance, imagine that you initially have this JSON in your server:
[
{
"nodes": [
{
"name": "MaaS",
"object_id": 0
},
{
"name": "Convergence",
"object_id": "531",
"colour": "#999900"
}
]
},
{
"links": [
{
"source": 0,
"target": "531"
}
]
}
]
You go get it from your server with AJAX and parse it with json = $.parseJSON(json);
.
Then, write your timeout
so that instead of running the whole function you have in success
, only runs after calculating the layout. Then, again, on success
, parse the new JSON you got from the server and add the_new_ nodes
and links
to the already existing force.nodes
and force.links
respectively.
Please note that I didn't test this and I'm not sure how it will work and/or perform, but I think the general approach is feasible.
Upvotes: 1