/*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);