diff options
Diffstat (limited to 'static/graph.html')
-rw-r--r-- | static/graph.html | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/static/graph.html b/static/graph.html new file mode 100644 index 0000000..0fe1a09 --- /dev/null +++ b/static/graph.html @@ -0,0 +1,624 @@ +<!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 } + .link { stroke-opacity: 0.75; stroke: #56f; } + .circle { stroke: #56f; stroke-width: 1px; } + .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 id="debug"></div></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": 230, "b": 20}; +// peaking links (red) +var peakColor = { "r": 255, "g": 40, "b": 30}; + +var clusterDirections = []; + +// 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 getCluster(ip) { + var p = ip.split('.', 4); + var i; + if (p.length !== 4) return false; + p = p[0] + '-' + p[1] + '-' + p[2]; + for (i = 0; i < clusterDirections.length; ++i) { + if (clusterDirections[i].id === p) + return function() { return clusterDirections[i] }; + } + clusterDirections.push({'id':p}); + var len = clusterDirections.length; + var col = len > 10 ? d3.scale.category20() : d3.scale.category10(); + for (i = 0; i < len; ++i) { + clusterDirections[i].x = Math.sin(2*(i/clusterDirections.length)*3.14159); + clusterDirections[i].y = Math.cos(2*(i/clusterDirections.length)*3.14159); + clusterDirections[i].color = col(clusterDirections[i].id); + } + i = clusterDirections.length - 1; + return function() { return clusterDirections[i] }; +} + +function myGraph(el) { + var changed = false; + + // Add/update and remove elements on the graph object + this.updateNode = function (id) { + var title = id; + var id = makeId("a", id); + + var node = findNode(id); + + // Add node, if it doesn't exist + if (!node) { + var len = Math.min(5, nodes.length); + var nx = 0, ny = 0, i; + if (len > 0) { + for (i = 0; i < len; ++i) { + nx += nodes[i].x; + ny += nodes[i].y; + } + nx /= len; + ny /= len; + } + nodes.push(node = {"id": id, "title": title, "x": nx, "y":ny, "cluster":getCluster(title)}); + changed = true; + } else if (!node.isServer && servers[title]) { + node.isServer = true; + node.cluster = false; + node.color = servers[title].color; + // Change color and radius if server. + var visNode = vis.select("#" + id); + if (visNode) { + visNode.select("circle") + .attr("r", 15) + .style("stroke-width", "3px"); + } + } + 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 width = '2.5px'; + + // Change color to red for links between servers + if (servers[sourceId] && servers[targetId]) width = '3.5px'; + + sourceId = makeId("a", sourceId); + targetId = makeId("a", targetId); + var sourceNode = findNode(sourceId); + var targetNode = findNode(targetId); + + if ((typeof(sourceNode) !== 'undefined') && (typeof(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": idleColor, "downloadRate" : 0, "lastReceived": 0, "lastTimestamp": 0, "lastUptime": 0, "colorIntensity": 0, "width": width }); + 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 + 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); + } + } + } + window.setInterval(decayLinkColor, 500); + window.setInterval(function() { + if (force.alpha() == 0) { + force.alpha(0.015); + force.tick(); + force.stop(); + } + }, 125); + + + // 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) + // Desired distance of the links + .distance(function(bla) { if (bla.source.isServer && bla.target.isServer) return 10; return 4; }) + // Strength of the links (how strongly the distance is enforced) (0-1) + .linkStrength(0.5) + // Force with which all nodes attract each other + .charge(function(bla) { return bla.isServer ? -3.5 : -0.9; }) + // Friction for the graph nodes + .friction(0.4) + .theta(0.75) + .size([w, h]); + + nodes = force.nodes(); + links = force.links(); + + + var updateCounter = 0; + this.update = function () { + if(changed) { + update(); + } + if (++updateCounter < 10) force.alpha(force.alpha() + 0.05); + } + + // Update the complete svg. + var svgNodes = []; + var svgLinks = []; + function update() { + changed = false; + + svgNodes = vis.selectAll("g.node") + .data(nodes, function(d) { return d.id; }); + + var nodeEnter = svgNodes.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", 4); + + nodeEnter.append("text") + .attr("class", "nodetext") + .attr("dx", -32) + .attr("dy", "-1em") + .text(function(d) {return d.title}); + + svgNodes.exit().remove(); + + svgLinks = vis.selectAll("line.link") + .data(links, function(d) { return d.source.id + "-" + d.target.id; }); + + svgLinks.enter().insert("line") + .attr("class", "link") + .attr("id", function(d) {return d.id; }) + .style({stroke: function(d) { return d.color }, "stroke-width": function(d) { return d.width } }); + + svgLinks.exit().remove(); + + // Put all nodes on top again by adding them again + var jGraph = $('#graph').find('svg'); + vis.selectAll('g.node').forEach(function(e){jGraph.append(e)}); + svgNodes.selectAll('circle').style({ "fill": function(d) { return d.color ? d.color : d.cluster().color } }); + + force.on("tick", renderTick); + + updateBounds(); + force.start(); + } + + var lastScale = false; + // Render function + function renderTick(e) { + var k = 1 * e.alpha; + nodes.forEach(function(o, i) { + if (typeof(o.cluster) === 'function') { + var c = o.cluster(); + o.x += k * c.x; + o.y += k * c.y; + } + }); + var lx = 1000000, ly = 1000000, ux = -1000000, uy = -1000000; + for (var i = 0; i < links.length; ++i) { // Use links not nodes so we ignore orphans + if (links[i].source.x < lx) lx = links[i].source.x; + if (links[i].source.x > ux) ux = links[i].source.x; + if (links[i].source.y < ly) ly = links[i].source.y; + if (links[i].source.y > uy) uy = links[i].source.y; + if (links[i].target.x < lx) lx = links[i].target.x; + if (links[i].target.x > ux) ux = links[i].target.x; + if (links[i].target.y < ly) ly = links[i].target.y; + if (links[i].target.y > uy) uy = links[i].target.y; + } + var width = (ux - lx), height = (uy - ly); + var desiredScale; + var offX = 0, offY = 0; + if ( (width / w) > (height / h) ) { + desiredScale = (w - 40) / width; + } else { + desiredScale = (h - 40) / height; + } + if (lastScale === false) lastScale = desiredScale; + var absDiff = Math.abs(desiredScale - lastScale); + + var newScale = desiredScale; + if (false) { + newScale = desiredScale; + } else if (desiredScale < lastScale) { + newScale = Math.min(lastScale * 0.99, lastScale - absDiff * 0.001); + if (newScale < desiredScale) newScale = desiredScale; + } else if (desiredScale > lastScale) { + newScale = Math.max(lastScale * 1.01, lastScale + absDiff * 0.001); + if (newScale > desiredScale) newScale = desiredScale; + } + lastScale = newScale; + if ( (width / w) > (height / h) ) { + offY = (h - (height * newScale + 20)) / 2; + } else { + offX = (w - (width * newScale + 20)) / 2; + } + + var fixX = function(x) { + return (x - lx) * newScale + 20 + offX; + } + var fixY = function(y) { + return (y - ly) * newScale + 20 + offY; + } + svgLinks.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); }); + + svgNodes.attr("transform", function(d) { + return "translate(" + fixX(d.x) + "," + fixY(d.y) + ")"; + }); + }; + + 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) { + var glob = $('#gstats'); + if (glob.length === 0) { + $('#statistics').append('<div id="gstats"></div>'); + glob = $('#gstats'); + } + glob.html('<p><b>Global</b><br>' + + 'Upload: ' + bytesToString(globalUploadRate) + '/s<br>' + + 'Upload Peak: ' + bytesToString(globalUploadMax) + '/s<p>'); + 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>" + ); + } + } +} + +var globalUploadRate = 0, globalUploadMax = 0; + +// Update the traffic graph +function updateTrafficGraph(stats, data) { + if (stats) { + globalUploadRate = 0; + 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; + globalUploadRate += server['uploadRate']; + } + if (globalUploadRate > globalUploadMax) globalUploadMax = globalUploadRate; + } +} + +</script> |