/** * Copyright (c) 2010 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/ * - MochiKit.Async: http://www.mochikit.com/ * * @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 = new MochiKit.Async.Deferred(); $.ajax({ _deferredResult: d, url: url, dataType: 'text', cache: false, beforeSend: function(request) { try { request.overrideMimeType('text/plain; charset=x-user-defined'); } catch(e) { // IE doesn't support overrideMimeType } }, success: function(data) { try { this._deferredResult.callback(new BinaryFile(data)); } catch(e) { this._deferredResult.errback(e); } }, error: function(xhr, textStatus, errorThrown) { // Special case for IE which handles binary data slightly // differently. if(textStatus == 'parsererror') { if (typeof xhr.responseBody != 'undefined') { return this.success(xhr.responseBody); } } this._deferredResult.errback(new Error(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" : ""; 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) { /** * 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(dsId == null) { dsId = 0; } var ds = this.rrd.getDS(dsId); if(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: '-90px', '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; }, this) .addErrback( function(self, failure) { self.template.find('.chart').empty().hide(); self.template.find('.graph-legend').empty().hide(); self.template.find('.error').text('error: ' + failure.message); }, this) .addBoth( function(self, res) { self.template.removeClass('loading'); return res; }, this); }; /** * 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; $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( $('