', {'class': 'css-tabs'}).appendTo($tpl);
this.$tabPanels = $('', {'class': 'css-panes charts'}).appendTo($tpl);
var tabName, $tabPanel, placeNames;
for(var i=0; i').appendTo($tabPanel)]);
}
}
this.setup();
};
jarmon.TabbedInterface.prototype.newTab = function(tabName) {
// Add a tab
$('').append(
$('', {href: ['#', tabName].join('')}).text(tabName)
).appendTo(this.$tabBar);
var $placeholder = $('');
// Add tab panel
$('').append($placeholder).appendTo(this.$tabPanels);
return $placeholder;
};
jarmon.TabbedInterface.prototype.setup = function() {
// 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'));
};
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]);
}
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];
};
// 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, charts) {
var self = this;
this.ui = ui;
this.charts = 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').text(jarmon.timeRangeShortcuts[i][0]));
}
// Append a custom option for when the user selects an area of the graph
options.append($('').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(), 10);
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(), 10);
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
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(
$('').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.
$(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 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(! 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);
}
);
};
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 now = new Date().getTime();
for(var i=0; i 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(i=0; i 0) {
if(this._currentCallCount < this.limit) {
this._currentCallCount++;
var nextCall = this._callQueue.pop();
nextCall[1].apply(null, nextCall[2]).always(
function(res) {
nextCall[0].resolve(res);
self._currentCallCount--;
self._nextCall();
});
}
}
};