From 8c6c2d7c67ef895f325091d4117e0efc3f70b082 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 8 Feb 2013 13:05:56 +0000 Subject: [PATCH] [build][s]: regular build of js and css. --- dist/recline.css | 26 ++ dist/recline.dataset.js | 8 +- dist/recline.js | 524 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 543 insertions(+), 15 deletions(-) diff --git a/dist/recline.css b/dist/recline.css index 6a7f23d7..610a56a7 100644 --- a/dist/recline.css +++ b/dist/recline.css @@ -1,3 +1,29 @@ +.recline-graph .graph { + height: 500px; + overflow: hidden; +} + +.recline-graph .legend table { + width: auto; + margin-bottom: 0; +} + +.recline-graph .legend td { + padding: 5px; + line-height: 13px; +} + +.recline-graph .graph .alert { + width: 450px; +} + +#recline-graph-tooltip { + position: absolute; + background-color: #FEE !important; + color: #000000 !important; + opacity: 0.8 !important; + border: 1px solid #fdd !important; +} .recline-graph .graph { height: 500px; } diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index da5c2a96..23fce729 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -4,6 +4,8 @@ this.recline.Model = this.recline.Model || {}; (function(my) { +var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + // ## Dataset my.Dataset = Backbone.Model.extend({ constructor: function Dataset() { @@ -47,7 +49,7 @@ my.Dataset = Backbone.Model.extend({ // Retrieve dataset and (some) records from the backend. fetch: function() { var self = this; - var dfd = new _.Deferred(); + var dfd = new Deferred(); if (this.backend !== recline.Backend.Memory) { this.backend.fetch(this.toJSON()) @@ -181,7 +183,7 @@ my.Dataset = Backbone.Model.extend({ // also returned. query: function(queryObj) { var self = this; - var dfd = new _.Deferred(); + var dfd = new Deferred(); this.trigger('query:start'); if (queryObj) { @@ -245,7 +247,7 @@ my.Dataset = Backbone.Model.extend({ this.fields.each(function(field) { query.addFacet(field.id); }); - var dfd = new _.Deferred(); + var dfd = new Deferred(); this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { if (queryResult.facets) { _.each(queryResult.facets, function(facetResult, facetId) { diff --git a/dist/recline.js b/dist/recline.js index 86137939..192b861b 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -143,6 +143,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file) (function(my) { + my.__type__ = 'csv'; // ## fetch // @@ -1390,6 +1391,8 @@ this.recline.Model = this.recline.Model || {}; (function(my) { +var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + // ## Dataset my.Dataset = Backbone.Model.extend({ constructor: function Dataset() { @@ -1433,7 +1436,7 @@ my.Dataset = Backbone.Model.extend({ // Retrieve dataset and (some) records from the backend. fetch: function() { var self = this; - var dfd = new _.Deferred(); + var dfd = new Deferred(); if (this.backend !== recline.Backend.Memory) { this.backend.fetch(this.toJSON()) @@ -1567,7 +1570,7 @@ my.Dataset = Backbone.Model.extend({ // also returned. query: function(queryObj) { var self = this; - var dfd = new _.Deferred(); + var dfd = new Deferred(); this.trigger('query:start'); if (queryObj) { @@ -1631,7 +1634,7 @@ my.Dataset = Backbone.Model.extend({ this.fields.each(function(field) { query.addFacet(field.id); }); - var dfd = new _.Deferred(); + var dfd = new Deferred(); this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { if (queryResult.facets) { _.each(queryResult.facets, function(facetResult, facetId) { @@ -1980,6 +1983,508 @@ this.recline.View = this.recline.View || {}; (function($, my) { +// ## 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}, ... ], +// graphType: 'line', +// 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: ' \ +
\ +
\ +
\ +

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.

