/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
"use strict";
// ## Graph view for a Dataset using Flot graphing library.
//
// Initialization arguments (in a hash in first parameter):
//
// * model: recline.Model.Dataset
// * state: (optional) configuration hash of form:
//
// {
// group: {column name for x-axis},
// series: [{column name for series A}, {column name series B}, ... ],
// // options are: lines, points, lines-and-points, bars, columns
// graphType: 'lines',
// graphOptions: {custom [flot options]}
// }
//
// NB: should *not* provide an el argument to the view but must let the view
// generate the element itself (you can then append view.el to the DOM.
my.Flot = Backbone.View.extend({
template: ' \
\
\
\
{{#t.flot_info}}
Hey there!
\
There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
\
Please tell us by using the menu on the right and a graph will automatically appear.
{{/t.flot_info}} \
\
\
\
',
initialize: function(options) {
var self = this;
this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
_.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
this.needToRedraw = false;
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model.fields, 'reset add', this.render);
this.listenTo(this.model.records, 'reset add', this.redraw);
var stateData = _.extend({
group: null,
// so that at least one series chooser box shows up
series: [],
graphType: 'lines-and-points'
},
options.state
);
this.state = new recline.Model.ObjectState(stateData);
this.previousTooltipPoint = {x: null, y: null};
this.editor = new my.FlotControls({
model: this.model,
state: this.state.toJSON()
});
this.listenTo(this.editor.state, 'change', function() {
self.state.set(self.editor.state.toJSON());
self.redraw();
});
this.elSidebar = this.editor.$el;
},
render: function() {
var self = this;
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON());
var htmls = Mustache.render(this.template, tmplData);
this.$el.html(htmls);
this.$graph = this.$el.find('.panel.graph');
this.$graph.on("plothover", this._toolTip);
return this;
},
remove: function () {
this.editor.remove();
Backbone.View.prototype.remove.apply(this, arguments);
},
redraw: function() {
// There are issues generating a Flot graph if either:
// * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
// Uncaught Invalid dimensions for plot, width = 0, height = 0
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
var areWeVisible = !jQuery.expr.filters.hidden(this.el);
if ((!areWeVisible || this.model.records.length === 0)) {
this.needToRedraw = true;
return;
}
// check we have something to plot
if (this.state.get('group') && this.state.get('series')) {
var series = this.createSeries();
var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
this.plot = $.plot(this.$graph, series, options);
}
},
show: function() {
// because we cannot redraw when hidden we may need to when becoming visible
if (this.needToRedraw) {
this.redraw();
}
},
// infoboxes on mouse hover on points/bars etc
_toolTip: function (event, pos, item) {
if (item) {
if (this.previousTooltipPoint.x !== item.dataIndex ||
this.previousTooltipPoint.y !== item.seriesIndex) {
this.previousTooltipPoint.x = item.dataIndex;
this.previousTooltipPoint.y = item.seriesIndex;
$("#recline-flot-tooltip").remove();
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2);
if (this.state.attributes.graphType === 'bars') {
x = item.datapoint[1].toFixed(2),
y = item.datapoint[0].toFixed(2);
}
var template = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>');
var content = template({
group: this.state.attributes.group,
x: this._xaxisLabel(x),
series: item.series.label,
y: y
});
// use a different tooltip location offset for bar charts
var xLocation, yLocation;
if (this.state.attributes.graphType === 'bars') {
xLocation = item.pageX + 15;
yLocation = item.pageY - 10;
} else if (this.state.attributes.graphType === 'columns') {
xLocation = item.pageX + 15;
yLocation = item.pageY;
} else {
xLocation = item.pageX + 10;
yLocation = item.pageY - 20;
}
$('' + content + '
').css({
top: yLocation,
left: xLocation
}).appendTo("body").fadeIn(200);
}
} else {
$("#recline-flot-tooltip").remove();
this.previousTooltipPoint.x = null;
this.previousTooltipPoint.y = null;
}
},
_xaxisLabel: function (x) {
if (this._groupFieldIsDateTime()) {
// oddly x comes through as milliseconds *string* (rather than int
// or float) so we have to reparse
x = new Date(parseFloat(x)).toLocaleDateString();
} else if (this.xvaluesAreIndex) {
x = parseInt(x, 10);
// HACK: deal with bar graph style cases where x-axis items were strings
// In this case x at this point is the index of the item in the list of
// records not its actual x-axis value
x = this.model.records.models[x].get(this.state.attributes.group);
}
return x;
},
// ### getGraphOptions
//
// Get options for Flot Graph
//
// needs to be function as can depend on state
//
// @param typeId graphType id (lines, lines-and-points etc)
// @param numPoints the number of points that will be plotted
getGraphOptions: function(typeId, numPoints) {
var self = this;
var groupFieldIsDateTime = self._groupFieldIsDateTime();
var xaxis = {};
if (!groupFieldIsDateTime) {
xaxis.tickFormatter = function (x) {
// convert x to a string and make sure that it is not too long or the
// tick labels will overlap
// TODO: find a more accurate way of calculating the size of tick labels
var label = self._xaxisLabel(x) || "";
if (typeof label !== 'string') {
label = label.toString();
}
if (self.state.attributes.graphType !== 'bars' && label.length > 10) {
label = label.slice(0, 10) + "...";
}
return label;
};
}
// for labels case we only want ticks at the label intervals
// HACK: however we also get this case with Date fields. In that case we
// could have a lot of values and so we limit to max 15 (we assume)
if (this.xvaluesAreIndex) {
var numTicks = Math.min(this.model.records.length, 15);
var increment = this.model.records.length / numTicks;
var ticks = [];
for (var i=0; i \
\
\
',
templateSeriesEditor: ' \
\
\
',
events: {
'change form select': 'onEditorSubmit',
'click .editor-add': '_onAddSeries',
'click .action-remove-series': 'removeSeries'
},
initialize: function(options) {
var self = this;
_.bindAll(this, 'render');
this.listenTo(this.model.fields, 'reset add', this.render);
this.state = new recline.Model.ObjectState(options.state);
this.render();
},
render: function() {
var self = this;
var tmplData = this.model.toTemplateJSON();
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
var htmls = Mustache.render(this.template, tmplData);
this.$el.html(htmls);
// set up editor from state
if (this.state.get('graphType')) {
this._selectOption('.editor-type', this.state.get('graphType'));
}
if (this.state.get('group')) {
this._selectOption('.editor-group', this.state.get('group'));
}
// ensure at least one series box shows up
var tmpSeries = [""];
if (this.state.get('series').length > 0) {
tmpSeries = this.state.get('series');
}
_.each(tmpSeries, function(series, idx) {
self.addSeries(idx);
self._selectOption('.editor-series.js-series-' + idx, series);
});
return this;
},
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
var options = this.$el.find(id + ' select > option');
if (options) {
options.each(function(opt){
if (this.value == value) {
$(this).attr('selected','selected');
return false;
}
});
}
},
onEditorSubmit: function(e) {
var select = this.$el.find('.editor-group select');
var $editor = this;
var $series = this.$el.find('.editor-series select');
var series = $series.map(function () {
return $(this).val();
});
var updatedState = {
series: $.makeArray(series),
group: this.$el.find('.editor-group select').val(),
graphType: this.$el.find('.editor-type select').val()
};
this.state.set(updatedState);
},
// Public: Adds a new empty series select box to the editor.
//
// @param [int] idx index of this series in the list of series
//
// Returns itself.
addSeries: function (idx) {
var data = _.extend({
seriesIndex: idx,
seriesName: String.fromCharCode(idx + 64 + 1)
}, this.model.toTemplateJSON());
data = I18nMessages('recline', recline.View.translations).injectMustache(data);
var htmls = Mustache.render(this.templateSeriesEditor, data);
this.$el.find('.editor-series-group').append(htmls);
return this;
},
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
},
// Public: Removes a series list item from the editor.
//
// Also updates the labels of the remaining series elements.
removeSeries: function (e) {
e.preventDefault();
var $el = $(e.target);
$el.parent().parent().remove();
this.onEditorSubmit();
}
});
})(jQuery, recline.View);