diff --git a/dist/recline.css b/dist/recline.css index 13cd38589..b8bdd15ad 100644 --- a/dist/recline.css +++ b/dist/recline.css @@ -24,42 +24,6 @@ opacity: 0.8 !important; border: 1px solid #fdd !important; } -.recline-graph .graph { - height: 500px; -} - -.recline-graph .legend table { - width: auto; - margin-bottom: 0; -} - -.recline-graph .legend td { - padding: 5px; - line-height: 13px; -} - -.recline-graph .graph .alert { - width: 450px; -} - -.flotr-mouse-value { - background-color: #FEE !important; - color: #000000 !important; - opacity: 0.8 !important; - border: 1px solid #fdd !important; -} - -.flotr-legend { - border: none !important; -} - -.flotr-legend-bg { - display: none; -} - -.flotr-legend-color-box { - padding: 5px; -} /********************************************************** * (Data) Grid *********************************************************/ @@ -653,40 +617,3 @@ classes should alter those! .recline-timeline { position: relative; } -.recline-transform { - overflow: hidden; -} - -.recline-transform .script textarea { - width: 100%; - height: 100px; - font-family: monospace; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.recline-transform h2 { - margin-bottom: 10px; -} - -.recline-transform h2 .okButton { - margin-left: 10px; - margin-top: -2px; -} - -.expression-preview-parsing-status { - color: #999; -} - -.expression-preview-parsing-status.error { - color: red; -} - -.recline-transform .before-after .after { - font-style: italic; -} - -.recline-transform .before-after .after.different { - font-weight: bold; -} diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index cdd33a0eb..3ca6fd41b 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -159,20 +159,6 @@ my.Dataset = Backbone.Model.extend({ return this._store.save(this._changes, this.toJSON()); }, - transform: function(editFunc) { - var self = this; - if (!this._store.transform) { - alert('Transform is not supported with this backend: ' + this.get('backend')); - return; - } - this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true}); - this._store.transform(editFunc).done(function() { - // reload data as records have changed - self.query(); - self.trigger('recline:flash', {message: "Records updated successfully"}); - }); - }, - // ### query // // AJAX method with promise API to get records from the backend. @@ -829,18 +815,6 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; }); return facetResults; }; - - this.transform = function(editFunc) { - var dfd = new Deferred(); - // TODO: should we clone before mapping? Do not see the point atm. - self.records = _.map(self.records, editFunc); - // now deal with deletes (i.e. nulls) - self.records = _.filter(self.records, function(record) { - return record != null; - }); - dfd.resolve(); - return dfd.promise(); - }; }; }(this.recline.Backend.Memory)); diff --git a/dist/recline.js b/dist/recline.js index 70d1931ba..9df9cacc1 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -1,158 +1,5 @@ this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; - -(function(my) { - // ## CKAN Backend - // - // This provides connection to the CKAN DataStore (v2) - // - // General notes - // - // We need 2 things to make most requests: - // - // 1. CKAN API endpoint - // 2. ID of resource for which request is being made - // - // There are 2 ways to specify this information. - // - // EITHER (checked in order): - // - // * Every dataset must have an id equal to its resource id on the CKAN instance - // * The dataset has an endpoint attribute pointing to the CKAN API endpoint - // - // OR: - // - // Set the url attribute of the dataset to point to the Resource on the CKAN instance. The endpoint and id will then be automatically computed. - - my.__type__ = 'ckan'; - - // private - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; - - // Default CKAN API endpoint used for requests (you can change this but it will affect every request!) - // - // DEPRECATION: this will be removed in v0.7. Please set endpoint attribute on dataset instead - my.API_ENDPOINT = 'http://datahub.io/api'; - - // ### fetch - my.fetch = function(dataset) { - var wrapper; - if (dataset.endpoint) { - wrapper = my.DataStore(dataset.endpoint); - } else { - var out = my._parseCkanResourceUrl(dataset.url); - dataset.id = out.resource_id; - wrapper = my.DataStore(out.endpoint); - } - var dfd = new Deferred(); - var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0}); - jqxhr.done(function(results) { - // map ckan types to our usual types ... - var fields = _.map(results.result.fields, function(field) { - field.type = field.type in CKAN_TYPES_MAP ? CKAN_TYPES_MAP[field.type] : field.type; - return field; - }); - var out = { - fields: fields, - useMemoryStore: false - }; - dfd.resolve(out); - }); - return dfd.promise(); - }; - - // only put in the module namespace so we can access for tests! - my._normalizeQuery = function(queryObj, dataset) { - var actualQuery = { - resource_id: dataset.id, - q: queryObj.q, - filters: {}, - limit: queryObj.size || 10, - offset: queryObj.from || 0 - }; - - if (queryObj.sort && queryObj.sort.length > 0) { - var _tmp = _.map(queryObj.sort, function(sortObj) { - return sortObj.field + ' ' + (sortObj.order || ''); - }); - actualQuery.sort = _tmp.join(','); - } - - if (queryObj.filters && queryObj.filters.length > 0) { - _.each(queryObj.filters, function(filter) { - if (filter.type === "term") { - actualQuery.filters[filter.field] = filter.term; - } - }); - } - return actualQuery; - }; - - my.query = function(queryObj, dataset) { - var wrapper; - if (dataset.endpoint) { - wrapper = my.DataStore(dataset.endpoint); - } else { - var out = my._parseCkanResourceUrl(dataset.url); - dataset.id = out.resource_id; - wrapper = my.DataStore(out.endpoint); - } - var actualQuery = my._normalizeQuery(queryObj, dataset); - var dfd = new Deferred(); - var jqxhr = wrapper.search(actualQuery); - jqxhr.done(function(results) { - var out = { - total: results.result.total, - hits: results.result.records - }; - dfd.resolve(out); - }); - return dfd.promise(); - }; - - // ### DataStore - // - // Simple wrapper around the CKAN DataStore API - // - // @param endpoint: CKAN api endpoint (e.g. http://datahub.io/api) - my.DataStore = function(endpoint) { - var that = {endpoint: endpoint || my.API_ENDPOINT}; - - that.search = function(data) { - var searchUrl = that.endpoint + '/3/action/datastore_search'; - var jqxhr = jQuery.ajax({ - url: searchUrl, - type: 'POST', - data: JSON.stringify(data) - }); - return jqxhr; - }; - - return that; - }; - - // Parse a normal CKAN resource URL and return API endpoint etc - // - // Normal URL is something like http://demo.ckan.org/dataset/some-dataset/resource/eb23e809-ccbb-4ad1-820a-19586fc4bebd - my._parseCkanResourceUrl = function(url) { - parts = url.split('/'); - var len = parts.length; - return { - resource_id: parts[len-1], - endpoint: parts.slice(0,[len-4]).join('/') + '/api' - }; - }; - - var CKAN_TYPES_MAP = { - 'int4': 'integer', - 'int8': 'integer', - 'float8': 'float' - }; - -}(this.recline.Backend.Ckan)); -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; 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) @@ -450,7 +297,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; (function(my) { my.__type__ = 'dataproxy'; // URL for the dataproxy - my.dataproxy_url = 'http://jsonpdataproxy.appspot.com'; + my.dataproxy_url = '//jsonpdataproxy.appspot.com'; // Timeout for dataproxy (after this time if no response we error) // Needed because use JSONP so do not receive e.g. 500 errors my.timeout = 5000; @@ -1202,88 +1049,9 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; }); return facetResults; }; - - this.transform = function(editFunc) { - var dfd = new Deferred(); - // TODO: should we clone before mapping? Do not see the point atm. - self.records = _.map(self.records, editFunc); - // now deal with deletes (i.e. nulls) - self.records = _.filter(self.records, function(record) { - return record != null; - }); - dfd.resolve(); - return dfd.promise(); - }; }; }(this.recline.Backend.Memory)); -this.recline = this.recline || {}; -this.recline.Data = this.recline.Data || {}; - -(function(my) { -// adapted from https://github.com/harthur/costco. heather rules - -my.Transform = {}; - -my.Transform.evalFunction = function(funcString) { - try { - eval("var editFunc = " + funcString); - } catch(e) { - return {errorMessage: e+""}; - } - return editFunc; -}; - -my.Transform.previewTransform = function(docs, editFunc, currentColumn) { - var preview = []; - var updated = my.Transform.mapDocs($.extend(true, {}, docs), editFunc); - for (var i = 0; i < updated.docs.length; i++) { - var before = docs[i] - , after = updated.docs[i] - ; - if (!after) after = {}; - if (currentColumn) { - preview.push({before: before[currentColumn], after: after[currentColumn]}); - } else { - preview.push({before: before, after: after}); - } - } - return preview; -}; - -my.Transform.mapDocs = function(docs, editFunc) { - var edited = [] - , deleted = [] - , failed = [] - ; - - var updatedDocs = _.map(docs, function(doc) { - try { - var updated = editFunc(_.clone(doc)); - } catch(e) { - failed.push(doc); - return; - } - if(updated === null) { - updated = {_deleted: true}; - edited.push(updated); - deleted.push(doc); - } - else if(updated && !_.isEqual(updated, doc)) { - edited.push(updated); - } - return updated; - }); - - return { - updates: edited, - docs: updatedDocs, - deletes: deleted, - failed: failed - }; -}; - -}(this.recline.Data)) // This file adds in full array method support in browsers that don't support it // see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc @@ -1511,20 +1279,6 @@ my.Dataset = Backbone.Model.extend({ return this._store.save(this._changes, this.toJSON()); }, - transform: function(editFunc) { - var self = this; - if (!this._store.transform) { - alert('Transform is not supported with this backend: ' + this.get('backend')); - return; - } - this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true}); - this._store.transform(editFunc).done(function() { - // reload data as records have changed - self.query(); - self.trigger('recline:flash', {message: "Records updated successfully"}); - }); - }, - // ### query // // AJAX method with promise API to get records from the backend. @@ -1930,525 +1684,25 @@ my.FacetList = Backbone.Collection.extend({ constructor: function FacetList() { Backbone.Collection.prototype.constructor.apply(this, arguments); }, - model: my.Facet -}); - -// ## Object State -// -// Convenience Backbone model for storing (configuration) state of objects like Views. -my.ObjectState = Backbone.Model.extend({ -}); - - -// ## Backbone.sync -// -// Override Backbone.sync to hand off to sync function in relevant backend -Backbone.sync = function(method, model, options) { - return model.backend.sync(method, model, options); -}; - -}(this.recline.Model)); - -/*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): -// -// * 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')) { - 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 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 - 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) { - 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.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); - } - 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 (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - - var xaxis = {}; - xaxis.tickFormatter = tickFormatter; - - // 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 (i=0; i \ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ - \ -', - 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); - }, + model: my.Facet +}); - // 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(); - } +// ## Object State +// +// Convenience Backbone model for storing (configuration) state of objects like Views. +my.ObjectState = Backbone.Model.extend({ }); -})(jQuery, recline.View); + +// ## Backbone.sync +// +// Override Backbone.sync to hand off to sync function in relevant backend +Backbone.sync = function(method, model, options) { + return model.backend.sync(method, model, options); +}; + +}(this.recline.Model)); + /*jshint multistr:true */ this.recline = this.recline || {}; @@ -2456,25 +1710,25 @@ this.recline.View = this.recline.View || {}; (function($, my) { -// ## Graph view for a Dataset using Flotr2 graphing library. +// ## 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 [Flotr2 options](http://www.humblesoftware.com/flotr2/documentation#configuration)} +// 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.Flotr2 = Backbone.View.extend({ +my.Flot = Backbone.View.extend({ template: ' \ -
\ +
\
\
\

