/** * Copyright (c) Richard Wall * See LICENSE for details. * * Wrappers and convenience fuctions for working with the javascriptRRD, jQuery, * and Flot charting packages. * * Designed to work well with the RRD files generated by Collectd: * - http://collectd.org/ * * Requirements: * - javascriptrrd.rrdFile: http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/rrdFile_js.html * - javascriptrrd.binaryXHR: http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/binaryXHR_js.html * - jQuery: http://jquery.com/ * - Flot: http://code.google.com/p/flot/ * * @module jarmon */ /** * A namespace for Jarmon * * @class jarmon * @static */ if(typeof(jarmon) === 'undefined') { var jarmon = {}; } jarmon.downloadBinary = function(url) { /** * Download a binary file asynchronously using the jQuery.ajax function * * @method downloadBinary * @param url {String} The url of the object to be downloaded * @return {Object} A deferred which will callback with an instance of * javascriptrrd.BinaryFile */ var d = jQuery.Deferred(); $.ajax({ url: url, dataType: 'text', cache: false, mimeType: 'text/plain; charset=x-user-defined', xhr: function() { // Save a reference to the native xhr object - we need it later // in IE to access the binary data from responseBody this._nativeXhr = jQuery.ajaxSettings.xhr(); return this._nativeXhr; }, complete: function(jqXHR, textStatus) { this._nativeXhr = null; delete this._nativeXhr; }, success: function(data, textStatus, jqXHR) { var response = this._nativeXhr.responseBody // for IE if (response==undefined) { response=this._nativeXhr.responseText // for standards-compliant browsers } d.resolve(new BinaryFile(response)); }, error: function(xhr, textStatus, errorThrown) { d.reject(new Error(textStatus + ':' + xhr.status)); } }); return d; }; jarmon.localTimeFormatter = function (v, axis) { /** * Copied from jquery.flot.js and modified to allow timezone * adjustment. * * @method localTimeFormatter * @param v {Number} The timestamp to be formatted * @param axis {Object} A hash containing information about the time axis * @return {String} The formatted datetime string **/ // map of app. size of time units in milliseconds var timeUnitSize = { "second": 1000, "minute": 60 * 1000, "hour": 60 * 60 * 1000, "day": 24 * 60 * 60 * 1000, "month": 30 * 24 * 60 * 60 * 1000, "year": 365.2425 * 24 * 60 * 60 * 1000 }; // Offset the input timestamp by the user defined amount var d = new Date(v + axis.options.tzoffset); // first check global format if (axis.options.timeformat !== null) { return $.plot.formatDate( d, axis.options.timeformat, axis.options.monthNames); } var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; var span = axis.max - axis.min; var suffix = (axis.options.twelveHourClock) ? " %p" : ""; var fmt; if (t < timeUnitSize.minute) fmt = "%h:%M:%S" + suffix; else if (t < timeUnitSize.day) { if (span < 2 * timeUnitSize.day) fmt = "%h:%M" + suffix; else fmt = "%b %d %h:%M" + suffix; } else if (t < timeUnitSize.month) fmt = "%b %d"; else if (t < timeUnitSize.year) { if (span < timeUnitSize.year) fmt = "%b"; else fmt = "%b %y"; } else fmt = "%y"; return $.plot.formatDate(d, fmt, axis.options.monthNames); }; /** * A wrapper around an instance of javascriptrrd.RRDFile which provides a * convenient way to query the RRDFile based on time range, RRD data source (DS) * and RRD consolidation function (CF). * * @class jarmon.RrdQuery * @constructor * @param rrd {Object} A javascriptrrd.RRDFile * @param unit {String} The unit symbol for this data series **/ jarmon.RrdQuery = function(rrd, unit) { this.rrd = rrd; this.unit = unit; }; jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, dsId, cfName, transformer) { /** * Generate a Flot compatible data object containing rows between start and * end time. The rows are taken from the first RRA whose data spans the * requested time range. * * @method getData * @param startTimeJs {Number} start timestamp in microseconds * @param endTimeJs {Number} end timestamp in microseconds * @param dsId {Variant} identifier of the RRD datasource (string or number) * @param cfName {String} The name of an RRD consolidation function (CF) * eg AVERAGE, MIN, MAX * @param transformer {Function} A callable which performs a * tranfsformation of the values returned from the RRD file. * @return {Object} A Flot compatible data series * eg label: '', data: [], unit: '' **/ if (startTimeJs >= endTimeJs) { throw RangeError( ['starttime must be less than endtime.', 'starttime:', startTimeJs, 'endtime:', endTimeJs].join(' ')); } var startTime = startTimeJs/1000; var lastUpdated = this.rrd.getLastUpdate(); // default endTime to the last updated time (quantized to rrd step boundry) var endTime = lastUpdated - lastUpdated%this.rrd.getMinStep(); if(endTimeJs) { endTime = endTimeJs/1000; } if(typeof(dsId) === 'undefined' && dsId !== null) { dsId = 0; } var ds = this.rrd.getDS(dsId); if(typeof(cfName) === 'undefined' && cfName !== null) { cfName = 'AVERAGE'; } if(typeof(transformer) === 'undefined') { transformer = function(v) {return v;}; } var rra, step, rraRowCount, lastRowTime, firstRowTime; for(var i=0; i (axis.max - axis.min) ) { break; } si++; } var minVal = axis.min/Math.pow(1000, si); var maxVal = axis.max/Math.pow(1000, si); var stepSizes = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 25, 50, 100, 250]; var realStep = (maxVal - minVal)/5.0; var stepSize, decimalPlaces = 0; for(var i=0; i') .text(self.siPrefix + unit) .css({'width': '100px', 'position': 'absolute', 'top': '80px', 'left': '-110px', 'text-align': 'right'}); self.template.find('.chart').append(yaxisUnitLabel); // Manipulate and move the flot generated legend to an // alternative position. // The default legend is formatted as an HTML table, so we // grab the contents of the cells and turn them into // divs. // Actually, formatting the legend first as a one column // table is useful as it generates an optimum label element // width which we can copy to the new divs + a little extra // to accomodate the color box var legend = self.template.find('.graph-legend').show(); legend.empty(); self.template.find('.legendLabel').each( function(i, el) { var orig = $(el); var label = orig.text(); var newEl = $('
', { 'class': 'legendItem', 'title': ('Data series switch - ' + 'click to turn this data series on or off') }) .width(orig.width()+20) .text(label) .prepend( orig.prev() .find('div div') .clone().addClass('legendColorBox')) .appendTo(legend); // The legend label is clickable - to enable / // disable different data series. The disabled class // results in a label formatted with strike though if( $.inArray(label, disabled) > -1 ) { newEl.addClass('disabled'); } } ).remove(); legend.append($('
').css('clear', 'both')); self.template.find('.legend').remove(); yaxisUnitLabel.position(self.template.position()); return data; }, null) .fail( function(failure) { self.template.find('.chart').empty().hide(); self.template.find('.graph-legend').empty().hide(); self.template.find('.error').text( 'error: ' + failure.message); }) .always( function(res) { self.template.removeClass('loading'); return res; }); }; /** * Generate a form through which to choose a data source from a remote RRD file * * @class jarmon.RrdChooser * @constructor **/ jarmon.RrdChooser = function($tpl) { this.$tpl = $tpl; this.data = { rrdUrl: '', dsName: '', dsLabel: '', dsUnit:'' }; }; jarmon.RrdChooser.prototype.drawRrdUrlForm = function() { var self = this; this.$tpl.empty(); $('
').append( $('
').append( $('

').text('Enter the URL of an RRD file'), $('

', {'class': 'next'}) ) ).submit( function(e) { self.data.rrdUrl = this.rrd_url.value; var $placeholder = $(this).find('.next').empty(); new jarmon.RrdQueryRemote( self.data.rrdUrl).getDSNames().addCallback( function($placeholder, dsNames) { if(dsNames.length > 1) { $('

').text( 'The RRD file contains multiple data sources. ' + 'Choose one:').appendTo($placeholder); $(dsNames).map( function(i, el) { return $('', { type: 'button', value: el } ).click( function(e) { self.data.dsName = this.value; self.drawDsLabelForm(); } ); }).appendTo($placeholder); } else { self.data.dsName = dsNames[0]; self.drawDsLabelForm(); } }, $placeholder ).addErrback( function($placeholder, err) { $('

', {'class': 'error'}) .text(err.toString()).appendTo($placeholder); }, $placeholder ); return false; } ).appendTo(this.$tpl); }; jarmon.RrdChooser.prototype.drawDsLabelForm = function() { var self = this; this.$tpl.empty(); $('').append( $('

').text('Choose a label and unit for this data source.'), $('

').append( $('