<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<script src="d3.v3.min.js"></script>
<script type="text/javascript" src="jquery-1.7.1.min.js"></script>
<script src="smoothie.js"></script>
<style type="text/css">
#graph, #statistics, #traffic {position: absolute }
.circle, .link { stroke: #56f; stroke-width: 3px; stroke-opacity: 0.75; }
.nodetext { pointer-events: none; font: 10px sans-serif; }
body { width:100%; height:100%; margin:0; padding:0; overflow: hidden; }
svg, div, canvas { margin:0; padding:0; }
</style>
</head>
<body>
<div id="graph"></div>
<div id="statistics"> </div>
<div> <canvas id="traffic" width="100%" height="150"></canvas></div>
</body>
<script type="text/javascript">
// Initialize the statistics chart at the bottom.
var smoothie = new SmoothieChart({'millisPerPixel': 300, 'grid': {'millisPerLine': 30000}, timestampFormatter:SmoothieChart.timeFormatter, yMaxFormatter: bytesToString, yMinFormatter: bytesToString, valueTransformFunction: logarithmicScaling, minValue: 0, maxValue: 262144000, labels: {fontSize: 13}});
smoothie.streamTo(document.getElementById("traffic"), 2000);
// For coloring the servers and the chart-lines.
var colorList = d3.scale.category10();
// Max intensity ~ 50MB/s
var maxIntensity = 50000;
var pendingLinks = [];
var intLinks = [];
var newLinks = [];
var links = {};
var nodes = {};
// color for links
// idle links (blue)
var idleColor = { "r": 85, "g": 102, "b": 255};
// active links (green)
var activeColor = { "r": 0, "g": 255, "b": 0};
// peaking links (red)
var peakColor = { "r": 255, "g": 0, "b": 0};
// IDs need to begin with a letter.
function makeId(prefix, text) {
return prefix + text.replace(/\./g, "-");
}
// Log scaling for the traffic graph
function logarithmicScaling(value) {
return Math.log(value/1000000+1);
}
function myGraph(el) {
var changed = false;
// to spawn new clients near already existing nodes.
var ccX = 0, ccY = 0, ccCount = 0;
// Add/update and remove elements on the graph object
this.updateNode = function (id) {
x = 1;
y = 1;
title = id;
id = makeId("a", id);
var distance = 2;
var radius = 4;
var color = "#0000FF"; // Blue for client nodes
// Servers look different
var s = servers[title]
if(s) {
distance = 6;
radius = 15;
}
distance *= w / 2000;
var node = findNode(id);
// Add node, if it doesn't exist
if (!node) {
if (ccCount != 0) {
x = ccX / ccCount;
y = ccY / ccCount;
}
nodes.push({"id":id, "title": title, "distance":distance, "radius":radius, "x":x, "y":y, "color":color});
changed = true;
} else {
var visNode = vis.select("#" + id);
if (visNode) {
// Change color and radius if server.
var s = servers[title];
if (s) {
visNode.select("circle").attr("r", radius);
visNode.select("circle").style("fill", s.color);
}
}
node.lifetime = 2;
}
}
// Remove nodes/edges that haven't reported for a while.
this.decay = function () {
for (var i = nodes.length - 1; i >= 0; --i) {
if ( nodes[i].lifetime <= 0 ) {
for (var j = links.length - 1; j >= 0; --j) {
if ((links[j].source === nodes[i]) || (links[j].target === nodes[i])) links.splice(j,1);
}
nodes.splice(i, 1);
changed = true;
continue;
}
nodes[i].lifetime--;
}
for (var i = links.length - 1; i >= 0; --i) {
if (links[i].lifetime <= 0) {
links.splice(i, 1);
changed = true;
continue;
}
links[i].lifetime--;
}
}
// Add/update links in the graph
this.updateLink = function (edge, timestamp) {
var sourceId = edge.source;
var targetId = edge.target;
var stroke = idleColor;
// Change color to red for links between servers
if (servers[sourceId] && servers[targetId]) stroke = "rgb(255, 0, 0)";
sourceId = makeId("a", sourceId);
targetId = makeId("a", targetId);
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if ((sourceNode !== undefined) && (targetNode !== undefined)) {
var link = findLink(sourceNode, targetNode);
// Add non existing links
if (!link) {
var index = $.inArray(sourceId + targetId, pendingLinks);
if (index !== -1) {
links.push({"id": sourceId + "-" + targetId ,"source": sourceNode, "target": targetNode, "lifetime":2, "color": stroke, "downloadRate" : 0, "lastReceived": 0, "lastTimestamp": 0, "lastUptime": 0, "colorIntensity": 0 });
changed = true;
} else {
newLinks.push(sourceId + targetId);
}
} else {
link.lifetime = 2;
// Calculate download rate for clients and color links.
if (link.lastTimestamp != 0) {
if(timestamp - lastTime > 0) {
// Download rate in KB/s
link.downloadRate = ((edge.received - link.lastReceived) / (timestamp - lastTime)) * 4 * 1000;
// Clients may have multiple connections with different received data -> prevent negative download rate...
if(link.downloadRate < 0) link.downloadRate = 0;
// only need fiddle with colors if needed.
if (link.downloadRate > 0) {
increasColorIntensity(link);
colorLink(link);
updateLinkColor(link);
}
}
} else {
link['downloadRate'] = 0;
}
link['lastReceived'] = edge.received;
link['lastTimestamp'] = timestamp;
}
}
}
// Color a link according to its intensity, does not apply the color in the svg! Use updateLinkColor function for that.
function colorLink(link) {
var red, green, blue;
// Green colors between 0 - 1 MB/s
if( link.colorIntensity <= 1000 ) {
// Blending idle (blue) and active (green) color
var factor = link.colorIntensity / 1000;
var cFactor = 1 - factor;
red = Math.sqrt(cFactor * Math.pow(idleColor.r, 2) + factor * Math.pow(activeColor.r, 2));
green = Math.sqrt(cFactor * Math.pow(idleColor.g, 2) + factor * Math.pow(activeColor.g, 2));
blue = Math.sqrt(cFactor * Math.pow(idleColor.b, 2) + factor * Math.pow(activeColor.b, 2));
} else { // Red over 1MB/s
// Blending active (green) and peak (red) color
var factor = (link.colorIntensity - 1000)/ (maxIntensity - 1000);
var cFactor = 1 - factor;
red = Math.sqrt(cFactor * Math.pow(activeColor.r, 2) + factor * Math.pow(peakColor.r, 2));
green = Math.sqrt(cFactor * Math.pow(activeColor.g, 2) + factor * Math.pow(peakColor.g, 2));
blue = Math.sqrt(cFactor * Math.pow(activeColor.b, 2) + factor * Math.pow(peakColor.b, 2));
}
link.color = "rgb("+ Math.floor(red) + ", " + Math.floor(green) + ", " + Math.floor(blue) + ")";
}
// Update the color of a link in the svg.
function updateLinkColor(link){
// Prevent future searches for links to improve performance.
if(!link.visObject) {
link.visObject = vis.select("#" + link.source.id + "-" + link.target.id);
}
var visLink = link.visObject;
visLink.style({stroke: function(d) { return d.color; }});
}
// Interval for fading out the color of active links
window.setInterval(decayLinkColor, 500);
function decayLinkColor() {
for (var i = 0; i < links.length ; i++ ){
var link = links[i];
// Do nothing if there is no intensity to decay.
if(link.colorIntensity > 0) {
// Fading rate of colored links
var fadingValue = (link.colorIntensity * 0.15) + 10;
link.colorIntensity -= fadingValue;
if (link.colorIntensity < 0) {
link.colorIntensity = 0;
} else if (link.colorIntensity < link.downloadRate) {
link.colorIntensity = link.downloadRate;
}
colorLink(link);
updateLinkColor(link);
}
}
}
// Set the color intensity of link according to its download rate.
function increasColorIntensity(link) {
if (link.colorIntensity < link.downloadRate) {
link.colorIntensity = link.downloadRate;
}
if (link.colorIntensity >= maxIntensity) {
link.colorIntensity = maxIntensity;
}
}
function findLink(sourceId, targetId) {
for (var i = 0; i < links.length; ++i) {
if ( (links[i].source === sourceId && links[i].target === targetId)
|| (links[i].source === targetId && links[i].target === sourceId) )
return links[i];
}
}
var findNode = function (id) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].id === id)
return nodes[i];
}
}
var findNodeIndex = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return i;
};
}
var w = 100, h = 100;
var lastW = -1, lastH = -1;
var updateBounds = function() {
var width = window.innerWidth;
// Chart needs to fit under the graph and the statistics
h = window.innerHeight - 150;
// Width for the graph and the statistics div
w = width/3*2;
if (lastW === w && lastH === h) return;
lastW = w;
lastH = h;
vis.attr("width", w);
vis.attr("height", h);
force.size([w, h]);
$(el).attr("width", w);
$(el).attr("height", h);
// Positions for statistics and the traffic graph
$("#statistics").attr("width", w/2).css("width", w/2 + "px").css("left", w + "px");
$("#statistics").attr("height", h).css("height", h + "px");
$("#traffic").attr("width", width).css("top", h + "px");
}
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
// Settings for movement of the graph
var force = d3.layout.force()
// Force of center gravitation
.gravity(.12)
// Distance of the links
.distance(function(bla) { return 4; })
// Strength of the links
.linkStrength(0.3)
// Force with which the links "pull"
.charge(function(bla) { return -0.9; })
// Friction for the graph nodes
.friction(0.5)
.size([w, h]);
nodes = force.nodes();
links = force.links();
this.update = function () {
if(changed){
update();
}
}
// Update the complete svg. Performance intensive.
function update () {
changed = false;
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
.on("mouseover", function(d) {
// Highlight statistics div
var statDiv = document.getElementById(makeId("b", d.title));
if (statDiv){
statDiv.setAttribute("style", "background-color:blue; color:white;");
}
// Increase line width of server in traffic chart
var s = servers[d.title];
if (!s) return;
smoothie.seriesSet[s.index].options.lineWidth = 4;
})
.on("mouseout", function(d) {
// Make statistics div normal again
var statDiv = document.getElementById(makeId("b", d.title));
if (statDiv){
statDiv.setAttribute("style", "background-color:white; color:black;");
}
// Reset line width
var s = servers[d.title];
if (!s) return;
smoothie.seriesSet[s.index].options.lineWidth = 2;
});
nodeEnter.append("circle")
.attr("class", "circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; });
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", -32)
.attr("dy", "-1em")
.text(function(d) {return d.title});
node.exit().remove();
var link = vis.selectAll("line.link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link")
.attr("id", function(d) {return d.id; })
.style({stroke: function(d) { return d.color; }});
// Aplly color of exitsting nodes
link.style({stroke: function(d) { return d.color; }});
link.exit().remove();
vis.selectAll('g.node').forEach(function(e){$('#graph').find('svg').append(e)});
var render = function() {
var fixX = function(x) {
return (x - lx) * scale + 20 + offX;
}
var fixY = function(y) {
return (y - ly) * scale + 20 + offY;
}
var lx = 1000000, ly = 1000000, ux = -1000000, uy = -1000000;
for (var i = 0; i < nodes.length; ++i) {
if (nodes[i].x < lx) lx = nodes[i].x;
if (nodes[i].x > ux) ux = nodes[i].x;
if (nodes[i].y < ly) ly = nodes[i].y;
if (nodes[i].y > uy) uy = nodes[i].y;
}
var width = (ux - lx), height = (uy - ly);
var scale;
var offX = 0, offY = 0;
if ( (width / w) > (height / h) ) {
scale = (w - 40) / width;
offY = (h - (height * scale + 20)) / 2;
} else {
scale = (h - 40) / height;
offX = (w - (width * scale + 20)) / 2;
}
link.attr("x1", function(d) { return fixX(d.source.x); })
.attr("y1", function(d) { return fixY(d.source.y); })
.attr("x2", function(d) { return fixX(d.target.x); })
.attr("y2", function(d) { return fixY(d.target.y); });
node.attr("transform", function(d) {
if (ccCount > 10) {
ccX = ccY = ccCount = 0;
}
ccCount++;
ccX += d.x;
ccY += d.y;
return "translate(" + fixX(d.x) + "," + fixY(d.y) + ")";
});
};
force.on("tick", render);
updateBounds();
force.start();
}
window.onresize = update;
update();
}
graph = new myGraph("#graph");
var servers = {};
var serverCount = 0;
var lastTime = 0;
// Get new data
setInterval( function() {
$.get('/data.json', function(data) {
if (data.timestamp < lastTime) lastTime = data.timestamp;
if(data.timestamp === lastTime) return;
var g = data.graph;
var stats = data.servers;
updateGraph(g, data);
// updateTrafficGraph has to be called before updateTextStatistics to populate servers
updateTrafficGraph(stats, data);
updateTextStatistics(stats);
lastTime = data.timestamp;
}, 'json').always(function() {
graph.decay();
graph.update();
});
}, 2000);
// Update data of the graph
function updateGraph(g, data){
if (g) {
for (var i = 0; i < g.nodes.length; ++i) {
graph.updateNode(g.nodes[i].name);
}
for (var i = 0; i < g.edges.length; ++i) {
graph.updateLink(g.edges[i], data.timestamp);
}
pendingLinks = intLinks;
intLinks = newLinks;
newLinks = [];
}
}
// Convert bytes to GiB or TiB and return a string in form "10,23 GiB"
function bytesToString( bytes ) {
var convertedValue;
var unit;
if (bytes >= 1099511627776 ) {
convertedValue = Math.round( (bytes / 1099511627776) * 100 ) / 100 ;
unit = " TiB";
} else if (bytes >= 1073741824 ) {
convertedValue = Math.round( (bytes / 1073741824) * 100 ) / 100 ;
unit = " GiB";
} else if (bytes >= 1048576 ) {
convertedValue = Math.round( (bytes / 1048576) * 100 ) / 100 ;
unit = " MiB";
} else if ( bytes >= 1024 ) {
convertedValue = Math.round( (bytes / 1024) * 100 ) / 100 ;
unit = " KiB";
} else {
convertedValue = Math.round(bytes);
unit = " B";
}
return convertedValue + unit;
}
// Update data of the statistics divs
function updateTextStatistics(stats){
if (stats) {
for (var i = 0; i < stats.length; ++i) {
var divId = makeId("b", stats[i].address);
var server = $('#' + divId);
if (server.length == 0){
$("#statistics").append("<div id=" + makeId ("b", stats[i].address) + "></div>");
server = $("#" + divId);
}
var upload = servers[stats[i].address].uploadRate;
upload = upload ? upload : 0;
// Generate the HTML string
server.html( "<p><b> Server: " + stats[i].address + "</b><br>"
+ "Number of clients: "
+ stats[i].clientCount + "<br>"
+ "uptime: " + Math.floor(stats[i].uptime / (3600 * 24)) + "d "
+ Math.floor(stats[i].uptime / 3600) % 24 + "h " + Math.floor((stats[i].uptime) / 60) % 60 + "min" + "<br>"
+ "Sent: " + bytesToString(stats[i].bytesSent) + "<br>"
+ "Received: " + bytesToString(stats[i].bytesReceived) + "<br>"
+ "Upload: " + bytesToString(servers[stats[i].address].uploadRate) + "/s<br>"
+ "Upload Peak: " + bytesToString(servers[stats[i].address].uploadPeak) + "/s" + "</p>"
);
}
}
}
// Update the traffic graph
function updateTrafficGraph(stats, data){
if (stats) {
for (var i = 0; i < stats.length; ++i) {
var server = servers[stats[i].address];
if (!server) {
servers[stats[i].address] = server = { 'lastUptime': 0, 'lastTimestamp': 0, 'lastSent': 0, 'line': new TimeSeries({logarithmicScale: true}), 'index': serverCount++ , 'uploadRate': 0, 'downloadRate': 0, 'uploadPeak': 0 }
server.color = colorList(stats[i].address);
smoothie.addTimeSeries(server['line'], {lineWidth:2, strokeStyle: server.color});
}
// Server seems to have rebootet, redo values but add no point to chart.
if (server['lastUptime'] > stats[i].uptime) {
server['lastUptime'] = 0;
}
// Add points to graph and set the upload rate for the statistics
if (server['lastUptime'] != 0) {
// Upload rate in bytes/s
server['uploadRate'] = (stats[i].bytesSent - server['lastSent'])/(stats[i].timestamp - server.lastTimestamp) * 1000;
if (server['uploadRate'] > server['uploadPeak']) server['uploadPeak'] = server['uploadRate'];
// Rate in MiB/s
server['line'].append(new Date().getTime(), server['uploadRate']);
} else {
server['uploadRate'] = 0;
}
server['lastUptime'] = stats[i].uptime;
server['lastSent'] = stats[i].bytesSent;
server['lastTimestamp'] = stats[i].timestamp;
}
}
}
</script>