/* 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/ */ if(typeof jarmon == 'undefined') { var jarmon = {}; } /** * Download a binary file asynchronously using the jQuery.ajax function * * @param url: The url of the object to be downloaded * @return: A I{MochiKit.Async.Deferred} which will callback with an instance of * I{javascriptrrd.BinaryFile} **/ jarmon.downloadBinary = function(url) { 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; }; /** * Limit the number of parallel async calls * * @param limit: The maximum number of in progress calls **/ jarmon.Parallimiter = function(limit) { this.limit = limit || 1; this._callQueue = []; this._currentCallCount = 0; }; 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 * * @param callable: A function which returns a Deferred. **/ var d = new MochiKit.Async.Deferred(); this._callQueue.unshift([d, callable, args]); this._nextCall(); return d; }; jarmon.Parallimiter.prototype._nextCall = function() { if(this._callQueue.length > 0) { if(this._currentCallCount < this.limit) { this._currentCallCount++; var nextCall = this._callQueue.pop(); nextCall[1].apply(null, nextCall[2]).addBoth( function(self, d, res) { d.callback(res); self._currentCallCount--; self._nextCall(); }, this, nextCall[0]); } } }; jarmon.localTimeFormatter = function (v, axis) { /** * Copied from jquery.flot.js and modified to allow timezone * adjustment. **/ // 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). * * @param startTime: A javascript {Date} instance representing the start of query * time range, or {null} to return earliest available data. * @param endTime: A javascript {Date} instance representing the end of query * time range, or {null} to return latest available data. * @param dsId: A {String} name of an RRD DS or an {Int} DS index number or * {null} to return the first available DS. * @param cfName: A {String} name of an RRD consolidation function * @return: A flot compatible data series object **/ jarmon.RrdQuery = function(rrd, unit) { this.rrd = rrd; this.unit = unit; }; jarmon.RrdQuery.prototype.getData = function(startTime, endTime, 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. * * @param startTime: The I{Date} start time * @param endTime: The I{Date} end time * @param dsId: An index I{Number} or key I{String} identifying the RRD * datasource (DS). * @param cfName: The name I{String} of an RRD consolidation function (CF) * eg AVERAGE, MIN, MAX * @return: A Flot compatible data series I{Object} * eg {label:'', data:[], unit: ''} **/ var startTimestamp = startTime/1000; var lastUpdated = this.rrd.getLastUpdate(); var endTimestamp = lastUpdated; if(endTime) { endTimestamp = endTime/1000; // If end time stamp is beyond the range of this rrd then reset it if(lastUpdated < endTimestamp) { endTimestamp = lastUpdated; } } if(dsId == null) { dsId = 0; } var ds = this.rrd.getDS(dsId); if(cfName == null) { cfName = 'AVERAGE'; } var rra, step, rraRowCount, firstUpdated; for(var i=0; i -1 && this.lastUpdate < endTimestamp )) { this._download = this.downloader(this.url) .addCallback( function(self, binary) { // Upon successful download convert the resulting binary // into an RRD file and pass it on to the next callback // in the chain. var rrd = new RRDFile(binary); self.lastUpdate = rrd.getLastUpdate(); return rrd; }, this); } // Set up a deferred which will call getData on the local RrdQuery object // returning a flot compatible data object to the caller. var ret = new MochiKit.Async.Deferred().addCallback( function(self, startTime, endTime, dsId, rrd) { return new jarmon.RrdQuery(rrd, self.unit).getData(startTime, endTime, dsId); }, this, startTime, endTime, dsId); // Add a pair of callbacks to the current download which will callback the // result which we setup above. this._download.addBoth( function(ret, res) { if(res instanceof Error) { ret.errback(res); } else { ret.callback(res); } return res; }, ret); return ret; }; /** * Wraps a I{RrdQueryRemote} to provide access to a different RRD DSs within a * single RrdDataSource. * * @param rrdQuery: An I{RrdQueryRemote} * @param dsId: An index or keyname of an RRD DS **/ jarmon.RrdQueryDsProxy = function(rrdQuery, dsId) { this.rrdQuery = rrdQuery; this.dsId = dsId; this.unit = rrdQuery.unit; }; jarmon.RrdQueryDsProxy.prototype.getData = function(startTime, endTime) { /** * Call I{RrdQueryRemote.getData} with a particular dsId **/ return this.rrdQuery.getData(startTime, endTime, this.dsId); }; /** * A class for creating a Flot chart from a series of RRD Queries * * @param template: A I{jQuery} containing a single element into which the chart * will be drawn * @param options: An I{Object} containing Flot options which describe how the * chart should be drawn. **/ jarmon.Chart = function(template, options) { this.template = template; this.options = jQuery.extend(true, {yaxis: {}}, options); this.data = []; var self = this; // Listen for clicks on the legend items - onclick enable / disable the // corresponding data source. $('.graph-legend .legendItem', this.template[0]).live('click', function(e) { self.switchDataEnabled($(this).text()); self.draw(); }); this.options['yaxis']['ticks'] = function(axis) { /** * Choose a suitable SI multiplier based on the min and max values from * the axis and then generate appropriate yaxis tick labels. * * @param axis: An I{Object} with min and max properties * @return: An array of ~5 tick labels **/ var siPrefixes = { 0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T' } var si = 0; while(true) { if( Math.pow(1000, si+1)*0.9 > 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'); legend.empty(); self.template.find('.legendLabel') .each(function(i, el) { var orig = $(el); var label = orig.text(); var newEl = $('
') .attr('class', 'legendItem') .attr('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.text('error: ' + failure.message); }, this) .addBoth( function(self, res) { self.template.removeClass('loading'); return res; }, this); }; jarmon.Chart.fromRecipe = function(recipes, templateFactory, downloader) { /** * A factory function to generate a list of I{Chart} from a list of recipes * and a list of available rrd files in collectd path format. * * @param rrdUrlList: A list of rrd download paths * @param recipes: A list of recipe objects * @param templateFactory: A callable which generates an html template for a * chart. **/ var charts = []; var dataDict = {}; var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match; for(i=0; i 0) { template = templateFactory(); template.find('.title').text(recipe['title']); c = new jarmon.Chart(template, recipe['options']); for(j=0; j').text(jarmon.timeRangeShortcuts[i][0])); } // Append a custom option for when the user selects an area of the graph options.append($('