\ +
\ +
\ +
\ +', + + initialize: function(options) { + var self = this; + this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; + + this.el = $(this.el); + _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); + this.needToRedraw = false; + this.model.bind('change', this.render); + this.model.fields.bind('reset', this.render); + this.model.fields.bind('add', this.render); + this.model.records.bind('add', this.redraw); + this.model.records.bind('reset', 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.editor.state.bind('change', function() { + self.state.set(self.editor.state.toJSON()); + self.redraw(); + }); + this.elSidebar = this.editor.el; + }, + + render: function() { + var self = this; + var tmplData = 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; + }, + + 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[0]); + 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')) { + // faff around with width because flot draws axes *outside* of the element + // width which means graph can get push down as it hits element next to it + this.$graph.width(this.el.width() - 240); + 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-graph-tooltip").remove(); + + var x = item.datapoint[0].toFixed(2), + y = item.datapoint[1].toFixed(2); + + var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { + 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; + } else { + xLocation = item.pageX + 10; + yLocation = item.pageY - 20; + } + + $('
' + content + '
').css({ + top: yLocation, + left: xLocation + }).appendTo("body").fadeIn(200); + } + } else { + $("#recline-graph-tooltip").remove(); + this.previousTooltipPoint.x = null; + this.previousTooltipPoint.y = null; + } + }, + + _xaxisLabel: function (x) { + var xfield = this.model.fields.get(this.state.attributes.group); + + // time series + var xtype = xfield.get('type'); + var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + + if (this.model.records.models[parseInt(x, 10)]) { + x = this.model.records.models[parseInt(x, 10)].get(this.state.attributes.group); + if (isDateTime) { + x = new Date(x).toLocaleDateString(); + } + } else if (isDateTime) { + x = new Date(parseInt(x, 10)).toLocaleDateString(); + } + + 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 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 (label.length > 8) { + label = label.slice(0, 5) + "..."; + } + + return label; + }; + + var xaxis = {}; + xaxis.tickFormatter = tickFormatter; + + // calculate the x-axis ticks + // + // the number of ticks should be a multiple of the number of points so that + // each tick lines up with a point + if (numPoints) { + var ticks = [], + maxTicks = 10, + x = 1, + i = 0; + + while (x <= maxTicks) { + if ((numPoints / x) <= maxTicks) { + break; + } + x = x + 1; + } + + for (i = 0; i < numPoints; i = i + x) { + ticks.push(i); + } + + xaxis.ticks = ticks; + } + + var yaxis = {}; + yaxis.autoscale = true; + yaxis.autoscaleMargin = 0.02; + + var legend = {}; + legend.position = 'ne'; + + var grid = {}; + grid.hoverable = true; + grid.clickable = true; + grid.borderColor = "#aaaaaa"; + grid.borderWidth = 1; + + var optionsPerGraphType = { + lines: { + legend: legend, + colors: this.graphColors, + lines: { show: true }, + xaxis: xaxis, + yaxis: yaxis, + grid: grid + }, + points: { + legend: legend, + colors: this.graphColors, + points: { show: true, hitRadius: 5 }, + xaxis: xaxis, + yaxis: yaxis, + grid: grid + }, + 'lines-and-points': { + legend: legend, + colors: this.graphColors, + points: { show: true, hitRadius: 5 }, + lines: { show: true }, + xaxis: xaxis, + yaxis: yaxis, + grid: grid + }, + bars: { + legend: legend, + colors: this.graphColors, + lines: { show: false }, + xaxis: yaxis, + yaxis: xaxis, + grid: grid, + bars: { + show: true, + horizontal: true, + shadowSize: 0, + align: 'center', + barWidth: 0.8 + } + }, + columns: { + legend: legend, + colors: this.graphColors, + lines: { show: false }, + xaxis: xaxis, + yaxis: yaxis, + grid: grid, + bars: { + show: true, + horizontal: false, + shadowSize: 0, + align: 'center', + barWidth: 0.8 + } + } + }; + + if (self.state.get('graphOptions')) { + return _.extend(optionsPerGraphType[typeId], + self.state.get('graphOptions')); + } else { + return optionsPerGraphType[typeId]; + } + }, + + createSeries: function() { + var self = this; + var series = []; + _.each(this.state.attributes.series, function(field) { + var points = []; + _.each(self.model.records.models, function(doc, index) { + var xfield = self.model.fields.get(self.state.attributes.group); + var x = doc.getFieldValue(xfield); + + // time series + var xtype = xfield.get('type'); + var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + + if (isDateTime) { + if (self.state.attributes.graphType != 'bars' && + self.state.attributes.graphType != 'columns') { + x = new Date(x).getTime(); + } else { + x = index; + } + } else if (typeof x === 'string') { + x = parseFloat(x); + if (isNaN(x)) { + x = index; + } + } + + var yfield = self.model.fields.get(field); + var y = doc.getFieldValue(yfield); + + if (self.state.attributes.graphType == 'bars') { + points.push([y, x]); + } else { + points.push([x, y]); + } + }); + series.push({ + data: points, + label: field, + hoverable: true + }); + }); + return series; + } +}); + +my.FlotControls = Backbone.View.extend({ + className: "editor", + template: ' \ +
\ +
\ +
\ + \ +
\ + \ +
\ + \ +
\ + \ +
\ +
\ +
\ +
\ +
\ + \ +
\ + \ +
\ +
\ +', + templateSeriesEditor: ' \ +
\ + \ +
\ + \ +
\ +
\ + ', + events: { + 'change form select': 'onEditorSubmit', + 'click .editor-add': '_onAddSeries', + 'click .action-remove-series': 'removeSeries' + }, + + initialize: function(options) { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.fields.bind('reset', this.render); + this.model.fields.bind('add', this.render); + this.state = new recline.Model.ObjectState(options.state); + this.render(); + }, + + render: function() { + var self = this; + var tmplData = this.model.toTemplateJSON(); + 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()); + + 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); +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + // ## Graph view for a Dataset using Flot graphing library. // // Initialization arguments (in a hash in first parameter): @@ -2295,7 +2800,7 @@ my.GraphControls = Backbone.View.extend({ \ \ \ - \ + \
\