summaryrefslogtreecommitdiffstats
path: root/static/graph.html
diff options
context:
space:
mode:
authorSimon Rettberg2019-01-29 16:06:26 +0100
committerSimon Rettberg2019-01-29 16:06:26 +0100
commit1e8ef96e022883f056610d55055622fb22016c0c (patch)
treee932d9a1320181e11c264905eaf91babb9dba115 /static/graph.html
parentSmall labeling change (diff)
downloaddnbd3-status-1e8ef96e022883f056610d55055622fb22016c0c.tar.gz
dnbd3-status-1e8ef96e022883f056610d55055622fb22016c0c.tar.xz
dnbd3-status-1e8ef96e022883f056610d55055622fb22016c0c.zip
Make colors more good and less bad
Diffstat (limited to 'static/graph.html')
-rw-r--r--static/graph.html624
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>