lib/visage-app/public/javascripts/graph.js in visage-app-0.3.3 vs lib/visage-app/public/javascripts/graph.js in visage-app-0.9.0.pre1

- old
+ new

@@ -1,45 +1,156 @@ +function formatSeriesLabel(labels) { + var host = labels[0], + plugin = labels[1], + instance = labels[2], + metric = labels[3], + name; + + // Generic label building + name = instance + name = name.replace(plugin, '') + name = name.replace(plugin.split('-')[0], '') + name = name.replace('tcp_connections', '') + name = name.replace('ps_state', '') + name += metric == "value" ? "" : " (" + metric + ")" + name = name.replace(/^[-|_]*/, '') + name = name.trim().replace(/^\((.*)\)$/, '$1') + name = plugin == "irq" ? name.replace(/^/, 'irq ') : '' + + // Plugin specific labeling + if (plugin == "interface") { + name += instance.replace(/^if_(.*)-(.*)/, '$2 $1') + ' (' + metric + ')' + } + if (["processes", "memory"].contains(plugin) || plugin.test(/^cpu-\d+/) ) { + name += instance.split('-')[1] + } + if (plugin == "swap") { + if (instance.test(/^swap_io/)) { + name += instance.replace(/^swap_(\w*)-(.*)$/, '$1 $2') + } + if (instance.test(/^swap-/)) { + name += instance.split('-')[1] + } + } + if (plugin == "load") { + name += metric.replace(/^\((.*)\)$/, '$1') + } + if (plugin.test(/^disk/)) { + name += instance.replace(/^disk_/, '') + ' (' + metric + ')' + } + if (["entropy","users"].contains(plugin)) { + name += metric + } + if (plugin == "uptime") { + name += instance + } + if (plugin == "ping") { + if (instance.test(/^ping_/)) { + name += instance.replace(/^ping_(.*)-(.*)$/, '$1 $2') + } else { + name += metric + ' ' + instance.split('-')[1] + } + } + if (plugin == "vmem") { + if (instance.test(/^vmpage_number-/)) { + name += instance.replace(/^vmpage_number-(.*)$/, '$1').replace('_', ' ') + } + if (instance.test(/^vmpage_io/)) { + name += instance.replace(/^vmpage_io-(.*)$/, '$1 ') + metric + } + if (instance.test(/^vmpage_faults/)) { + name += metric.trim() == "minflt" ? 'minor' : 'major' + name += ' faults' + } + } + if (plugin.test(/^tcpconns/)) { + name += instance.split('-')[1].replace('_', ' ') + } + if (plugin.test(/^tail/)) { + name += plugin.split('-').slice(1).join('-') + ' ' + name += instance.split('-').slice(1).join('-') + } + if (plugin == "apache") { + var stash = instance.split('_')[1] + if (stash.test(/^scoreboard/)) { + name += 'connections: ' + stash.split('-')[1] + } else { + name += stash + } + + } + return name.trim() +} + +function formatValue(value, places) { + var places = places ? places : 0 + switch(true) { + case (Math.abs(value) > 1125899906842624): + var label = value / 1125899906842624, + unit = 'P'; + break + case (Math.abs(value) > 1099511627776): + var label = value / 1099511627776, + unit = 'T'; + break + case (Math.abs(value) > 1073741824): + var label = value / 1073741824, + unit = 'G'; + break + case (Math.abs(value) > 1048576): + var label = value / 1048576, + unit = 'M'; + break + case (Math.abs(value) > 1024): + var label = value / 1024, + unit = 'K'; + break + default: + var label = value, + unit = ''; + break + } + + var rounded = label.round(places) + + return rounded + unit +} + +function formatDate(d) { + var datetime = new Date(d * 1000) + return datetime.format("%Y-%m-%d %H:%M:%S UTC%T") +} + + + /* * visageBase() * * Base class for fetching data and setting graph options. * Should be used by other classes to build specialised graphing behaviour. * */ var visageBase = new Class({ Implements: [Options, Events], options: { - width: 900, - height: 220, - leftEdge: 100, - topEdge: 10, - gridWidth: 670, - gridHeight: 200, - columns: 60, - rows: 8, - gridBorderColour: '#ccc', - shade: false, secureJSON: false, httpMethod: 'get', - axis: "0 0 1 1" + live: false }, initialize: function(element, host, plugin, options) { - this.parentElement = element; - this.setOptions(options); - this.options.host = host; - this.options.plugin = plugin; - this.buildGraphHeader(); - this.buildGraphContainer(); - this.canvas = Raphael(this.graphContainer, this.options.width, this.options.height); + this.parentElement = element + this.setOptions(options) + this.options.host = host + this.options.plugin = plugin data = new Hash() if($chk(this.options.start)) { data.set('start', this.options.start) } if($chk(this.options.finish)) { data.set('finish', this.options.finish) } - this.requestData = data + this.requestData = data; this.getData(); // calls graphData }, dataURL: function() { var url = ['data', this.options.host, this.options.plugin] // if the data exists on another host (useful for embedding) @@ -70,32 +181,14 @@ }.bind(this) }); this.request.send(); }, - buildGraphHeader: function() { - header = $chk(this.options.name) ? this.options.name : this.options.plugin - this.graphHeader = new Element('h3', { - 'class': 'graph-title', - 'html': header, - 'styles': { - 'color': "#121212" - } - }); - $(this.parentElement).grab(this.graphHeader); + graphName: function() { + name = $chk(this.options.name) ? this.options.name : this.options.plugin + return name }, - buildGraphContainer: function() { - $(this.parentElement).set('style', 'padding-top: 1em'); - - this.graphContainer = new Element('div', { - 'class': 'graph container', - 'styles': { - 'margin-bottom': '24px' - } - }); - $(this.parentElement).grab(this.graphContainer) - } }); /* * visageGraph() @@ -109,455 +202,321 @@ var visageGraph = new Class({ Extends: visageBase, Implements: Chain, // assemble data to graph, then draw it graphData: function(data) { + this.response = data + this.buildDataStructures() - this.ys = [] - this.colors = [] - this.instances = [] - this.metrics = [] + if ( $defined(this.chart) ) { + this.series.each(function(series, index) { + this.chart.series[index].setData(series.data) + }, this); + } else { + this.drawChart() + } + }, + buildDataStructures: function (data) { + var series = this.series = [] + var host = this.options.host + var plugin = this.options.plugin + var data = data ? data : this.response - var host = this.options.host - var plugin = this.options.plugin - $each(data[host][plugin], function(instance, iname) { $each(instance, function(metric, mname) { - this.colors.push(metric.color) - if ( !$defined(this.x) ) { - this.x = this.buildXAxis(metric) - } - this.ys.push(metric.data) - this.instances.push(iname) // labels - this.metrics.push(mname) // labels + var set = { + name: [ host, plugin, iname, mname ], + data: metric.data, + pointStart: metric.start, + pointInterval: (metric.finish - metric.start) / metric.data.length + }; + + series.push(set) }, this); }, this); - this.buildContainers(); - this.drawGraph(); - - this.buildLabels(); - this.addSelectionInterface(); - this.addDebugInterface(); - this.buildDateSelector(); - - /* disabling this for now for dramatic effect - this.buildEmbedder(); - */ + return series }, - buildXAxis: function(metric) { - var start = metric.start.toInt(), - finish = metric.finish.toInt(), - length = metric.data.length, - interval = (finish - start) / length, - counter = start, - x = [] + drawChart: function() { + var series = this.series, + title = this.graphName(), + element = this.parentElement, + ytitle = this.options.plugin + max = 0 - while (counter < finish) { - x.push(counter) - counter += interval - } - return x - }, - drawGraph: function() { - - var colors = this.colors; - var left = this.options.leftEdge - var top = this.options.topEdge - var width = this.options.gridWidth - var height = this.options.gridHeight - var x = this.x // x axis - var ys = this.ys // y axes - var xstep = x.length / 20 - var shade = this.options.shade - var axis = this.options.axis - - this.canvas.g.txtattr.font = "11px 'sans-serif'"; - this.graph = this.canvas.g.linechart(left, top, width, height, x, ys, { - nostroke: false, - width: 1.5, - axis: axis, - colors: colors, - axisxstep: xstep, - shade: shade - }); - - this.formatAxes(); - }, - formatAxes: function() { - - /* clean up graph labels */ - this.graph.axis[0].text.items.getLast().hide() - $each(this.graph.axis[0].text.items, function (time) { - - var unixTime = time.attr('text') - var d = new Date(time.attr('text') * 1000); - time.attr({'text': d.strftime("%H:%M")}); - - time.mouseover(function () { - this.attr({'text': d.strftime("%H:%M")}); - }); - - /* - time.mouseout(function () { - this.attr({'text': d.strftime("%H:%M")}); - }); - */ - }); - - $each(this.graph.axis[1].text.items, function (value) { - // FIXME: no JS reference on train means awful rounding hacks! - // if you are reading this, it's a bug! - if (value.attr('text') > 1073741824) { - var label = value.attr('text') / 1073741824; - var unit = 'g' - } else if (value.attr('text') > 1048576) { - // and again :-( - var label = value.attr('text') / 1048576; - var unit = 'm' - } else if (value.attr('text') > 1024) { - var label = value.attr('text') / 1024; - var unit = 'k'; - } else { - var label = value.attr('text'); - var unit = '' + /* Get the maximum value across all sets. + * Used later on to determine the decimal place in the label. */ + series.each(function(set) { + var setMax = set.data.max() + if ( setMax > max ) { + max = setMax } - - var decimal = label.toString().split('.') - if ($chk(this.previous) && this.previous.toString()[0] == label.toString()[0] && decimal.length > 1) { - var round = '.' + decimal[1][0] - } else { - var round = '' - } - - value.attr({'text': Math.floor(label) + round + unit}) - this.previous = value.attr('text') }); - }, - buildEmbedder: function() { - var pre = new Element('textarea', { - 'id': 'embedder', - 'class': 'embedder', - 'html': this.embedCode(), - 'styles': { - 'width': '500px', - 'padding': '3px' + this.chart = new Highcharts.Chart({ + chart: { + renderTo: element, + defaultSeriesType: 'line', + marginRight: 200, + marginBottom: 25, + zoomType: 'xy', + height: 300, + events: { + load: function(e) { + setInterval(function() { + if (this.options.live) { + this.getData() + } + }.bind(this), 10000); + }.bind(this) } - }); - this.embedderContainer.grab(pre); + }, + title: { + text: title, + style: { + fontSize: '20px', + fontWeight: 'bold', + color: "#333333" + } + }, + xAxis: { + type: 'datetime', + labels: { + y: 20, + formatter: function() { + var d = new Date(this.value * 1000) + return d.format("%H:%M") + } + }, + title: { + text: null + } + }, + yAxis: { + title: { + text: ytitle + }, + maxPadding: 0, + plotLines: [{ + width: 0.5, + }], + labels: { + formatter: function() { + var places = max < 10 ? 2 : 0 + return formatValue(this.value, places) + } + } + }, + plotOptions: { + series: { + stacking: 'normal', + marker: { + enabled: false + }, + states: { + hover: { + enabled: true, + marker: { + symbol: 'triangle' + } + } + } + } + }, + tooltip: { + formatter: function() { + var tip; + tip = '<b>' + formatSeriesLabel(this.series.name).trim() + '</b>-> ' + tip += formatValue(this.y, 2) + ' <br/>' + tip += formatDate(this.x) - var slider = new Fx.Slide(pre, { - duration: 200 - }); + return tip + } + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'top', + x: -10, + y: 60, + borderWidth: 0, + itemWidth: 186, + labelFormatter: function() { + return formatSeriesLabel(this.name) + }, + itemStyle: { + cursor: 'pointer', + color: '#333333' + }, + itemHoverStyle: { + color: '#777777' + } - slider.hide(); + }, + series: series, + credits: { + enabled: false + } + }); - var toggler = new Element('a', { - 'id': 'toggler', - 'class': 'toggler', - 'html': '(embed)', - 'href': '#', - 'styles': { - 'font-size': '0.7em', - } - }); - toggler.addEvent('click', function(e) { - e.stop(); - slider.toggle(); - }); - this.embedderTogglerContainer.grab(toggler); + this.buildDateSelector(); }, - embedCode: function() { - baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol}); - code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl}); - code += "<div id='graph'></div>" - code += "<script type='text/javascript'>window.addEvent('domready', function() { var graph = new visageGraph('graph', '{host}', '{plugin}', ".substitute({'host': this.options.host, 'plugin': this.options.plugin}); - code += "{" - code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl}); - code += "}); });</script>" - return code.replace('<', '&lt;').replace('>', '&gt;') - }, - addDebugInterface: function() { - var graph = this.graph; + buildDateSelector: function() { /* - graph.hoverColumn(function () { - }); - */ - }, - addSelectionInterface: function() { - var graph = this.graph; - var parentElement = this.parentElement - var gridHeight = this.options.gridHeight - graph.selectionMade = true - this.graph.clickColumn(function () { - if ($chk(graph.selectionMade) && graph.selectionMade) { - if ($defined(graph.selection)) { - graph.selection.remove(); - } - graph.selectionMade = false - graph.selection = this.paper.rect(this.x, 0, 1, gridHeight); - graph.selection.toBack(); - graph.selection.attr({fill: '#555', stroke: '#555', opacity: 0.4}); - graph.selectionStart = this.axis.toInt() - } else { - graph.selectionMade = true - graph.selectionFinish = this.axis.toInt() - var select = $(parentElement).getElement('div.timescale.container select') - var hasSelected = select.getChildren('option').some(function(option) { - return option.get('html') == 'selected' - }); - if (!hasSelected) { - var option = new Element('option', { - html: 'selected', - value: '', - selected: true + * container + * \ + * - form + * \ + * - select + * | \ + * | - option + * | | + * | - option + * | + * - submit + */ + var currentDate = new Date; + var currentUnixTime = parseInt(currentDate.getTime() / 1000); + + var container = $(this.parentElement); + var form = new Element('form', { + 'method': 'get', + 'events': { + 'submit': function(e, foo) { + e.stop(); + e.target.getElement('select').getSelected().each(function(option) { + data = new Hash() + split = option.value.split('=') + data.set(split[0], split[1]) }); - select.grab(option) - } + this.requestData = data + + /* Draw everything again. */ + this.getData(); + }.bind(this) } }); - this.graph.hoverColumn(function () { - if ($chk(graph.selection) && !graph.selectionMade) { - var width = this.x - graph.selection.attr('x'); - graph.selection.attr({'width': width}); - } - }); - }, - buildContainers: function() { - this.embedderTogglerContainer = new Element('div', { - 'class': 'embedder-toggler container', - 'styles': { - 'float': 'right', - 'width': '20%', - 'text-align': 'right', - 'margin-right': '12px', - 'padding-top': '4px' - } + var select = new Element('select', { 'class': 'date timescale' }); + var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12, + 'day': 24, '2 days': 48, '3 days': 72, + 'week': 168, '2 weeks': 336, 'month': 672 }); + timescales.each(function(hour, label) { + var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label }); + var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)}); + var html = 'last {label}'.substitute({'label': label }); + + var option = new Element('option', { + html: html, + value: value, + selected: (current ? 'selected' : '') + + }); + select.grab(option) }); - $(this.parentElement).grab(this.embedderTogglerContainer, 'top') - this.timescaleContainer = new Element('div', { - 'class': 'timescale container', + var submit = new Element('input', { 'type': 'submit', 'value': 'show' }); + + var liveToggler = new Element('input', { + 'type': 'checkbox', + 'id': this.parentElement + '-live', + 'name': 'live', + 'events': { + 'click': function() { + this.options.live = !this.options.live + }.bind(this) + }, 'styles': { - 'float': 'right', - 'width': '20%' + 'margin-left': '4px', + 'cursor': 'pointer' } }); - $(this.parentElement).grab(this.timescaleContainer, 'top') - this.labelsContainer = new Element('div', { - 'class': 'labels container', - 'title': 'click to hide', + var liveLabel = new Element('label', { + 'for': this.parentElement + '-live', + 'html': 'Live', 'styles': { - 'float': 'left', - 'margin-left': '80px', - 'padding-bottom': '1em' + 'font-family': 'sans-serif', + 'font-size': '11px', + 'margin-left': '8px', + 'cursor': 'pointer' } }); - $(this.parentElement).grab(this.labelsContainer) - this.embedderContainer = new Element('div', { - 'class': 'embedder container', + var exportLink = new Element('a', { + 'href': this.dataURL(), + 'html': 'Export data', 'styles': { - 'font-style': 'monospace', - 'margin-left': '80px', - 'font-size': '0.8em', - 'clear': 'both' - } - }); - $(this.parentElement).grab(this.embedderContainer) - }, - buildDateSelector: function() { - /* - * container - * \ - * - form - * \ - * - select - * | \ - * | - option - * | | - * | - option - * | - * - submit - */ - var currentDate = new Date; - var currentUnixTime = parseInt(currentDate.getTime() / 1000); + 'font-family': 'sans-serif', + 'font-size': '11px', + 'margin-left': '8px', + }, + 'events': { + 'mouseover': function(e) { + var url = e.target.get('href') + var options = this.requestData.toQueryString() - var container = $(this.timescaleContainer); - var form = new Element('form', { - 'method': 'get', - 'events': { - 'submit': function(e, foo) { - e.stop(); - - /* - * Get the selected option, turn it into a hash for - * getData() to use. - */ - data = new Hash() - if (e.target.getElement('select').getSelected().get('html') == 'selected') { - data.set('start', this.graph.selectionStart); - data.set('finish', this.graph.selectionFinish); - } else { - e.target.getElement('select').getSelected().each(function(option) { - split = option.value.split('=') - data.set(split[0], split[1]) - currentTimePeriod = option.get('html') // is this setting a global? - }, this); - } - this.requestData = data - - /* Nuke graph + labels. */ - this.graph.remove(); - delete this.x; - $(this.labelsContainer).empty(); - $(this.timescaleContainer).empty(); - $(this.embedderContainer).empty(); - $(this.embedderTogglerContainer).empty(); - if ($defined(this.graph.selection)) { - this.graph.selection.remove(); - } - /* Draw everything again. */ - this.getData(); - }.bind(this) + if ( options != '' && ! url.contains('?') ) { + url += '?' + options } - }); - var select = new Element('select', { 'class': 'date timescale' }); - var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12, - 'day': 24, '2 days': 48, '3 days': 72, - 'week': 168, '2 weeks': 336, 'month': 672 }); - timescales.each(function(hour, label) { - var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label }); - var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)}); - var html = 'last {label}'.substitute({'label': label }); + e.target.set('href', url) + }.bind(this) + } + }); - var option = new Element('option', { - html: html, - value: value, - selected: (current ? 'selected' : '') - - }); - select.grab(option) - }); - - var submit = new Element('input', { 'type': 'submit', 'value': 'show' }); - - form.grab(select); - form.grab(submit); - container.grab(form); + form.grab(select) + form.grab(submit) + form.grab(liveToggler) + form.grab(liveLabel) + form.grab(exportLink) + container.grab(form, 'top') }, - buildLabels: function() { - //buildLabels: function(graphLines, instanceNames, dataSources, colors) { - this.ys.each(function(set, index) { - var path = this.graph.lines[index], - color = this.colors[index] - plugin = this.options.plugin - instance = this.instances[index] - metric = this.metrics[index] - var container = new Element('div', { - 'class': 'label plugin', - 'styles': { - 'padding': '0.2em 0.5em 0', - 'float': 'left', - 'width': '180px', - 'font-size': '0.8em' - }, - 'events': { - 'mouseover': function(e) { - e.stop(); - path.animate({'stroke-width': 3}, 300); - //path.toFront(); - }, - 'mouseout': function(e) { - e.stop(); - path.animate({'stroke-width': 1.5}, 300); - //path.toBack(); - }, - 'click': function(e) { - e.stop(); - path.attr('opacity') == 0 ? path.animate({'opacity': 1}, 350) : path.animate({'opacity': 0}, 350); - } - } - }); - var box = new Element('div', { - 'class': 'label plugin box ' + metric, - 'html': '&nbsp;', - 'styles': { - 'background-color': color, - 'width': '48px', - 'height': '18px', - 'float': 'left', - 'margin-right': '0.5em' - } - }); +}); - // plugin/instance/metrics names can be unmeaningful. make them pretty - var name; - name = instance.replace(plugin, ''); - name = name.replace('tcp_connections', '') - name = name.replace('ps_state', '') - name = name.replace(plugin.split('-')[0], '') - name += metric == "value" ? "" : " (" + metric + ")" - name = name.replace(/^[-|_]*/, '') +// buildEmbedder: function() { +// var pre = new Element('textarea', { +// 'id': 'embedder', +// 'class': 'embedder', +// 'html': this.embedCode(), +// 'styles': { +// 'width': '500px', +// 'padding': '3px' +// } +// }); +// this.embedderContainer.grab(pre); +// +// var slider = new Fx.Slide(pre, { +// duration: 200 +// }); +// +// slider.hide(); +// +// var toggler = new Element('a', { +// 'id': 'toggler', +// 'class': 'toggler', +// 'html': '(embed)', +// 'href': '#', +// 'styles': { +// 'font-size': '0.7em', +// } +// }); +// toggler.addEvent('click', function(e) { +// e.stop(); +// slider.toggle(); +// }); +// this.embedderTogglerContainer.grab(toggler); +// }, +// embedCode: function() { +// baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol}); +// code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl}); +// code += "<div id='graph'></div>" +// code += "<script type='text/javascript'>window.addEvent('domready', function() { var graph = new visageGraph('graph', '{host}', '{plugin}', ".substitute({'host': this.options.host, 'plugin': this.options.plugin}); +// code += "{" +// code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl}); +// code += "}); });</script>" +// return code.replace('<', '&lt;').replace('>', '&gt;') +// }, - var desc = new Element('span', { - 'class': 'label plugin description ' + metric, - 'html': name - }); - - container.grab(box); - container.grab(desc); - $(this.labelsContainer).grab(container); - - }, this); - } -}) - -var visageSparkline = new Class({ - Extends: visageGraph, - options: { - width: 450, - height: 80, - leftEdge: 1, - topEdge: 1, - gridWidth: 449, - gridHeight: 79, - columns: 60, - rows: 8, - gridBorderColour: '#ccc', - shade: false, - secureJSON: false, - httpMethod: 'get', - axis: "0 0 0 0" - }, - graphData: function(data) { - - this.ys = [] - this.colors = [] - this.instances = [] - this.metrics = [] - - var host = this.options.host - var plugin = this.options.plugin - - $each(data[host][plugin], function(instance, iname) { - $each(instance, function(metric, mname) { - this.colors.push(metric.color) - if ( !$defined(this.x) ) { - this.x = this.buildXAxis(metric) - } - this.ys.push(metric.data) - this.instances.push(iname) // labels - this.metrics.push(mname) // labels - }, this); - }, this); - - this.drawGraph(); - } -});