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.
Build a force-directed graph with React and D3 D3's d3-force module gives you the tools to simulate forces. You create a new simulation with d3.forceSimulation (), add different forces with.force ('name', func), pass-in data with.nodes (), and update your visuals on each tick of the animation with.on ('tick', func).
The graph generated with d3-force contains only nodes and edges and there is only a small collection of graph samples for reference because most graphs are customized. The following is a popular network diagram I found that is realized with D3-force. Apparently it is too simple for some use cases.
We chose D3 not just because of its popularity and bulletproof core, but especially because of the amazing physical tools provided by d3-force. D3 is all about (complex) data visualisation. Our goal was to build a force-directed graph similar to this example by D3’s creator Mike Bostock himself.
A force-directed graph uses forces that work on the nodes and links to move them around to create the structure here and make it visually pleasing. The forces can be attractive and repulsive, and we use both in this graph.
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 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With