/** * 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: http://javascriptrrd.sourceforge.net/ * - 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 = {}; } // A VBScript and Javascript helper function to convert IE responseBody to a // byte string. // http://miskun.com/javascript/internet-explorer-and-binary-files-data-access/ var IEBinaryToArray_ByteStr_Script = "\r\n"+ "\r\n"; document.write(IEBinaryToArray_ByteStr_Script); jarmon.GetIEByteArray_ByteStr = function(IEByteArray) { if(typeof(jarmon.ByteMapping) === 'undefined') { jarmon.ByteMapping = {}; for ( var i = 0; i < 256; i++ ) { for ( var j = 0; j < 256; j++ ) { jarmon.ByteMapping[ String.fromCharCode( i + j * 256 ) ] = String.fromCharCode(i) + String.fromCharCode(j); } } } var rawBytes = IEBinaryToArray_ByteStr(IEByteArray); var lastChr = IEBinaryToArray_ByteStr_Last(IEByteArray); return rawBytes.replace(/[\s\S]/g, function( match ) { return jarmon.ByteMapping[match]; }) + lastChr; }; /* * BinaryFile over XMLHttpRequest * Part of the javascriptRRD package * Copyright (c) 2009 Frank Wuerthwein, fkw@ucsd.edu * MIT License [http://www.opensource.org/licenses/mit-license.php] * * Original repository: http://javascriptrrd.sourceforge.net/ * * Based on: * Binary Ajax 0.1.5 * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, * http://blog.nihilogic.dk/ * MIT License [http://www.opensource.org/licenses/mit-license.php] */ // ============================================================ // Exception class jarmon.InvalidBinaryFile = function(msg) { this.message=msg; this.name="Invalid BinaryFile"; }; // pretty print jarmon.InvalidBinaryFile.prototype.toString = function() { return this.name + ': "' + this.message + '"'; }; // ===================================================================== // BinaryFile class // Allows access to element inside a binary stream jarmon.BinaryFile = function(strData, iDataOffset, iDataLength) { var data = strData; var dataOffset = iDataOffset || 0; var dataLength = 0; // added var doubleMantExpHi=Math.pow(2,-28); var doubleMantExpLo=Math.pow(2,-52); var doubleMantExpFast=Math.pow(2,-20); if (typeof strData === "string") { dataLength = iDataLength || data.length; } else { throw new jarmon.InvalidBinaryFile( "Unsupported type " + (typeof strData)); } this.getRawData = function() { return data; }; this.getByteAt = function(iOffset) { return data.charCodeAt(iOffset + dataOffset) & 0xFF; }; this.getLength = function() { return dataLength; }; this.getSByteAt = function(iOffset) { var iByte = this.getByteAt(iOffset); if (iByte > 127) return iByte - 256; else return iByte; }; this.getShortAt = function(iOffset) { var iShort = ( this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset); if (iShort < 0) iShort += 65536; return iShort; }; this.getSShortAt = function(iOffset) { var iUShort = this.getShortAt(iOffset); if (iUShort > 32767) return iUShort - 65536; else return iUShort; }; this.getLongAt = function(iOffset) { var iByte1 = this.getByteAt(iOffset), iByte2 = this.getByteAt(iOffset + 1), iByte3 = this.getByteAt(iOffset + 2), iByte4 = this.getByteAt(iOffset + 3); var iLong = (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1; if (iLong < 0) iLong += 4294967296; return iLong; }; this.getSLongAt = function(iOffset) { var iULong = this.getLongAt(iOffset); if (iULong > 2147483647) return iULong - 4294967296; else return iULong; }; this.getStringAt = function(iOffset, iLength) { var aStr = []; for (var i=iOffset,j=0;i0);i++,j++) { aStr[j] = String.fromCharCode(this.getByteAt(i)); } return aStr.join(""); }; // Added this.getDoubleAt = function(iOffset) { var iByte1 = this.getByteAt(iOffset), iByte2 = this.getByteAt(iOffset + 1), iByte3 = this.getByteAt(iOffset + 2), iByte4 = this.getByteAt(iOffset + 3), iByte5 = this.getByteAt(iOffset + 4), iByte6 = this.getByteAt(iOffset + 5), iByte7 = this.getByteAt(iOffset + 6), iByte8 = this.getByteAt(iOffset + 7); var iSign=iByte8 >> 7; var iExpRaw=((iByte8 & 0x7F)<< 4) + (iByte7 >> 4); var iMantHi=((((((iByte7 & 0x0F) << 8) + iByte6) << 8) + iByte5) << 8) + iByte4; var iMantLo=((((iByte3) << 8) + iByte2) << 8) + iByte1; if (iExpRaw===0) return 0.0; if (iExpRaw===0x7ff) return undefined; var iExp=(iExpRaw & 0x7FF)-1023; var dDouble = ((iSign===1)?-1:1)*Math.pow(2,iExp)*(1.0 + iMantLo*doubleMantExpLo + iMantHi*doubleMantExpHi); return dDouble; }; // added // Extracts only 4 bytes out of 8, loosing in precision (20 bit mantissa) this.getFastDoubleAt = function(iOffset) { var iByte5 = this.getByteAt(iOffset + 4), iByte6 = this.getByteAt(iOffset + 5), iByte7 = this.getByteAt(iOffset + 6), iByte8 = this.getByteAt(iOffset + 7); var iSign=iByte8 >> 7; var iExpRaw=((iByte8 & 0x7F)<< 4) + (iByte7 >> 4); var iMant=((((iByte7 & 0x0F) << 8) + iByte6) << 8) + iByte5; if (iExpRaw===0) return 0.0; if (iExpRaw===0x7ff) return undefined; var iExp=(iExpRaw & 0x7FF)-1023; var dDouble = ((iSign===1)?-1:1)*Math.pow(2,iExp)*(1.0 + iMant*doubleMantExpFast); return dDouble; }; this.getCharAt = function(iOffset) { return String.fromCharCode(this.getByteAt(iOffset)); }; }; 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) { // In IE we return the responseBody if(typeof(this._nativeXhr.responseBody) !== 'undefined') { d.resolve( new jarmon.BinaryFile( jarmon.GetIEByteArray_ByteStr( this._nativeXhr.responseBody))); } else { d.resolve(new jarmon.BinaryFile(data)); } }, 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 * @param transformer {Function} A callable which performs a * tranfsformation of the values returned from the RRD file. **/ jarmon.RrdQuery = function(rrd, unit, transformer) { this.rrd = rrd; this.unit = unit; if(typeof(transformer) !== 'undefined') { this.transformer = transformer; } else { this.transformer = function(v) {return v;}; } }; jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, dsId, cfName) { /** * 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 * @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'; } var rra, step, rraRowCount, lastRowTime, firstRowTime; for(var i=0; i axis.max ) { 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, 5, 10, 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( $('