Hey there!

\ @@ -2490,7 +1744,7 @@ my.Flotr2 = Backbone.View.extend({ this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; this.el = $(this.el); - _.bindAll(this, 'render', 'redraw'); + _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); this.needToRedraw = false; this.model.bind('change', this.render); this.model.fields.bind('reset', this.render); @@ -2506,7 +1760,8 @@ my.Flotr2 = Backbone.View.extend({ options.state ); this.state = new recline.Model.ObjectState(stateData); - this.editor = new my.Flotr2Controls({ + this.previousTooltipPoint = {x: null, y: null}; + this.editor = new my.FlotControls({ model: this.model, state: this.state.toJSON() }); @@ -2523,16 +1778,15 @@ my.Flotr2 = Backbone.View.extend({ 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 appear to be issues generating a Flotr2 graph if either: - - // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flotr2 will complain with - // + // 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' + // * 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; @@ -2541,11 +1795,9 @@ my.Flotr2 = Backbone.View.extend({ // 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() - 20); var series = this.createSeries(); - var options = this.getGraphOptions(this.state.attributes.graphType); - this.plot = Flotr.draw(this.$graph.get(0), series, options); + var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length); + this.plot = $.plot(this.$graph, series, options); } }, @@ -2556,85 +1808,143 @@ my.Flotr2 = Backbone.View.extend({ } }, + // 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 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 - 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) { + 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.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); + } + if (isDateTime) { + x = new Date(x).toLocaleDateString(); + } + // } else if (isDateTime) { + // x = new Date(parseInt(x, 10)).toLocaleDateString(); + // } + + return x; + }, + // ### getGraphOptions // - // Get options for Flotr2 Graph + // Get options for Flot Graph // // needs to be function as can depend on state // // @param typeId graphType id (lines, lines-and-points etc) - getGraphOptions: function(typeId) { + // @param numPoints the number of points that will be plotted + getGraphOptions: function(typeId, numPoints) { var self = this; var tickFormatter = function (x) { - return getFormattedX(x); - }; - - // infoboxes on mouse hover on points/bars etc - var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y; - // it's horizontal so we have to flip - if (self.state.attributes.graphType === 'bars') { - var _tmp = x; - x = y; - y = _tmp; + // 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) + "..."; } - - x = getFormattedX(x); - var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { - group: self.state.attributes.group, - x: x, - series: obj.series.label, - y: y - }); - - return content; + return label; }; - - var getFormattedX = function (x) { - var xfield = self.model.fields.get(self.state.attributes.group); - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + var xaxis = {}; + xaxis.tickFormatter = tickFormatter; - if (self.model.records.models[parseInt(x)]) { - x = self.model.records.models[parseInt(x)].get(self.state.attributes.group); - if (isDateTime) { - x = new Date(x).toLocaleDateString(); - } - } else if (isDateTime) { - x = new Date(parseInt(x)).toLocaleDateString(); + // 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 (i=0; i \ @@ -2910,7 +2203,6 @@ my.Flotr2Controls = Backbone.View.extend({ }); })(jQuery, recline.View); - this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; this.recline.View.Graph = this.recline.View.Flot; @@ -4003,12 +3295,6 @@ my.MultiView = Backbone.View.extend({ model: this.model, state: this.state.get('view-timeline') }) - }, { - id: 'transform', - label: 'Transform', - view: new my.Transform({ - model: this.model - }) }]; } // Hashes of sidebar elements @@ -4947,138 +4233,6 @@ my.Timeline = Backbone.View.extend({ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; -// Views module following classic module pattern -(function($, my) { - -// ## ColumnTransform -// -// View (Dialog) for doing data transformations -my.Transform = Backbone.View.extend({ - template: ' \ -
\ -
\ -

\ - Transform Script \ - \ -

\ - \ -
\ -
\ - No syntax error. \ -
\ -
\ -

Preview

\ -
\ -
\ -
\ - ', - - events: { - 'click .okButton': 'onSubmit', - 'keydown .expression-preview-code': 'onEditorKeydown' - }, - - initialize: function(options) { - this.el = $(this.el); - }, - - render: function() { - var htmls = Mustache.render(this.template); - this.el.html(htmls); - // Put in the basic (identity) transform script - // TODO: put this into the template? - var editor = this.el.find('.expression-preview-code'); - if (this.model.fields.length > 0) { - var col = this.model.fields.models[0].id; - } else { - var col = 'unknown'; - } - editor.val("function(doc) {\n doc['"+ col +"'] = doc['"+ col +"'];\n return doc;\n}"); - editor.keydown(); - }, - - onSubmit: function(e) { - var self = this; - var funcText = this.el.find('.expression-preview-code').val(); - var editFunc = recline.Data.Transform.evalFunction(funcText); - if (editFunc.errorMessage) { - this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage}); - return; - } - this.model.transform(editFunc); - }, - - editPreviewTemplate: ' \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - {{#row}} \ - \ - \ - \ - \ - \ - {{/row}} \ - \ -
FieldBeforeAfter
\ - {{field}} \ - \ - {{before}} \ - \ - {{after}} \ -
\ - ', - - onEditorKeydown: function(e) { - var self = this; - // if you don't setTimeout it won't grab the latest character if you call e.target.value - window.setTimeout( function() { - var errors = self.el.find('.expression-preview-parsing-status'); - var editFunc = recline.Data.Transform.evalFunction(e.target.value); - if (!editFunc.errorMessage) { - errors.text('No syntax error.'); - var docs = self.model.records.map(function(doc) { - return doc.toJSON(); - }); - var previewData = recline.Data.Transform.previewTransform(docs, editFunc); - var $el = self.el.find('.expression-preview-container'); - var fields = self.model.fields.toJSON(); - var rows = _.map(previewData.slice(0,4), function(row) { - return _.map(fields, function(field) { - return { - field: field.id, - before: row.before[field.id], - after: row.after[field.id], - different: !_.isEqual(row.before[field.id], row.after[field.id]) - } - }); - }); - $el.html(''); - _.each(rows, function(row) { - var templated = Mustache.render(self.editPreviewTemplate, { - row: row - }); - $el.append(templated); - }); - } else { - errors.text(editFunc.errorMessage); - } - }, 1, true); - } -}); - -})(jQuery, recline.View); -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - (function($, my) { // ## FacetViewer