diff options
author | Richard Wall <richard@aziz> | 2010-08-22 22:38:05 +0100 |
---|---|---|
committer | Richard Wall <richard@aziz> | 2010-08-22 22:38:05 +0100 |
commit | 54b3c39b40077b045e7d10b78eaf85b11dad5a3a (patch) | |
tree | 613de63bd5fa0976e138be2601bd4caf991e61f9 /jarmon/jarmon.js | |
parent | fb130e633439f43a733f5de3bd36aa4f34a89fe2 (diff) | |
parent | 352f0ea3d709eced87f598b4058d43aff0d5664a (diff) |
Merge ~richardw/jarmon/auto-apidocumentationv10.8pre1
* Cleanup and add further doc strings for compatibility with yuidoc
* Add a tool to automatically download and run yuidoc on the source tree
* Add a tool to automatically create a releasable source archive containing apidocs
* Add documentation of the apidoc and release tools.
* Rearrange the source tree - moving all examples into the docs/examplesfolder
Diffstat (limited to 'jarmon/jarmon.js')
-rw-r--r-- | jarmon/jarmon.js | 954 |
1 files changed, 954 insertions, 0 deletions
diff --git a/jarmon/jarmon.js b/jarmon/jarmon.js new file mode 100644 index 0000000..44c4dad --- /dev/null +++ b/jarmon/jarmon.js @@ -0,0 +1,954 @@ +/** + * Copyright (c) 2010 Richard Wall <richard (at) the-moon.net> + * 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(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. + * + * @method getData + * @param startTime {Number} start timestamp + * @param endTime {Number} end timestamp + * @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: '' + **/ + 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<this.rrd.getNrRRAs(); i++) { + // Look through all RRAs looking for the most suitable + // data resolution. + rra = this.rrd.getRRA(i); + + // If this rra doesn't use the requested CF then move on to the next. + if(rra.getCFName() != cfName) { + continue; + } + + step = rra.getStep(); + rraRowCount = rra.getNrRows(); + firstUpdated = lastUpdated - (rraRowCount - 1) * step; + // We assume that the RRAs are listed in ascending order of time range, + // therefore the first RRA which contains the range minimum should give + // the highest resolution data for this range. + if(firstUpdated <= startTimestamp) { + break; + } + } + // If we got to the end of the loop without ever defining step, it means + // that the CF check never succeded. + if(!step) { + throw new Error('Unrecognised consolidation function: ' + cfName); + } + + var startRow = rraRowCount - parseInt((lastUpdated - Math.max(startTimestamp, firstUpdated))/step) - 1; + var endRow = rraRowCount - parseInt((lastUpdated - endTimestamp)/step) - 1; + + var flotData = []; + var timestamp = firstUpdated + (startRow - 1) * step; + var dsIndex = ds.getIdx(); + for (var i=startRow; i<=endRow; i++) { + flotData.push([timestamp*1000.0, rra.getEl(i, dsIndex)]); + timestamp += step; + } + + // Now get the date of the earliest record in entire rrd file, ie that of + // the last (longest range) rra. + rra = this.rrd.getRRA(this.rrd.getNrRRAs()-1); + firstUpdated = lastUpdated - (rra.getNrRows() -1) * rra.getStep(); + + return {'label': ds.getName(), 'data': flotData, 'unit': this.unit, + 'firstUpdated': firstUpdated*1000.0, + 'lastUpdated': lastUpdated*1000.0}; +}; + +/** + * A wrapper around 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. + **/ +jarmon.RrdQueryRemote = function(url, unit, downloader) { + this.url = url; + this.unit = unit; + this.downloader = downloader; + this.lastUpdate = 0; + this._download = null; +}; + +jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) { + /** + * 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. + **/ + var endTimestamp = endTime/1000; + + // Download the rrd if there has never been a download or if the last + // completed download had a lastUpdated timestamp less than the requested + // end time. + // Don't start another download if one is already in progress. + if(!this._download || (this._download.fired > -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 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) + **/ +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 + * + * @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); +}; + + +/** + * 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. + **/ +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<stepSizes.length; i++) { + stepSize = stepSizes[i] + if( realStep < stepSize ) { + if(stepSize < 10) { + decimalPlaces = 2; + } + break; + } + } + + if(self.options.yaxis.tickDecimals != null) { + decimalPlaces = self.options.yaxis.tickDecimals; + } + + var tickMin = minVal - minVal % stepSize; + var tickMax = maxVal - maxVal % stepSize + stepSize + + var ticks = []; + for(var j=tickMin; j<=tickMax; j+=stepSize) { + ticks.push([ + j*Math.pow(1000, si), + j.toFixed(decimalPlaces) + ]); + } + + self.siPrefix = siPrefixes[si]; + + return ticks; + }; +}; + +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]); +}; + +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]; + } + } +}; + +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(); +} + +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. + **/ + this.template.addClass('loading'); + + var result; + var results = []; + for(var i=0; i<this.data.length; i++) { + if(this.data[i][2]) { + result = this.data[i][1].getData(this.startTime, this.endTime); + } else { + // If the data source has been marked as disabled return a fake + // empty dataset + // 0 values so that it can contribute to a stacked chart. + // 0 linewidth so that it doesn't cause a line in stacked chart + result = new MochiKit.Async.Deferred(); + result.callback({ + data: [ + [this.startTime, 0], + [this.endTime, 0] + ], + lines: { + lineWidth: 0 + } + }); + } + + results.push(result); + } + + return MochiKit.Async.gatherResults(results) + .addCallback( + function(self, data) { + var i, label, disabled = []; + unit = ''; + for(i=0; i<data.length; i++) { + label = self.data[i][0]; + if(label) { + data[i].label = label; + } + if(typeof data[i].unit != 'undefined') { + // Just use the last unit for now + unit = data[i].unit; + } + if(!self.data[i][2]) { + disabled.push(label); + } + } + + $.plot(self.template.find('.chart'), data, self.options); + + var yaxisUnitLabel = $('<div>').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 = $('<div />') + .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($('<div />').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 static factory method to generate a list of I{Chart} from a list of + * recipes and a list of available rrd files in collectd path format. + * + * @method fromRecipe + * @param recipes {Array} A list of recipe objects. + * @param templateFactory {Function} A callable which generates an html + * template for a chart. + * @param downloader {Function} A download function which returns a Deferred + * @return {Array} A list of Chart objects + **/ + + var charts = []; + var dataDict = {}; + + var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match; + + for(i=0; i<recipes.length; i++) { + recipe = recipes[i]; + chartData = []; + + for(j=0; j<recipe['data'].length; j++) { + rrd = recipe['data'][j][0]; + ds = recipe['data'][j][1]; + label = recipe['data'][j][2]; + unit = recipe['data'][j][3]; + if(typeof dataDict[rrd] == 'undefined') { + dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, downloader); + } + chartData.push([label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)]); + } + if(chartData.length > 0) { + template = templateFactory(); + template.find('.title').text(recipe['title']); + c = new jarmon.Chart(template, recipe['options']); + for(j=0; j<chartData.length; j++) { + c.addData.apply(c, chartData[j]); + } + charts.push(c); + } + } + return charts; +}; + + +// Options common to all the chart on this page +jarmon.Chart.BASE_OPTIONS = { + grid: { + clickable: false, + borderWidth: 1, + borderColor: "#000", + color: "#000", + backgroundColor: "#fff", + tickColor: "#eee" + }, + legend: { + position: 'nw', + noColumns: 1 + }, + selection: { + mode: 'x' + }, + series: { + points: { show: false }, + lines: { + show: true, + steps: false, + shadowSize: 0, + lineWidth: 1 + }, + shadowSize: 0 + }, + xaxis: { + mode: "time", + tickFormatter: jarmon.localTimeFormatter + } +}; + +// Extra options to generate a stacked chart +jarmon.Chart.STACKED_OPTIONS = { + series: { + stack: true, + lines: { + fill: 0.5 + } + } +}; + + +// A selection of useful time ranges +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]; }], + ['last 6 hours', function(now) { return [now-60*60*1000*6, now]; }], + ['last 12 hours', function(now) { return [now-60*60*1000*12, now]; }], + ['last day', function(now) { return [now-60*60*1000*24, now]; }], + ['last week', function(now) { return [now-60*60*1000*24*7, now]; }], + ['last month', function(now) { return [now-60*60*1000*24*31, now]; }], + ['last year', function(now) { return [now-60*60*1000*24*365, now]; }] +]; + + +/** + * 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 + * @constructor + * @param ui {Object} A one element jQuery containing an input form and + * placeholders for the timeline and for the series of charts. + **/ +jarmon.ChartCoordinator = function(ui) { + var self = this; + this.ui = ui; + this.charts = []; + + // Style and configuration of the range timeline + this.rangePreviewOptions = { + grid: { + borderWidth: 1 + }, + selection: { + mode: 'x' + }, + xaxis: { + mode: 'time', + tickFormatter: jarmon.localTimeFormatter + }, + yaxis: { + ticks: [] + } + }; + + 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])); + } + + // 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]); + + options.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', + function(e) { + self.ui.find('[name="from_standard"]').val('custom'); + var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val()); + self.setTimeRange(new Date(this.value + ' UTC').getTime() - tzoffset, null); + self.update(); + } + ); + + this.ui.find('[name="to_custom"]').bind('change', + function(e) { + self.ui.find('[name="from_standard"]').val('custom'); + var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val()); + self.setTimeRange(null, new Date(this.value + ' UTC').getTime() - tzoffset); + self.update(); + } + ); + + // Populate a list of tzoffset options if the element is present in the + // template as a select list + tzoffsetEl = this.ui.find('[name="tzoffset"]'); + if(tzoffsetEl.is('select')) { + var label, val; + for(var 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); + + // Update the time ranges and redraw charts when the form is submitted + this.ui.find('[name="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. + this.ui.bind("plotselected", function(event, ranges) { + self.ui.find('[name="from_standard"]').val('custom'); + self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to); + self.update(); + }); +}; + + +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 selection = this.ui.find('[name="from_standard"]').val(); + + var now = new Date().getTime(); + for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { + if(jarmon.timeRangeShortcuts[i][0] == selection) { + range = jarmon.timeRangeShortcuts[i][1](now); + this.setTimeRange(range[0], range[1]); + break; + } + } + + var startTime = parseInt(this.ui.find('[name="from"]').val()); + var endTime = parseInt(this.ui.find('[name="to"]').val()); + var tzoffset = parseInt(this.ui.find('[name="tzoffset"]').val()); + + 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.rangePreviewOptions.xaxis.tzoffset = tzoffset; + + var chartsLoading = []; + for(var i=0; i<this.charts.length; i++){ + this.charts[i].options.xaxis.tzoffset = tzoffset; + // Don't render charts which are not currently visible + if(this.charts[i].template.is(':visible')) { + chartsLoading.push( + this.charts[i].setTimeRange(startTime, endTime)); + } + } + return MochiKit.Async.gatherResults(chartsLoading).addCallback( + function(self, startTime, endTime, chartData) { + + var firstUpdate = new Date().getTime(); + var lastUpdate = 0; + + for(var i=0; i<chartData.length; i++) { + for(var j=0; j<chartData[i].length; j++) { + if(chartData[i][j].firstUpdated < firstUpdate) { + firstUpdate = chartData[i][j].firstUpdated; + } + if(chartData[i][j].lastUpdated > lastUpdate) { + lastUpdate = chartData[i][j].lastUpdated; + } + } + } + + var ranges = { + xaxis: { + from: Math.max(startTime, firstUpdate), + to: Math.min(endTime, lastUpdate) + } + }; + + // Add a suitable extended head and tail to preview graph time axis + var HOUR = 1000 * 60 * 60; + var DAY = HOUR * 24; + var WEEK = DAY * 7; + var MONTH = DAY * 31; + var YEAR = DAY * 365; + var periods = [HOUR, HOUR*6, HOUR*12, + DAY, DAY*3, + WEEK, WEEK*2, + MONTH, MONTH*3, MONTH*6, YEAR]; + + var range = ranges.xaxis.to - ranges.xaxis.from; + for(var i=0; i<periods.length; i++) { + if(range <= periods[i]) { + i++; + break; + } + } + + // Dummy data for the range timeline + var data = [ + [Math.max(ranges.xaxis.from - periods[i-1], firstUpdate), 1], + [Math.min(ranges.xaxis.to + periods[i-1], lastUpdate), 1]]; + + self.rangePreview = $.plot(self.ui.find('.range-preview'), [data], + self.rangePreviewOptions); + + self.rangePreview.setSelection(ranges, true); + }, this, startTime, endTime); +}; + +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(from != null) { + this.ui.find('[name="from"]').val(from); + } + if(to != null) { + this.ui.find('[name="to"]').val(to); + } +}; + +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 + **/ +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 + * + * @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 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]); + } + } +}; |