diff options
Diffstat (limited to 'jarmon')
-rw-r--r-- | jarmon/external.doc.js | 63 | ||||
-rw-r--r-- | jarmon/jarmon.js | 1055 | ||||
-rw-r--r-- | jarmon/jarmon.test.js | 137 |
3 files changed, 629 insertions, 626 deletions
diff --git a/jarmon/external.doc.js b/jarmon/external.doc.js new file mode 100644 index 0000000..10d9ea7 --- /dev/null +++ b/jarmon/external.doc.js @@ -0,0 +1,63 @@ +/** + * jQuery + * @module jQuery + * @see http://api.jquery.com/ + */ +/** + * @typedef module:jQuery.jQuery + * @see http://api.jquery.com/Types/#jQuery + */ +/** + * @typedef module:jQuery.Deferred + * @see http://api.jquery.com/category/deferred-object/ + */ +/** + * @function module:jQuery.ajax + * @see https://api.jquery.com/jQuery.ajax/ + */ + +/** + * {@link http://jquerytools.github.io/ jQuery Tools} / tabs + * @module jQueryTools/tabs + * @see http://jquerytools.github.io/documentation/tabs/index.html + */ + +/** + * {@link http://jquerytools.github.io/ jQuery Tools} / toolbox/history + * @module jQueryTools/toolbox/history + * @see http://jquerytools.github.io/documentation/toolbox/history.html + */ + +/** + * {@link http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/index.html javascriptRRD}/binaryXHR + * @module javascriptRRD/binaryXHR + * @see http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/binaryXHR_js.html + */ +/** + * @typedef module:javascriptRRD/binaryXHR.BinaryFile + * @see http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/binaryXHR_js.html#BinaryFile + */ + +/** + * {@link http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/index.html javascriptRRD}/rrdFile + * @module javascriptRRD/rrdFile + * @see http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/rrdFile_js.html + */ +/** + * @typedef module:javascriptRRD/rrdFile.RRDFile + * @see http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/rrdFile_js.html#RRDFile + */ + +/** + * {@link http://www.flotcharts.org/ Flot} + * @module Flot + * @see https://github.com/flot/flot/blob/master/API.md + */ +/** + * @typedef module:Flot.data + * @see https://github.com/flot/flot/blob/master/API.md#data-format + */ +/** + * @typedef module:Flot.options + * @see https://github.com/flot/flot/blob/master/API.md#plot-options + */ diff --git a/jarmon/jarmon.js b/jarmon/jarmon.js index e30a228..c7af4ed 100644 --- a/jarmon/jarmon.js +++ b/jarmon/jarmon.js @@ -9,233 +9,41 @@ * - http://collectd.org/ * * Requirements: - * - JavascriptRRD: http://javascriptrrd.sourceforge.net/ - * - jQuery: http://jquery.com/ - * - Flot: http://code.google.com/p/flot/ - * - * @module jarmon + * - javascriptRRD/rrdFile 0.6/1.1: http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/rrdFile_js.html + * - javascriptRRD/binaryXHR 0.6/1.1: http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/binaryXHR_js.html + * - jQuery 1.6.3: http://jquery.com/ + * - Flot 1.7: http://www.flotcharts.org/ + * - jQueryTools/tabs: http://jquerytools.github.io/documentation/tabs/index.html + * - jQueryTools/toolbox/history: http://jquerytools.github.io/documentation/toolbox/history.html */ /** * A namespace for Jarmon * - * @class jarmon - * @static + * @namespace jarmon */ 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 = - "<!-- IEBinaryToArray_ByteStr -->\r\n"+ - "<script type='text/vbscript'>\r\n"+ - "Function IEBinaryToArray_ByteStr(Binary)\r\n"+ - " IEBinaryToArray_ByteStr = CStr(Binary)\r\n"+ - "End Function\r\n"+ - "Function IEBinaryToArray_ByteStr_Last(Binary)\r\n"+ - " Dim lastIndex\r\n"+ - " lastIndex = LenB(Binary)\r\n"+ - " if lastIndex mod 2 Then\r\n"+ - " IEBinaryToArray_ByteStr_Last = Chr( AscB( MidB( Binary, lastIndex, 1 ) ) )\r\n"+ - " Else\r\n"+ - " IEBinaryToArray_ByteStr_Last = "+'""'+"\r\n"+ - " End If\r\n"+ - "End Function\r\n"+ - "</script>\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; -}; +/** + * @callback jarmon.RrdQueryRemote.Downloader + * @param url {string} The url of the object to be downloaded + * @return {module:jQuery.Deferred} A Deferred which will callback with an + * instance of {@link module:javascriptRRD/binaryXHR.BinaryFile} + */ -/* - * 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] +/** + * Download a binary file asynchronously using the {@link jQuery.ajax} + * function. * - * Original repository: http://javascriptrrd.sourceforge.net/ + * @member + * @type jarmon.RrdQueryRemote.Downloader * - * 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] + * @requires jQuery + * @requires javascriptRRD/binaryXHR */ - -// ============================================================ -// 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;i<iOffset+iLength;i++,j++) { - aStr[j] = String.fromCharCode(this.getByteAt(i)); - } - return aStr.join(""); - }; - - // Added - this.getCStringAt = function(iOffset, iMaxLength) { - var aStr = []; - for (var i=iOffset,j=0;(i<iOffset+iMaxLength) && - (this.getByteAt(i)>0);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, @@ -253,15 +61,11 @@ jarmon.downloadBinary = function(url) { 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)); + 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)); @@ -271,16 +75,18 @@ jarmon.downloadBinary = function(url) { }; +/** + * Copied from jquery.flot.js and modified to allow timezone + * adjustment. + * + * @method + * @requires Flot + * + * @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 + **/ 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, @@ -329,44 +135,37 @@ jarmon.localTimeFormatter = function (v, axis) { /** - * 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). + * A wrapper around an instance of {@link module:javascriptRRD/rrdFile.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. + * @param rrd {module:javascriptRRD/rrdFile.RRDFile} + * @param unit {string} The unit symbol for this data series **/ -jarmon.RrdQuery = function(rrd, unit, transformer) { +jarmon.RrdQuery = function(rrd, unit) { this.rrd = rrd; this.unit = unit; - if(typeof(transformer) !== 'undefined') { - this.transformer = transformer; - } else { - this.transformer = function(v) {return v;}; - } }; +/** + * 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 + * @param startTimeJs {number} start timestamp in microseconds + * @param endTimeJs {number} end timestamp in microseconds + * @param [dsId=0] {(string|number)} identifier of the RRD datasource + * @param [cfName="AVERAGE"] {string} The name of an RRD consolidation function + * (CF) eg AVERAGE, MIN, MAX + * @param {function} [transformer=function(v){return v}] A callable which + * performs a transformation of the values returned from the RRD file. + * @return {module:Flot.data} A Flot compatible data series + * eg `{label: '', data: [], unit: ''}` + **/ 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: '' - **/ - + dsId, cfName, transformer) { if (startTimeJs >= endTimeJs) { throw RangeError( ['starttime must be less than endtime.', @@ -392,6 +191,10 @@ jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, cfName = 'AVERAGE'; } + if(typeof(transformer) === 'undefined') { + transformer = function(v) {return v;}; + } + var rra, step, rraRowCount, lastRowTime, firstRowTime; for(var i=0; i<this.rrd.getNrRRAs(); i++) { @@ -450,7 +253,7 @@ jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, var val; var timestamp = startRowTime; for(i=startRowIndex; i<endRowIndex; i++) { - val = this.transformer(rra.getEl(i, dsIndex)); + val = transformer(rra.getEl(i, dsIndex)); flotData.push([timestamp*1000.0, val]); timestamp += step; } @@ -466,31 +269,29 @@ jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, }; +/** + * Return a list of RRD Data Source names + * + * @method + * @return {Array.<string>} An array of DS names. + **/ jarmon.RrdQuery.prototype.getDSNames = function() { - /** - * Return a list of RRD Data Source names - * - * @method getDSNames - * @return {Array} An array of DS names. - **/ return this.rrd.getDSNames(); }; /** - * A wrapper around RrdQuery which provides asynchronous access to the data in a - * remote RRD file. + * A wrapper around {@link jarmon.RrdQuery} which provides asynchronous access + * to the data in a remote RRD file. * - * @class jarmon.RrdQueryRemote * @constructor - * @param url {String} The url of a remote RRD file - * @param unit {String} The unit suffix of this data eg 'bit/sec' - * @param downloader {Function} A callable which returns a Deferred and calls - * back with a javascriptrrd.BinaryFile when it has downloaded. - * @param transformer {Function} A callable which performs a - * tranfsformation of the values returned from the RRD file. + * @requires javascriptRRD/rrdFile + * + * @param url {string} The url of a remote RRD file + * @param unit {string} The unit suffix of this data eg 'bit/sec' + * @param [downloader=jarmon.downloadBinary] {jarmon.RrdQueryRemote.Downloader} **/ -jarmon.RrdQueryRemote = function(url, unit, downloader, transformer) { +jarmon.RrdQueryRemote = function(url, unit, downloader) { this.url = url; this.unit = unit; if(typeof(downloader) == 'undefined') { @@ -498,13 +299,22 @@ jarmon.RrdQueryRemote = function(url, unit, downloader, transformer) { } else { this.downloader = downloader; } - this.transformer = transformer; this.lastUpdate = 0; this._download = null; }; - +/** + * Asynchronously call a method on the underlying {@link jarmon.RrdQuery}. Think of it + * as an async .apply(). + * + * @method + * @private + * @param methodName {string} + * @param args {Array} + * @return {module:jQuery.Deferred} A Deferred that calls .methodName(args...) on the + * underlying {@link jarmon.RrdQuery}. + */ jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) { // Download the rrd if there has never been a download and don't start // another download if one is already in progress. @@ -514,7 +324,7 @@ jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) { } // Set up a deferred which will call getData on the local RrdQuery object - // returning a flot compatible data object to the caller. + // returning a Flot compatible data object to the caller. var ret = jQuery.Deferred(); // Add a pair of callbacks to the current download which will callback the @@ -541,7 +351,7 @@ jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) { var rrd = new RRDFile(res); self.lastUpdate = rrd.getLastUpdate(); - var rq = new jarmon.RrdQuery(rrd, self.unit, self.transformer); + var rq = new jarmon.RrdQuery(rrd, self.unit); try { ret.resolve(rq[methodName].apply(rq, args)); } catch(e) { @@ -555,31 +365,37 @@ jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) { }; +/** + * Return a Flot compatible data series asynchronously. + * + * @method + * @param startTime {number} The start timestamp + * @param endTime {number} The end timestamp + * @param [dsId=0] {(string|number)} identifier of the RRD datasource (string or number) + * @param [cfName="AVERAGE"] {string} The name of an RRD consolidation function + * (CF) eg AVERAGE, MIN, MAX + * @param {function} [transformer=function(v){return v}] A callable which + * performs a tranfsformation of the values returned from the RRD file. + * @return {module:jQuery.Deferred} A Deferred which calls back with a Flot data + * series. + **/ jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, - dsId, cfName) { - /** - * Return a Flot compatible data series asynchronously. - * - * @method getData - * @param startTime {Number} The start timestamp - * @param endTime {Number} The end timestamp - * @param dsId {Variant} identifier of the RRD datasource (string or number) - * @return {Object} A Deferred which calls back with a flot data series. - **/ + dsId, cfName, transformer) { if(this.lastUpdate < endTime/1000) { this._download = null; } - return this._callRemote('getData', [startTime, endTime, dsId, cfName]); + return this._callRemote('getData', [startTime, endTime, dsId, cfName, transformer]); }; +/** + * Return a list of RRD Data Source names + * + * @method + * @return {module:jquery.Deferred} A Deferred which calls back with an array of + * DS names. + **/ jarmon.RrdQueryRemote.prototype.getDSNames = function() { - /** - * Return a list of RRD Data Source names - * - * @method getDSNames - * @return {Object} A Deferred which calls back with an array of DS names. - **/ return this._callRemote('getDSNames'); }; @@ -588,39 +404,45 @@ jarmon.RrdQueryRemote.prototype.getDSNames = function() { * Wraps RrdQueryRemote to provide access to a different RRD DSs within a * single RrdDataSource. * - * @class jarmon.RrdQueryDsProxy * @constructor - * @param rrdQuery {Object} An RrdQueryRemote instance - * @param dsId {Variant} identifier of the RRD datasource (string or number) + * @param rrdQuery {jarmon.RrdQueryRemote} An RrdQueryRemote instance + * @param dsId {(string|number)} identifier of the RRD datasource (string or number) **/ -jarmon.RrdQueryDsProxy = function(rrdQuery, dsId) { +jarmon.RrdQueryDsProxy = function(rrdQuery, dsId, transformer) { this.rrdQuery = rrdQuery; this.dsId = dsId; this.unit = rrdQuery.unit; + this.transformer = transformer; }; +/** + * Call I{@link RrdQueryRemote.getData} with a particular dsId + * + * @method + * @param startTime {number} A unix timestamp marking the start time + * @param endTime {number} A unix timestamp marking the start time + * @return {module:jQuery.Deferred} A Deferred which calls back with a Flot data + * series. + **/ jarmon.RrdQueryDsProxy.prototype.getData = function(startTime, endTime) { - /** - * Call I{RrdQueryRemote.getData} with a particular dsId - * - * @method getData - * @param startTime {Number} A unix timestamp marking the start time - * @param endTime {Number} A unix timestamp marking the start time - * @return {Object} A Deferred which calls back with a flot data series. - **/ - return this.rrdQuery.getData(startTime, endTime, this.dsId); + return this.rrdQuery.getData(startTime, endTime, this.dsId, undefined, this.transformer); }; /** * A class for creating a Flot chart from a series of RRD Queries * - * @class jarmon.Chart * @constructor - * @param template {Object} A jQuery containing a single element into which the - * chart will be drawn - * @param options {Object} Flot options which control how the chart should be - * drawn. + * @requires jQuery + * @requires Flot + * + * @param template {module:jQuery.jQuery} A jQuery containing a single element + * into which the chart will be drawn + * @param recipe {Object} + * @param recipe.title {string} + * @param recipe.data {Array} + * @param recipe.options {module:Flot.options} Flot options which control how + * the chart should be drawn. **/ jarmon.Chart = function(template, recipe, downloader) { this.template = template; @@ -643,6 +465,11 @@ jarmon.Chart = function(template, recipe, downloader) { self.draw(); }); + /* @todo The user should be able to override this with `ticks`, `tickSize`, + * or `minTickSize`. + * @todo Look in to implementing this as a tickFormatter, and letting Flot + * take care of scaling. + */ this.options.yaxis.ticks = function(axis) { /* * Choose a suitable SI multiplier based on the min and max values from @@ -660,7 +487,7 @@ jarmon.Chart = function(template, recipe, downloader) { }; var si = 0; while(true) { - if( Math.pow(1000, si+1)*0.9 > axis.max ) { + if( Math.pow(1000, si+1)*0.9 > (axis.max - axis.min) ) { break; } si++; @@ -670,7 +497,7 @@ jarmon.Chart = function(template, recipe, downloader) { 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]; + 1, 2, 5, 10, 20, 25, 50, 100, 250]; var realStep = (maxVal - minVal)/5.0; var stepSize, decimalPlaces = 0; @@ -705,6 +532,9 @@ jarmon.Chart = function(template, recipe, downloader) { }; }; +/** + * @method + */ jarmon.Chart.prototype.setup = function() { this.template.find('.title').text(this.recipe.title); this.data = []; @@ -724,38 +554,38 @@ jarmon.Chart.prototype.setup = function() { if(typeof(dataDict[rrd]) === 'undefined') { dataDict[rrd] = new jarmon.RrdQueryRemote( - rrd, unit, this.downloader, transformer); + rrd, unit, this.downloader); } - this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)); + this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds, transformer)); } }; +/** + * Add details of a remote RRD data source whose data will be added to this + * chart. + * + * @method + * @param label {string} The label for this data which will be shown in the + * chart legend + * @param db {string} The url of the remote RRD database + * @param [enabled=true] {boolean} true if you want this data plotted on the chart, + * false if not. + **/ jarmon.Chart.prototype.addData = function(label, db, enabled) { - /** - * Add details of a remote RRD data source whose data will be added to this - * chart. - * - * @method addData - * @param label {String} The label for this data which will be shown in the - * chart legend - * @param db {String} The url of the remote RRD database - * @param enabled {Boolean} true if you want this data plotted on the chart, - * false if not. - **/ if(typeof(enabled) === 'undefined') { enabled = true; } this.data.push([label, db, enabled]); }; +/** + * Enable / Disable a single data source + * + * @method + * @param label {string} The label of the data source to be enabled / + * disabled. + **/ jarmon.Chart.prototype.switchDataEnabled = function(label) { - /** - * Enable / Disable a single data source - * - * @method switchDataEnabled - * @param label {String} The label of the data source to be enabled / - * disabled. - **/ for(var i=0; i<this.data.length; i++) { if(this.data[i][0] === label) { this.data[i][2] = !this.data[i][2]; @@ -763,29 +593,31 @@ jarmon.Chart.prototype.switchDataEnabled = function(label) { } }; +/** + * Alter the time range of this chart and redraw + * + * @method + * @param startTime {number} The start timestamp + * @param endTime {number} The end timestamp + * @return {Object} The same thing as .draw() returns + **/ jarmon.Chart.prototype.setTimeRange = function(startTime, endTime) { - /** - * Alter the time range of this chart and redraw - * - * @method setTimeRange - * @param startTime {Number} The start timestamp - * @param endTime {Number} The end timestamp - **/ this.startTime = startTime; this.endTime = endTime; return this.draw(); }; +/** + * Draw the chart + * + * - A 'chart_loading' event is triggered before the data is requested + * - A 'chart_loaded' event is triggered when the chart has been drawn + * + * @method + * @return {module:jQuery.Deferred} A Deferred which calls back with the chart data + * when the chart has been rendered. + **/ jarmon.Chart.prototype.draw = function() { - /** - * Draw the chart - * A 'chart_loading' event is triggered before the data is requested - * A 'chart_loaded' event is triggered when the chart has been drawn - * - * @method draw - * @return {Object} A Deferred which calls back with the chart data when - * the chart has been rendered. - **/ var self = this; this.template.addClass('loading'); @@ -839,6 +671,7 @@ jarmon.Chart.prototype.draw = function() { self.template.find('.chart').empty().show(), data, self.options); + // @todo Make this styleable var yaxisUnitLabel = $('<div>') .text(self.siPrefix + unit) .css({'width': '100px', @@ -848,7 +681,7 @@ jarmon.Chart.prototype.draw = function() { 'text-align': 'right'}); self.template.find('.chart').append(yaxisUnitLabel); - // Manipulate and move the flot generated legend to an + // 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 @@ -857,6 +690,10 @@ jarmon.Chart.prototype.draw = function() { // 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 + // + // @todo This seems super hacky + // @todo Look in to implementing this using Flot + // `.legend.container`. var legend = self.template.find('.graph-legend').show(); legend.empty(); self.template.find('.legendLabel').each( @@ -908,8 +745,12 @@ jarmon.Chart.prototype.draw = function() { /** * Generate a form through which to choose a data source from a remote RRD file * - * @class jarmon.RrdChooser + * @todo Create an example that uses this. + * * @constructor + * @requires jQuery + * + * @param $tpl {module:jQuery.jQuery} jQuery object for the DOM node to attach to. **/ jarmon.RrdChooser = function($tpl) { this.$tpl = $tpl; @@ -921,6 +762,9 @@ jarmon.RrdChooser = function($tpl) { }; }; +/** + * @method + */ jarmon.RrdChooser.prototype.drawRrdUrlForm = function() { var self = this; this.$tpl.empty(); @@ -980,6 +824,9 @@ jarmon.RrdChooser.prototype.drawRrdUrlForm = function() { ).appendTo(this.$tpl); }; +/** + * @method + */ jarmon.RrdChooser.prototype.drawDsLabelForm = function() { var self = this; this.$tpl.empty(); @@ -1024,6 +871,9 @@ jarmon.RrdChooser.prototype.drawDsLabelForm = function() { }; +/** + * @method + */ jarmon.RrdChooser.prototype.drawDsSummary = function() { var self = this; this.$tpl.empty(); @@ -1046,6 +896,15 @@ jarmon.RrdChooser.prototype.drawDsSummary = function() { }; +/** + * UI element to toggle datasources in a graph. + * + * @constructor + * @requires jQuery + * + * @param $tpl {module:jQuery.jQuery} + * @param chart {jarmon.Chart} + */ jarmon.ChartEditor = function($tpl, chart) { this.$tpl = $tpl; this.chart = chart; @@ -1103,6 +962,10 @@ jarmon.ChartEditor = function($tpl, chart) { ); }; + +/** + * @method + */ jarmon.ChartEditor.prototype.draw = function() { var self = this; this.$tpl.empty(); @@ -1166,6 +1029,12 @@ jarmon.ChartEditor.prototype.draw = function() { }; +/** + * @method + * @private + * @param $row {module:jQuery.jQuery} + * @return {module:jQuery.jQuery} + */ jarmon.ChartEditor.prototype._extractRowValues = function($row) { return $row.find('input[type=text]').map( function(i, el) { @@ -1175,6 +1044,11 @@ jarmon.ChartEditor.prototype._extractRowValues = function($row) { }; +/** + * @method + * @private + * @param record {Array} + */ jarmon.ChartEditor.prototype._addDatasourceRow = function(record) { $('<tr/>').append( $('<td/>').append( @@ -1200,9 +1074,33 @@ jarmon.ChartEditor.prototype._addDatasourceRow = function(record) { }; +/** + * + * The `recipe` is an array of `["Tab Name", [placeholder-name-list]]` + * pairs. For example: + * + * recipe = [ + * ["System", ["cpu", "memory", "load"]], + * ["Network", ["interface"]] + * ]; + * + * @constructor + * @requires jQuery + * @requires jQueryTools/tabs + * @requires jQueryTools/toolbox/history + * + * @param $tpl {module:jQuery.jQuery} + * @param recipe {Array.<Array.<string, Array.<string>>>} An array of + * `["Tab Name", [placeholder-name-list]]` pairs. + */ jarmon.TabbedInterface = function($tpl, recipe) { this.$tpl = $tpl; this.recipe = recipe; + /** + * An array of `["placeholder-name", placeholder]` pairs. + * @member + * @type {Array.<Array.<string, module:jQuery.jQuery>>} + */ this.placeholders = []; this.$tabBar = $('<ul/>', {'class': 'css-tabs'}).appendTo($tpl); @@ -1223,6 +1121,11 @@ jarmon.TabbedInterface = function($tpl, recipe) { this.setup(); }; +/** + * @method + * @param tabName {string} + * @return {module:jQuery.jQuery} A <div> that is the contents of the tab panel. + */ jarmon.TabbedInterface.prototype.newTab = function(tabName) { // Add a tab $('<li/>').append( @@ -1235,6 +1138,9 @@ jarmon.TabbedInterface.prototype.newTab = function(tabName) { return $placeholder; }; +/** + * @method + */ jarmon.TabbedInterface.prototype.setup = function() { // Destroy then re-initialise the jquerytools tabs plugin var api = this.$tabBar.data("tabs"); @@ -1244,15 +1150,30 @@ jarmon.TabbedInterface.prototype.setup = function() { this.$tabBar.tabs(this.$tabPanels.children('div'), {history: true}); }; +/** + * Setup chart date range controls and all charts + * + * @todo Figure out how to allow `$chartTemplate` to be an HTML5 <template> + * element. + * + * @method + * @requires jQuery + * + * @param $chartTemplate {module:jQuery.jQuery} See {@link jarmon.Chart} + * @param chartRecipes {Object.<string, recipe>} A map of + * placeholder-names (see {@link jarmon.TabbedInterface}) + * to chart recipes (see {@link jarmon.Chart}) + * @param $tabTemplate {module:jQuery.jQuery} See {@link jarmon.TabbedInterface} + * @param tabRecipes {Array} See {@link jarmon.TabbedInterface} + * @param $controlPanelTemplate {module:jQuery.jQuery} + * @return {Array.<Array.<jarmon.Chart>, jarmon.TabbedInterface, jarmon.ChartCoordinator>} + **/ jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes, $tabTemplate, tabRecipes, $controlPanelTemplate) { - /** - * Setup chart date range controls and all charts - **/ var p = new jarmon.Parallimiter(2); function serialDownloader(url) { - return p.addCallable(jarmon.downloadBinary, [url]); + return p.addCallable(function(){ return jarmon.downloadBinary(url); }); } var ti = new jarmon.TabbedInterface($tabTemplate, tabRecipes); @@ -1297,7 +1218,7 @@ jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes, function(e) { var cc = e.data.cc; // XXX: Hack to give the tab just enough time to become visible - // so that flot can calculate chart dimensions. + // so that Flot can calculate chart dimensions. window.clearTimeout(cc.t); cc.t = window.setTimeout( function() { @@ -1307,13 +1228,23 @@ jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes, ); // Initialise all the charts - cc.init(); + cc.update(); return [charts, ti, cc]; }; -// Options common to all the chart on this page +/** + * Options common to all the chart on this page + * + * This is not used by Jarmon internally; it is here for convenience to use as + * part of chart recipes passed to {@linkcode jarmon.Chart new jarmon.Chart} (or + * {@linkcode jarmon.buildTabbedChartUi}). + * + * @static + * @default + * @type module:Flot.options + */ jarmon.Chart.BASE_OPTIONS = { grid: { clickable: false, @@ -1346,7 +1277,17 @@ jarmon.Chart.BASE_OPTIONS = { } }; -// Extra options to generate a stacked chart +/** + * Extra options to generate a stacked chart + * + * This is not used by Jarmon internally; it is here for convenience to use as + * part of chart recipes passed to {@linkcode jarmon.Chart new jarmon.Chart} (or + * {@linkcode jarmon.buildTabbedChartUi}). + * + * @static + * @default + * @type module:Flot.options + */ jarmon.Chart.STACKED_OPTIONS = { series: { stack: true, @@ -1357,7 +1298,19 @@ jarmon.Chart.STACKED_OPTIONS = { }; -// A selection of useful time ranges +/** + * A selection of useful time ranges + * + * For the most part, this is an array of `["human description", function]` + * pairs. However, if a third member of a tuple is `===true`, then that tuple + * is used as the default option. If no tuple is identified as the default this + * way, then the default is the first tuple. + * + * @todo This should probably be a member of jarmon.ChartCoordinator. + * + * @member + * @default + */ jarmon.timeRangeShortcuts = [ ['last hour', function(now) { return [now-60*60*1000*1, now]; }], ['last 3 hours', function(now) { return [now-60*60*1000*3, now]; }], @@ -1374,10 +1327,39 @@ jarmon.timeRangeShortcuts = [ * Presents the user with a form and a timeline with which they can choose a * time range and co-ordinates the refreshing of a series of charts. * - * @class jarmon.ChartCoordinator + * The `ui` parameter should be a jQuery for a DOM node containing elements + * matching these selectors: + * + * - `select[name="shortcuts"]` : An empty `<select>` to be populated with + * {@linkcode jarmon.timeRangeShortcuts} + * - `[name="from"]` ; An input to select the start datetime + * - `[name="to"]` : An input to select the end datetime + * - `[name="tzoffset"]` : An input to select the timezone + * - `[name="action"]` : A button to update the charts + * - `.range-preview` : Placeholder for a Flot chart + * + * The `[name="to"]` and `[name="from"]` inputs should behave somewhat like + * WHATWG HTML (revision 2016-12-12) `type=datetime-local` inputs with `step=1`. + * Because `type=datetime-local` is not currently in a W3C spec, and not all + * common browsers implement it, and polyfills vary in quality; Jarmon avoids + * interacting with it too deeply, placing only a few requirements placed on the + * implementation: + * + * - The value MUST be settable via `el.value = string` where `string` is of the + * format `%Y-%m-%dT%H:%M:%S`. + * - The value MUST be gettable via `Date.parse(el.value+'Z')`. + * - The user interface SHOULD prohibit the user from selecting a value that is + * before than the `min` attribute, which is in the format + * `%Y-%m-%dT%H:%M:%S`. + * - The user interface SHOULD prohibit the user from selecting a value that is + * after than the `max` attribute, which is in the format `%Y-%m-%dT%H:%M:%S`. + * * @constructor - * @param ui {Object} A one element jQuery containing an input form and - * placeholders for the timeline and for the series of charts. + * @requires jQuery + * @requires Flot + * + * @param ui {module:jQuery.jQuery} A one element jQuery containing an input + * form and placeholders for the timeline and for the series of charts. **/ jarmon.ChartCoordinator = function(ui, charts) { var self = this; @@ -1401,93 +1383,98 @@ jarmon.ChartCoordinator = function(ui, charts) { } }; - var options = this.ui.find('select[name="from_standard"]'); - for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { - options.append($('<option />').text(jarmon.timeRangeShortcuts[i][0])); - } + this.inputs = { + shortcuts: this.ui.find('select[name="shortcuts"]'), + from: this.ui.find('[name="from"]'), + to: this.ui.find('[name="to"]'), + tzoffset: this.ui.find('[name="tzoffset"]'), + action: this.ui.find('[name="action"]') + }; + + // DOM manipulation //////////////////////////////////////////////////////// + + // (This is put in a closure to avoid leaking variables and making it hard + // for me to reason about the code.) + (function(options, tzoffsetEl) { + // Set up shortcuts + var option; + for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { + option = $('<option />').text(jarmon.timeRangeShortcuts[i][0]) + if (jarmon.timeRangeShortcuts[i][2] === true) { + option[0].setAttribute('selected', 'selected'); + } + options.append(option); + } + // Append a custom option for when the user selects an area of the graph + options.append($('<option />').text('custom')); + + // Populate a list of tzoffset options if the element is present in the + // template as a select list + if (tzoffsetEl.is('select')) { + var label, val; + for(i=-12; i<=12; i++) { + label = 'UTC'; + val = i; + if(val >= 0) { + label += ' + '; + } else { + label += ' - '; + } + val = Math.abs(val).toString(); + if(val.length === 1) { + label += '0'; + } + label += val + '00'; + tzoffsetEl.append( + $('<option />').attr('value', i*60*60*1000).text(label)); + } + } - // Append a custom option for when the user selects an area of the graph - options.append($('<option />').text('custom')); - // Select the first shortcut by default - options.val(jarmon.timeRangeShortcuts[0][0]); + // Default timezone offset based on localtime + var tzoffset = -1 * (new Date()).getTimezoneOffset() * 60 * 1000; + tzoffsetEl.val(tzoffset); + })(this.inputs.shortcuts, this.inputs.tzoffset); - options.bind('change', function(e) { + // Event bindings ////////////////////////////////////////////////////////// + + this.inputs.shortcuts.bind('change', function(e) { // No point in updating if the user chose custom. if($(this).val() !== 'custom') { self.update(); } }); - // Update the time ranges and redraw charts when the custom datetime inputs - // are changed - this.ui.find('[name="from_custom"]').bind('change', + this.inputs.from.bind('change', function(e) { - self.ui.find('[name="from_standard"]').val('custom'); - var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val(), 10); - self.setTimeRange( - new Date(this.value + ' UTC').getTime() - tzoffset, null); + self.inputs.shortcuts.val('custom'); self.update(); } ); - this.ui.find('[name="to_custom"]').bind('change', + this.inputs.to.bind('change', function(e) { - self.ui.find('[name="from_standard"]').val('custom'); - var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val(), 10); - self.setTimeRange( - null, new Date(this.value + ' UTC').getTime() - tzoffset); + self.inputs.shortcuts.val('custom'); self.update(); } ); - // Populate a list of tzoffset options if the element is present in the - // template as a select list - var tzoffsetEl = this.ui.find('[name="tzoffset"]'); - if(tzoffsetEl.is('select')) { - var label, val; - for(i=-12; i<=12; i++) { - label = 'UTC'; - val = i; - if(val >= 0) { - label += ' + '; - } else { - label += ' - '; - } - val = Math.abs(val).toString(); - if(val.length === 1) { - label += '0'; - } - label += val + '00'; - tzoffsetEl.append( - $('<option />').attr('value', i*60*60*1000).text(label)); - } - - tzoffsetEl.bind('change', function(e) { - self.update(); - }); - } - - // Default timezone offset based on localtime - var tzoffset = -1 * new Date().getTimezoneOffset() * 60 * 1000; - tzoffsetEl.val(tzoffset); + this.inputs.tzoffset.bind('change', function(e) { + self.update(); + }); - // Update the time ranges and redraw charts when the form is submitted - this.ui.find('[name="action"]').bind('click', function(e) { + this.inputs.action.bind('click', function(e) { self.update(); return false; }); // When a selection is made on the range timeline, or any of my charts // redraw all the charts. - $(document).bind( - 'plotselected', - {self: this}, + $(document).bind('plotselected', {}, function(e, ranges) { - var self = e.data.self; var eventSourceIsMine = false; // plotselected event may be from my range selector chart or - if( self.ui.has(e.target) ) { + if (self.ui.has(e.target) ) { eventSourceIsMine = true; } else { // ...it may come from one of the charts under my supervision @@ -1500,126 +1487,40 @@ jarmon.ChartCoordinator = function(ui, charts) { } if(eventSourceIsMine) { - // Update the prepared time range select box to value "custom" - self.ui.find('[name="from_standard"]').val('custom'); - - // Update all my charts self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to); - self.update(); } } ); - - // Add dhtml calendars to the date input fields - this.ui.find(".timerange_control img") - .dateinput({ - 'format': 'dd mmm yyyy 00:00:00', - 'max': +1, - 'css': {'input': 'jquerytools_date'}}) - .bind('onBeforeShow', function(e) { - var classes = $(this).attr('class').split(' '); - var currentDate, input_selector; - for(var i=0; i<=classes.length; i++) { - input_selector = '[name="' + classes[i] + '"]'; - // Look for a neighboring input element whose name matches the - // class name of this calendar - // Parse the value as a date if the returned date.getTime - // returns NaN we know it's an invalid date - // XXX: is there a better way to check for valid date? - currentDate = new Date($(this).siblings(input_selector).val()); - if(! isNaN(currentDate.getTime()) ) { - $(this).data( - 'dateinput')._initial_val = currentDate.getTime(); - $(this).data('dateinput').setValue(currentDate); - break; - } - } - }) - .bind( - 'onHide', - {self: this}, - function(e) { - var self = e.data.self; - // Called after a calendar date has been chosen by the user. - // Use the sibling selector that we generated above - // before opening the calendar - var oldStamp = $(this).data('dateinput')._initial_val; - var newDate = $(this).data('dateinput').getValue(); - // Only update the form field if the date has changed. - if(oldStamp !== newDate.getTime()) { - // Update the prepared time range select box to - // value "custom" - self.ui.find('[name="from_standard"]').val('custom'); - var from = null; - var to = null; - if($(this).hasClass('from_custom')){ - from = newDate.getTime(); - } else { - to = newDate.getTime(); - } - self.setTimeRange(from, to); - self.update(); - } - }); - - // Avoid overlaps between the calendars - // XXX: This is a bit of hack, what if there's more than one set of calendar - // controls on a page? - this.ui.find(".timerange_control img.from_custom").bind( - 'onBeforeShow', - {self: this}, - function(e) { - var self = e.data.self; - var otherVal = new Date( - self.ui.find('.timerange_control [name="to_custom"]').val()); - - $(this).data('dateinput').setMax(otherVal); - } - ); - this.ui.find(".timerange_control img.to_custom").bind( - 'onBeforeShow', - {self: this}, - function(e) { - var self = e.data.self; - var otherVal = new Date( - self.ui.find('.timerange_control [name="from_custom"]').val()); - - $(this).data('dateinput').setMin(otherVal); - } - ); - }; - +/** + * Grab the start and end time from the ui form, highlight the range on the + * range timeline and set the time range of all the charts and redraw. + * + * @method + * @return {module:jQuery.Deferred} + **/ jarmon.ChartCoordinator.prototype.update = function() { - /** - * Grab the start and end time from the ui form, highlight the range on the - * range timeline and set the time range of all the charts and redraw. - * - * @method update - **/ var self = this; - var selection = this.ui.find('[name="from_standard"]').val(); + var shortcut = this.inputs.shortcuts.val(); - var now = new Date().getTime(); + var now = new Date(); for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { - if(jarmon.timeRangeShortcuts[i][0] === selection) { - var range = jarmon.timeRangeShortcuts[i][1](now); - this.setTimeRange(range[0], range[1]); + if(jarmon.timeRangeShortcuts[i][0] === shortcut) { + var range = jarmon.timeRangeShortcuts[i][1](now.getTime()); + this.inputs.from.val(this.unix2datetimelocal(range[0])) + this.inputs.to.val(this.unix2datetimelocal(range[1])); break; } } - var startTime = parseInt(this.ui.find('[name="from"]').val(), 10); - var endTime = parseInt(this.ui.find('[name="to"]').val(), 10); - var tzoffset = parseInt(this.ui.find('[name="tzoffset"]').val(), 10); + var startTime = this.datetimelocal2unix(this.inputs.from.val()); + var endTime = this.datetimelocal2unix(this.inputs.to.val()); + var tzoffset = parseInt(this.inputs.tzoffset.val(), 10); - this.ui.find('[name="from_custom"]').val( - new Date(startTime + tzoffset) - .toUTCString().split(' ').slice(1,5).join(' ')); - this.ui.find('[name="to_custom"]').val( - new Date(endTime + tzoffset) - .toUTCString().split(' ').slice(1,5).join(' ')); + this.inputs.from[0].setAttribute('max', this.inputs.to.val()); + this.inputs.to[0].setAttribute( 'min', this.inputs.from.val()); + this.inputs.to[0].setAttribute( 'max', now.toISOString().slice(0, -1)); this.rangePreviewOptions.xaxis.tzoffset = tzoffset; @@ -1688,37 +1589,73 @@ jarmon.ChartCoordinator.prototype.update = function() { }); }; +/** + * Convert a UNIX millisecond timestamp into a [datetime-local string][]; using + * the `[name="tzoffoffset"]` input for the timezone. + * + * [datetime-local string]: https://html.spec.whatwg.org/multipage/infrastructure.html#concept-datetime-local + * + * @method + * @private + * @param unix {number} + * @return {string} + */ +jarmon.ChartCoordinator.prototype.unix2datetimelocal = function(unix) { + var tz = parseInt(this.inputs.tzoffset.val(), 10); + return (new Date(unix + tz)).toISOString().slice(0, -1); +} + +/** + * Convert a [datetime-local string][] into a UNIX millisecond timestamp; using + * the `[name="tzoffoffset"]` input for the timezone. + * + * [datetime-local string]: https://html.spec.whatwg.org/multipage/infrastructure.html#concept-datetime-local + * + * @method + * @private + * @param datetimelocal {string} + * @return {number} + */ +jarmon.ChartCoordinator.prototype.datetimelocal2unix = function(datetimelocal) { + var tz = parseInt(this.inputs.tzoffset.val(), 10); + return Date.parse(datetimelocal+'Z') - tz; +}; + +/** + * Set the start and end time fields in the form and trigger an update + * + * @method + * @param [startTime=] {number} The start timestamp + * @param [endTime=] {number} The end timestamp + **/ jarmon.ChartCoordinator.prototype.setTimeRange = function(from, to) { - /** - * Set the start and end time fields in the form and trigger an update - * - * @method setTimeRange - * @param startTime {Number} The start timestamp - * @param endTime {Number} The end timestamp - **/ if(typeof(from) !== 'undefined' && from !== null) { - this.ui.find('[name="from"]').val(from); + this.inputs.from.val(this.unix2datetimelocal(from)) } if(typeof(to) !== 'undefined' && to !== null) { - this.ui.find('[name="to"]').val(to); + this.inputs.to.val(this.unix2datetimelocal(to)); } + this.inputs.shortcuts.val('custom'); + this.update(); }; +/** + * An alias for {@linkcode jarmon.ChartCoordinator#update .update()}. + * + * @method + * @deprecated Use {@linkcode jarmon.ChartCoordinator#update .update()} instead. + **/ jarmon.ChartCoordinator.prototype.init = function() { - /** - * Reset all charts and the input form to the default time range - last hour - * - * @method init - **/ this.update(); }; /** * Limit the number of parallel async calls * - * @class jarmon.Parallimiter * @constructor - * @param limit {Number} The maximum number of in progress calls + * @requires jQuery + * + * @param limit {number} The maximum number of in progress calls **/ jarmon.Parallimiter = function(limit) { this.limit = limit || 1; @@ -1726,23 +1663,29 @@ jarmon.Parallimiter = function(limit) { this._currentCallCount = 0; }; +/** + * Add a function to be called when the number of in progress calls drops + * below the configured limit + * + * @todo Remove the `args` argument in favor of passing in closures. + * + * @method + * @param callable {function} A function which returns a Deferred. + * @param args {Array} A list of arguments to pass to the callable + * @return {module:jQuery.jQuery} A Deferred which fires with the result of the callable + * when it is called. + **/ jarmon.Parallimiter.prototype.addCallable = function(callable, args) { - /** - * Add a function to be called when the number of in progress calls drops - * below the configured limit - * - * @method addCallable - * @param callable {Function} A function which returns a Deferred. - * @param args {Array} A list of arguments to pass to the callable - * @return {Object} A Deferred which fires with the result of the callable - * when it is called. - **/ var d = new jQuery.Deferred(); this._callQueue.unshift([d, callable, args]); this._nextCall(); return d; }; +/** + * @method + * @private + */ jarmon.Parallimiter.prototype._nextCall = function() { var self = this; if(this._callQueue.length > 0) { diff --git a/jarmon/jarmon.test.js b/jarmon/jarmon.test.js index 5f5989d..0bccf9c 100644 --- a/jarmon/jarmon.test.js +++ b/jarmon/jarmon.test.js @@ -8,11 +8,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { Y.Test.Runner.add(new Y.Test.Case({ name: "jarmon.downloadBinary", + /** + * When url cannot be found, the deferred should errback with status + * 404. + **/ test_urlNotFound: function () { - /** - * When url cannot be found, the deferred should errback with status - * 404. - **/ var self = this; var d = new jarmon.downloadBinary('non-existent-file.html'); d.always( @@ -26,17 +26,17 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * When url is found, the deferred should callback with an instance + * of javascriptrrd.BinaryFile + **/ test_urlFound: function () { - /** - * When url is found, the deferred should callback with an instance - * of javascriptrrd.BinaryFile - **/ var self = this; var d = new jarmon.downloadBinary('testfile.bin'); d.always( function(ret) { self.resume(function() { - Y.Assert.isInstanceOf(jarmon.BinaryFile, ret); + Y.Assert.isInstanceOf(BinaryFile, ret); Y.Assert.areEqual( String.fromCharCode(0), ret.getCharAt(0)); }); @@ -69,11 +69,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }); }, + /** + * The generated rrd file should have a lastupdate date of + * 1980-01-01 00:50:01 + **/ test_getLastUpdate: function () { - /** - * The generated rrd file should have a lastupdate date of - * 1980-01-01 00:50:01 - **/ var self = this; this.d.done( function(rrd) { @@ -85,12 +85,12 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * The generated rrd file should have a single DS whose name is + * 'speed'. A RangeError is thrown if the requested index or dsName + * doesnt exist. + **/ test_getDSIndex: function () { - /** - * The generated rrd file should have a single DS whose name is - * 'speed'. A RangeError is thrown if the requested index or dsName - * doesnt exist. - **/ var self = this; this.d.done( function(rrd) { @@ -110,10 +110,10 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * The generated rrd file should have a single RRA + **/ test_getNrRRAs: function () { - /** - * The generated rrd file should have a single RRA - **/ var self = this; this.d.done( function(rrd) { @@ -124,13 +124,13 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * The generated rrd file should have a single RRA using AVERAGE + * consolidation, step=10, rows=6 and values 0-5 + * rra.getEl throws a RangeError if asked for row which doesn't + * exist. + **/ test_getRRA: function () { - /** - * The generated rrd file should have a single RRA using AVERAGE - * consolidation, step=10, rows=6 and values 0-5 - * rra.getEl throws a RangeError if asked for row which doesn't - * exist. - **/ var self = this; this.d.done( function(rrd) { @@ -168,10 +168,10 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }); }, + /** + * The starttime must be less than the endtime + **/ test_getDataTimeRangeOverlapError: function () { - /** - * The starttime must be less than the endtime - **/ var self = this; this.d.done( function(rrd) { @@ -191,11 +191,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }, + /** + * Error is raised if the rrd file doesn't contain an RRA with the + * requested consolidation function (CF) + **/ test_getDataUnknownCfError: function () { - /** - * Error is raised if the rrd file doesn't contain an RRA with the - * requested consolidation function (CF) - **/ var self = this; this.d.done( function(rrd) { @@ -214,13 +214,13 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }, + /** + * The generated rrd file should have values 0-9 at 300s intervals + * starting at 1980-01-01 00:00:00 + * Result should include a data points with times > starttime and + * <= endTime + **/ test_getData: function () { - /** - * The generated rrd file should have values 0-9 at 300s intervals - * starting at 1980-01-01 00:00:00 - * Result should include a data points with times > starttime and - * <= endTime - **/ var self = this; this.d.done( function(rrd) { @@ -266,11 +266,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * If the requested time range is outside the range of the RRD file + * we should not get any values back + **/ test_getDataUnknownValues: function () { - /** - * If the requested time range is outside the range of the RRD file - * we should not get any values back - **/ var self = this; this.d.done( function(rrd) { @@ -287,20 +287,17 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { return v * 10; }, + /** + * RrdQuery can be passed a transformer function which may + * manipulate the values from the RRDFile + **/ test_transformerFunction: function () { - /** - * RrdQuery can be passed a transformer function which may - * manipulate the values from the RRDFile - **/ var self = this; this.d.done( function(rrd) { self.resume(function() { - var rq = new jarmon.RrdQuery(rrd, '', self._x10transformer); - var data = rq.getData(RRD_STARTTIME, RRD_ENDTIME); - // Make sure that the transformer is the - // function we asked for - Y.Assert.areEqual(self._x10transformer, rq.transformer); + var rq = new jarmon.RrdQuery(rrd, ''); + var data = rq.getData(RRD_STARTTIME, RRD_ENDTIME, undefined, undefined, self._x10transformer); // Real data goes 0,1,2,3... // should be transformed to 0,10,20... Y.Assert.areEqual(0, data.data[0][1]); @@ -321,10 +318,10 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.rq = new jarmon.RrdQueryRemote('build/test.rrd', ''); }, + /** + * The starttime must be less than the endtime + **/ test_getDataTimeRangeOverlapError: function () { - /** - * The starttime must be less than the endtime - **/ var self = this; this.rq.getData(1, 0).fail( function(res) { @@ -336,11 +333,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }, + /** + * Error is raised if the rrd file doesn't contain an RRA with the + * requested consolidation function (CF) + **/ test_getDataUnknownCfError: function () { - /** - * Error is raised if the rrd file doesn't contain an RRA with the - * requested consolidation function (CF) - **/ var self = this; this.rq.getData(RRD_STARTTIME, RRD_ENDTIME, 0, 'FOO').always( function(res) { @@ -352,13 +349,13 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { }, + /** + * The generated rrd file should have values 0-9 at 300s intervals + * starting at 1980-01-01 00:00:00 + * Result should include a data points with times > starttime and + * <= endTime + **/ test_getData: function () { - /** - * The generated rrd file should have values 0-9 at 300s intervals - * starting at 1980-01-01 00:00:00 - * Result should include a data points with times > starttime and - * <= endTime - **/ var self = this; this.rq.getData(RRD_STARTTIME + (RRD_STEP+1) * 1000, RRD_ENDTIME - (RRD_STEP-1) * 1000).always( @@ -399,11 +396,11 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { this.wait(); }, + /** + * If the requested time range is outside the range of the RRD file + * we should not get any values back + **/ test_getDataUnknownValues: function () { - /** - * If the requested time range is outside the range of the RRD file - * we should not get any values back - **/ var self = this; this.rq.getData(RRD_ENDTIME, RRD_ENDTIME+1000).always( function(data) { |