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.
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 + ")");
}