summaryrefslogtreecommitdiff
path: root/jarmon/jarmon.js
diff options
context:
space:
mode:
authorRichard Wall <richard@largo>2011-06-12 16:42:44 +0100
committerRichard Wall <richard@largo>2011-06-12 16:42:44 +0100
commit153f51bcc0f537fab5ab059b8fb76cd34389354d (patch)
tree299a0c681f63653f0fb5a8de582f57847192530a /jarmon/jarmon.js
parent579605cb677a0345688c5421b0075b26111c4393 (diff)
parent2e0922788f4f3d34e8fe56fcdf140e59299c730c (diff)
Various changes and re-factoring towards implementation of a customisable interface. Old and unfinished but needs merging before starting work on Jquery 1.5 compatibility work.
Diffstat (limited to 'jarmon/jarmon.js')
-rw-r--r--jarmon/jarmon.js761
1 files changed, 690 insertions, 71 deletions
diff --git a/jarmon/jarmon.js b/jarmon/jarmon.js
index 406c4d1..1dda6a8 100644
--- a/jarmon/jarmon.js
+++ b/jarmon/jarmon.js
@@ -256,6 +256,18 @@ jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, dsId, cfNam
'lastUpdated': lastUpdated*1000.0};
};
+
+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.
@@ -275,23 +287,11 @@ jarmon.RrdQueryRemote = function(url, unit, downloader) {
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 )) {
+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.
+ if(!this._download) {
this._download = this.downloader(this.url)
.addCallback(
function(self, binary) {
@@ -307,9 +307,10 @@ jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) {
// 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);
+ function(self, methodName, args, rrd) {
+ var rq = new jarmon.RrdQuery(rrd, self.unit);
+ return rq[methodName].apply(rq, args);
+ }, this, methodName, args);
// Add a pair of callbacks to the current download which will callback the
// result which we setup above.
@@ -326,6 +327,35 @@ jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) {
return ret;
};
+
+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.
+ **/
+ if(this.lastUpdate < endTime/1000) {
+ this._download = null;
+ }
+ return this._callRemote('getData', [startTime, endTime, dsId, cfName]);
+};
+
+
+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');
+};
+
+
/**
* Wraps RrdQueryRemote to provide access to a different RRD DSs within a
* single RrdDataSource.
@@ -364,12 +394,17 @@ jarmon.RrdQueryDsProxy.prototype.getData = function(startTime, endTime) {
* @param options {Object} Flot options which control how the chart should be
* drawn.
**/
-jarmon.Chart = function(template, options) {
+jarmon.Chart = function(template, recipe, downloader) {
this.template = template;
- this.options = jQuery.extend(true, {yaxis: {}}, options);
+ this.recipe = recipe;
+ this.downloader = downloader;
+
+ this.options = jQuery.extend(true, {yaxis: {}}, recipe.options);
this.data = [];
+ this.setup();
+
var self = this;
@@ -380,7 +415,6 @@ jarmon.Chart = function(template, options) {
self.draw();
});
-
this.options['yaxis']['ticks'] = function(axis) {
/*
* Choose a suitable SI multiplier based on the min and max values from
@@ -442,6 +476,29 @@ jarmon.Chart = function(template, options) {
};
};
+jarmon.Chart.prototype.setup = function() {
+ this.template.find('.title').text(this.recipe['title']);
+ this.data = [];
+ var recipe = this.recipe;
+ var dataDict = {};
+ for(var j=0; j<recipe['data'].length; j++) {
+ var rrd = recipe['data'][j][0];
+ var ds = recipe['data'][j][1];
+ // Test for integer DS index as opposed to DS name
+ var dsi = parseInt(ds);
+ if(ds.toString() == dsi.toString()) {
+ ds = dsi;
+ }
+ var label = recipe['data'][j][2];
+ var unit = recipe['data'][j][3];
+
+ if(typeof dataDict[rrd] == 'undefined') {
+ dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, this.downloader);
+ }
+ this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds));
+ }
+};
+
jarmon.Chart.prototype.addData = function(label, db, enabled) {
/**
* Add details of a remote RRD data source whose data will be added to this
@@ -568,13 +625,15 @@ jarmon.Chart.prototype.draw = function() {
// to accomodate the color box
var legend = self.template.find('.graph-legend').show();
legend.empty();
- self.template.find('.legendLabel')
- .each(function(i, el) {
+ 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')
+ var newEl = $('<div />', {
+ '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'))
@@ -585,8 +644,8 @@ jarmon.Chart.prototype.draw = function() {
if( $.inArray(label, disabled) > -1 ) {
newEl.addClass('disabled');
}
- })
- .remove();
+ }
+ ).remove();
legend.append($('<div />').css('clear', 'both'));
self.template.find('.legend').remove();
@@ -608,49 +667,513 @@ jarmon.Chart.prototype.draw = function() {
};
-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
- **/
+/**
+ * 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:''
+ };
+};
- var charts = [];
- var dataDict = {};
+jarmon.RrdChooser.prototype.drawRrdUrlForm = function() {
+ var self = this;
+ this.$tpl.empty();
+
+ $('<form/>').append(
+ $('<div/>').append(
+ $('<p/>').text('Enter the URL of an RRD file'),
+ $('<label/>').append(
+ 'URL: ',
+ $('<input/>', {
+ type: 'text',
+ name: 'rrd_url',
+ value: this.data.rrdUrl
+ })
+ ),
+ $('<input/>', {type: 'submit', value: 'download'}),
+ $('<div/>', {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) {
+ $('<p/>').text(
+ 'The RRD file contains multiple data sources. \
+ Choose one:').appendTo($placeholder);
+
+ $(dsNames).map(
+ function(i, el) {
+ return $('<input/>', {
+ 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) {
+ $('<p/>', {'class': 'error'}).text(err.toString()).appendTo($placeholder);
+ }, $placeholder
+ );
+ return false;
+ }
+ ).appendTo(this.$tpl);
+}
+
+jarmon.RrdChooser.prototype.drawDsLabelForm = function() {
+ var self = this;
+ this.$tpl.empty();
+
+ $('<form/>').append(
+ $('<p/>').text('Choose a label and unit for this data source.'),
+ $('<div/>').append(
+ $('<label/>').append(
+ 'Label: ',
+ $('<input/>', {
+ type: 'text',
+ name: 'dsLabel',
+ value: this.data.dslabel || this.data.dsName
+ })
+ )
+ ),
+ $('<div/>').append(
+ $('<label/>').append(
+ 'Unit: ',
+ $('<input/>', {
+ type: 'text',
+ name: 'dsUnit',
+ value: this.data.dsUnit
+ })
+ )
+ ),
+ $('<input/>', {type: 'button', value: 'back'}).click(
+ function(e) {
+ self.drawRrdUrlForm();
+ }
+ ),
+ $('<input/>', {type: 'submit', value: 'save'}),
+ $('<div/>', {class: 'next'})
+ ).submit(
+ function(e) {
+ self.data.dsLabel = this['dsLabel'].value;
+ self.data.dsUnit = this['dsUnit'].value;
+ self.drawDsSummary();
+ return false;
+ }
+ ).appendTo(this.$tpl);
+};
- var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match;
- for(i=0; i<recipes.length; i++) {
- recipe = recipes[i];
- chartData = [];
+jarmon.RrdChooser.prototype.drawDsSummary = function() {
+ var self = this;
+ this.$tpl.empty();
+
+ jQuery.each(this.data, function(i, el) {
+ $('<p/>').append(
+ $('<strong/>').text(i),
+ [': ', el].join('')
+ ).appendTo(self.$tpl);
+ });
- 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);
+ this.$tpl.append(
+ $('<input/>', {type: 'button', value: 'back'}).click(
+ function(e) {
+ self.drawDsLabelForm();
}
- 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]);
+ ),
+ $('<input/>', {type: 'button', value: 'finish'})
+ );
+};
+
+
+jarmon.ChartEditor = function($tpl, chart) {
+ this.$tpl = $tpl;
+ this.chart = chart;
+
+ $('form', this.$tpl[0]).live(
+ 'submit',
+ {self: this},
+ function(e) {
+ var self = e.data.self;
+ self.chart.recipe.title = this['title'].value;
+ self.chart.recipe.data = $(this).find('.datasources tbody tr').map(
+ function(i, el) {
+ return $(el).find('input[type=text]').map(
+ function(i, el) {
+ return el.value;
+ }
+ );
+ }
+ );
+ self.chart.setup();
+ self.chart.draw();
+ return false;
+ }
+ );
+
+ $('form', this.$tpl[0]).live(
+ 'reset',
+ {self: this},
+ function(e) {
+ var self = e.data.self;
+ self.draw();
+ return false;
+ }
+ );
+
+ $('form input[name=datasource_delete]', this.$tpl[0]).live(
+ 'click',
+ function(e) {
+ $(this).closest('tr').remove();
+ }
+ );
+
+ $('form input[name=datasource_add]', this.$tpl[0]).live(
+ 'click',
+ {self: this},
+ function(e) {
+ var self = e.data.self;
+ self._addDatasourceRow(
+ self._extractRowValues(
+ $(this).closest('tr')
+ )
+ );
+ $(this).closest('tr').find('input[type=text]').val('');
+ }
+ );
+};
+
+jarmon.ChartEditor.prototype.draw = function() {
+ var self = this;
+ this.$tpl.empty();
+
+ $('<form/>').append(
+ $('<div/>').append(
+ $('<label/>').append(
+ 'Title: ',
+ $('<input/>', {
+ type: 'text',
+ name: 'title',
+ value: this.chart.recipe.title
+ })
+ )
+ ),
+ $('<fieldset/>').append(
+ $('<legend/>').text('Data Sources'),
+ $('<table/>', {'class': 'datasources'}).append(
+ $('<thead/>').append(
+ $('<tr/>').append(
+ $('<th/>').text('RRD File'),
+ $('<th/>').text('DS Name'),
+ $('<th/>').text('DS Label'),
+ $('<th/>').text('DS Unit'),
+ $('<th/>')
+ )
+ ),
+ $('<tfoot/>').append(
+ $('<tr/>').append(
+ $('<td/>').append(
+ $('<input/>', {type: 'text'})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text'})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text'})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text'})
+ ),
+ $('<td/>').append(
+ $('<input/>', {
+ type: 'button',
+ value: 'add',
+ name: 'datasource_add'
+ })
+ )
+ )
+ ),
+ $('<tbody/>')
+ )
+ ),
+ $('<input/>', {type: 'submit', value: 'save'}),
+ $('<input/>', {type: 'reset', value: 'reset'})
+ ).appendTo(this.$tpl);
+
+ for(var i=0; i<this.chart.recipe.data.length; i++) {
+ this._addDatasourceRow(this.chart.recipe.data[i]);
+ }
+};
+
+
+jarmon.ChartEditor.prototype._extractRowValues = function($row) {
+ return $row.find('input[type=text]').map(
+ function(i, el) {
+ return el.value;
+ }
+ )
+};
+
+
+jarmon.ChartEditor.prototype._addDatasourceRow = function(record) {
+ $('<tr/>').append(
+ $('<td/>').append(
+ $('<input/>', {type: 'text', value: record[0]})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text', value: record[1]})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text', value: record[2]})
+ ),
+ $('<td/>').append(
+ $('<input/>', {type: 'text', value: record[3]})
+ ),
+ $('<td/>').append(
+ $('<input/>', {
+ type: 'button',
+ value: 'delete',
+ name: 'datasource_delete'
+ })
+ )
+ ).appendTo(this.$tpl.find('.datasources tbody'));
+};
+
+
+jarmon.TabbedInterface = function($tpl, recipe) {
+ this.$tpl = $tpl;
+ this.recipe = recipe;
+ this.placeholders = [];
+
+ this.$tabBar = $('<ul/>', {'class': 'css-tabs'}).appendTo($tpl);
+
+ // Icon and hidden input box for adding new tabs. See event handlers below.
+ this.$newTabControls = $('<li/>', {
+ 'class': 'newTabControls',
+ 'title': 'Add new tab'
+ }).append(
+ $('<img/>', {src: 'assets/icons/next.gif'}),
+ $('<input/>', {'type': 'text'}).hide()
+ ).appendTo(this.$tabBar);
+
+ this.$tabPanels = $('<div/>', {'class': 'css-panes charts'}).appendTo($tpl);
+ var tabName, $tabPanel, placeNames;
+ for(var i=0; i<recipe.length; i++) {
+ tabName = recipe[i][0];
+ placeNames = recipe[i][1];
+
+ $tabPanel = this.newTab(tabName);
+
+ for(var j=0; j<placeNames.length; j++) {
+ this.placeholders.push([
+ placeNames[j], $('<div/>').appendTo($tabPanel)]);
+ }
+ }
+
+ this.setup();
+
+ // Show the new tab name input box when the user clicks the new tab icon
+ $('ul.css-tabs > li.newTabControls > img', $tpl[0]).live(
+ 'click',
+ function(e) {
+ $(this).hide().siblings().show().focus();
+ }
+ );
+
+ // When the "new" tab input loses focus, use its value to create a new
+ // tab.
+ // XXX: Due to event bubbling, this event seems to be triggered twice, but
+ // only when the input is forcefully blurred by the "keypress" event handler
+ // below. To prevent two tabs, we blank the input field value. Tried
+ // preventing event bubbling, but there seems to be some subtle difference
+ // with the use of jquery live event handlers.
+ $('ul.css-tabs > li.newTabControls > input', $tpl[0]).live(
+ 'blur',
+ {self: this},
+ function(e) {
+ var self = e.data.self;
+ var value = this.value;
+ this.value = '';
+ $(this).hide().siblings().show();
+ if(value) {
+ self.newTab(value);
+ self.setup();
+ self.$tabBar.data("tabs").click(value);
+ }
+ }
+ );
+
+ // Unfocus the input element when return key is pressed. Triggers a
+ // blur event which then replaces the input with a tab
+ $('ul.css-tabs > li > input', $tpl[0]).live(
+ 'keypress',
+ function(e) {
+ if(e.which == 13) {
+ $(this).blur();
}
- charts.push(c);
}
+ );
+
+ // Show tab name input box when tab is double clicked.
+ $('ul.css-tabs > li > a', $tpl[0]).live(
+ 'dblclick',
+ {self: this},
+ function(e) {
+ var $originalLink = $(this);
+ var $input = $('<input/>', {
+ 'value': $originalLink.text(),
+ 'name': 'editTabTitle',
+ 'type': 'text'
+ })
+ $originalLink.replaceWith($input);
+ $input.focus();
+ }
+ );
+
+ // Handle the updating of the tab when its name is edited.
+ $('ul.css-tabs > li > input[name=editTabTitle]', $tpl[0]).live(
+ 'blur',
+ {self: this},
+ function(e) {
+ var self = e.data.self;
+ $(this).replaceWith(
+ $('<a/>', {
+ href: ['#', this.value].join('')
+ }).text(this.value)
+ )
+ self.setup();
+ self.$tabBar.data("tabs").click(this.value);
+ }
+ );
+
+ $('input[name=add_new_chart]', $tpl[0]).live(
+ 'click',
+ {self: this},
+ function(e) {
+ console.log(e);
+ }
+ );
+};
+
+jarmon.TabbedInterface.prototype.newTab = function(tabName) {
+ // Add a tab
+ $('<li/>').append(
+ $('<a/>', {href: ['#', tabName].join('')}).text(tabName)
+ ).appendTo(this.$tabBar);
+ var $placeholder = $('<div/>');
+ // Add tab panel
+ $('<div/>').append(
+ $placeholder,
+ $('<div/>', {'class': 'tab-controls'}).append(
+ $('<input/>', {
+ type: 'button',
+ value: 'Add new chart',
+ name: 'add_new_chart'
+ })
+ )
+ ).appendTo(this.$tabPanels);
+
+ return $placeholder;
+};
+
+jarmon.TabbedInterface.prototype.setup = function() {
+ this.$newTabControls.remove();
+ // Destroy then re-initialise the jquerytools tabs plugin
+ var api = this.$tabBar.data("tabs");
+ if(api) {
+ api.destroy();
+ }
+ this.$tabBar.tabs(this.$tabPanels.children('div'));
+ this.$newTabControls.appendTo(this.$tabBar);
+};
+
+
+jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes,
+ $tabTemplate, tabRecipes,
+ $controlPanelTemplate) {
+ /**
+ * Setup chart date range controls and all charts
+ **/
+ var p = new jarmon.Parallimiter(1);
+ function serialDownloader(url) {
+ return p.addCallable(jarmon.downloadBinary, [url]);
}
- return charts;
+
+ var ti = new jarmon.TabbedInterface($tabTemplate, tabRecipes);
+
+ var charts = jQuery.map(
+ ti.placeholders,
+ function(el, i) {
+ var chart = new jarmon.Chart(
+ $chartTemplate.clone().appendTo(el[1]),
+ chartRecipes[el[0]],
+ serialDownloader
+ );
+
+ $('input[name=chart_edit]', el[1][0]).live(
+ 'click',
+ {chart: chart},
+ function(e) {
+ var chart = e.data.chart;
+ new jarmon.ChartEditor(
+ chart.template.find('.graph-legend'), chart).draw();
+ }
+ );
+
+ $('input[name=chart_delete]', el[1][0]).live(
+ 'click',
+ {chart: chart},
+ function(e) {
+ var chart = e.data.chart;
+ chart.template.remove();
+ }
+ );
+
+ return chart;
+ }
+ );
+
+ var cc = new jarmon.ChartCoordinator($controlPanelTemplate, charts);
+ // Update charts when tab is clicked
+ ti.$tpl.find(".css-tabs:first").bind(
+ 'click',
+ {'cc': cc},
+ 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.
+ window.clearTimeout(cc.t);
+ cc.t = window.setTimeout(
+ function() {
+ cc.update();
+ }, 100);
+ }
+ );
+
+ // Initialise all the charts
+ cc.init();
+
+ return [charts, ti, cc];
};
@@ -720,10 +1243,10 @@ jarmon.timeRangeShortcuts = [
* @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) {
+jarmon.ChartCoordinator = function(ui, charts) {
var self = this;
this.ui = ui;
- this.charts = [];
+ this.charts = charts;
// Style and configuration of the range timeline
this.rangePreviewOptions = {
@@ -817,11 +1340,107 @@ jarmon.ChartCoordinator = function(ui) {
// 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();
- });
+ $(document).bind(
+ 'plotselected',
+ {self: this},
+ 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) ) {
+ eventSourceIsMine = true;
+ } else {
+ // ...it may come from one of the charts under my supervision
+ for(var i=0; i<self.charts.length; i++) {
+ if(self.charts[i].template.has(e.target).length > 0) {
+ eventSourceIsMine = true;
+ break;
+ }
+ }
+ }
+
+ 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(currentDate.getTime() != NaN) {
+ $(this).data('dateinput')._input_selector = input_selector;
+ $(this).data('dateinput')._initial_val = currentDate.getTime();
+ $(this).data('dateinput').setValue(currentDate);
+ break;
+ }
+ }
+ })
+ .bind('onHide', function(e) {
+ // Called after a calendar date has been chosen by the user.
+
+ // Use the sibling selector that we generated above before opening
+ // the calendar
+ var input_selector = $(this).data('dateinput')._input_selector;
+ 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()) {
+ $(this).siblings(input_selector).val(
+ newDate.toString().split(' ').slice(1,5).join(' '));
+ // Trigger a change event which should automatically update the
+ // graphs and change the timerange drop down selector to
+ // "custom"
+ $(this).siblings(input_selector).trigger('change');
+ }
+ });
+
+ // 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);
+ }
+ );
+
};