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: ' \
-