From f0005bed6cc2233c8a2dc1b8bf936b84296df913 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 6 Feb 2013 12:09:34 +0100 Subject: [PATCH 01/46] Hide the sidebar if it's empty. When updating the view check to see if the sidebar contains any visible children elements. If not, hide it. --- css/graph.css | 3 +-- css/grid.css | 1 - css/multiview.css | 1 - css/transform.css | 12 +++++------- src/view.graph.js | 2 +- src/view.multiview.js | 17 +++++++++++++++++ 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/css/graph.css b/css/graph.css index e961f8cb..7b1c085b 100644 --- a/css/graph.css +++ b/css/graph.css @@ -14,7 +14,6 @@ .recline-graph .graph .alert { width: 450px; - margin: auto; } .flotr-mouse-value { @@ -34,4 +33,4 @@ .flotr-legend-color-box { padding: 5px; -} \ No newline at end of file +} diff --git a/css/grid.css b/css/grid.css index 070e2903..caaef192 100644 --- a/css/grid.css +++ b/css/grid.css @@ -193,4 +193,3 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-read-only a.row-header-menu { display: none; } - diff --git a/css/multiview.css b/css/multiview.css index 4e7fbf20..06724df8 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -1,6 +1,5 @@ .recline-data-explorer .data-view-container { display: block; - margin-right: 225px; } .recline-data-explorer .data-view-sidebar { diff --git a/css/transform.css b/css/transform.css index 6c1e9b0e..9d404877 100644 --- a/css/transform.css +++ b/css/transform.css @@ -1,11 +1,14 @@ -.recline-transform .script { - margin-right: 10px; +.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 { @@ -17,10 +20,6 @@ margin-top: -2px; } -.recline-transform .preview { - margin-right: 10px; -} - .expression-preview-parsing-status { color: #999; } @@ -36,4 +35,3 @@ .recline-transform .before-after .after.different { font-weight: bold; } - diff --git a/src/view.graph.js b/src/view.graph.js index d0371d69..6dcef514 100644 --- a/src/view.graph.js +++ b/src/view.graph.js @@ -91,7 +91,7 @@ my.Graph = 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); + this.$graph.width(this.el.width() - 240); var series = this.createSeries(); var options = this.getGraphOptions(this.state.attributes.graphType); this.plot = Flotr.draw(this.$graph.get(0), series, options); diff --git a/src/view.multiview.js b/src/view.multiview.js index 28eb67e7..b8dead8c 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -203,6 +203,7 @@ my.MultiView = Backbone.View.extend({ } else { this.updateNav(this.pageViews[0].id); } + this._showHideSidebar(); this.model.bind('query:start', function() { self.notify({loader: true, persist: true}); @@ -280,6 +281,20 @@ my.MultiView = Backbone.View.extend({ }, + // hide the sidebar if empty + _showHideSidebar: function() { + var $dataSidebar = this.el.find('.data-view-sidebar'); + var visibleChildren = $dataSidebar.children().filter(function() { + return $(this).css("display") != "none"; + }).length; + + if (visibleChildren > 0) { + $dataSidebar.show(); + } else { + $dataSidebar.hide(); + } + }, + updateNav: function(pageName) { this.el.find('.navigation a').removeClass('active'); var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); @@ -310,6 +325,7 @@ my.MultiView = Backbone.View.extend({ e.preventDefault(); var action = $(e.target).attr('data-action'); this['$'+action].toggle(); + this._showHideSidebar(); }, _onSwitchView: function(e) { @@ -317,6 +333,7 @@ my.MultiView = Backbone.View.extend({ var viewName = $(e.target).attr('data-view'); this.updateNav(viewName); this.state.set({currentView: viewName}); + this._showHideSidebar(); }, // create a state object for this view and do the job of From 67b684d58f3192835c906f47f29159323dd3084a Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 6 Feb 2013 16:37:59 +0100 Subject: [PATCH 02/46] Hide filters and fields by default --- css/multiview.css | 5 +++++ src/view.multiview.js | 2 +- src/widget.fields.js | 23 ++--------------------- src/widget.filtereditor.js | 4 +--- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/css/multiview.css b/css/multiview.css index 4e7fbf20..bb18abec 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -98,6 +98,7 @@ .recline-filter-editor { padding: 8px; + display: none; } .recline-filter-editor .filter-term a { @@ -115,6 +116,10 @@ * Fields Widget *********************************************************/ +.recline-fields-view { + display: none; +} + .recline-fields-view .fields-list { padding: 0; } diff --git a/src/view.multiview.js b/src/view.multiview.js index 28eb67e7..83897c82 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -112,7 +112,7 @@ my.MultiView = Backbone.View.extend({ \ diff --git a/src/widget.fields.js b/src/widget.fields.js index 645ba055..30155dc6 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -38,7 +38,7 @@ my.Fields = Backbone.View.extend({ \ \ \ -
\ +
\
\ {{#facets}} \
\ @@ -57,9 +57,6 @@ my.Fields = Backbone.View.extend({
\ ', - events: { - 'click .js-show-hide': 'onShowHide' - }, initialize: function(model) { var self = this; this.el = $(this.el); @@ -77,6 +74,7 @@ my.Fields = Backbone.View.extend({ self.model.getFieldsSummary(); self.render(); }); + this.el.find('.collapse').collapse(); this.render(); }, render: function() { @@ -91,23 +89,6 @@ my.Fields = Backbone.View.extend({ }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - this.el.find('.collapse').collapse('hide'); - }, - onShowHide: function(e) { - e.preventDefault(); - var $target = $(e.target); - // weird collapse class seems to have been removed (can watch this happen - // if you watch dom) but could not work why. Absence of collapse then meant - // we could not toggle. - // This seems to fix the problem. - this.el.find('.accordion-body').addClass('collapse');; - if ($target.text() === '+') { - this.el.find('.collapse').collapse('show'); - $target.text('-'); - } else { - this.el.find('.collapse').collapse('hide'); - $target.text('+'); - } } }); diff --git a/src/widget.filtereditor.js b/src/widget.filtereditor.js index 28c80bb3..09864963 100644 --- a/src/widget.filtereditor.js +++ b/src/widget.filtereditor.js @@ -123,8 +123,6 @@ my.FilterEditor = Backbone.View.extend({ var filterType = $target.find('select.filterType').val(); var field = $target.find('select.fields').val(); this.model.queryState.addFilter({type: filterType, field: field}); - // trigger render explicitly as queryState change will not be triggered (as blank value for filter) - this.render(); }, onRemoveFilter: function(e) { e.preventDefault(); @@ -141,7 +139,7 @@ my.FilterEditor = Backbone.View.extend({ var $input = $(input); var filterType = $input.attr('data-filter-type'); var fieldId = $input.attr('data-filter-field'); - var filterIndex = parseInt($input.attr('data-filter-id')); + var filterIndex = parseInt($input.attr('data-filter-id'), 10); var name = $input.attr('name'); var value = $input.val(); From bc3ade4c237dc2c7d2b791ceca48692f59798d7a Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 11 Feb 2013 12:10:57 +0100 Subject: [PATCH 03/46] [#309] Fix rendering of fields widget in IE7. --- src/widget.fields.js | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/widget.fields.js b/src/widget.fields.js index 645ba055..13078d2f 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -38,7 +38,7 @@ my.Fields = Backbone.View.extend({ \ \
\ -
\ +
\
\ {{#facets}} \
\ @@ -57,9 +57,6 @@ my.Fields = Backbone.View.extend({
\ ', - events: { - 'click .js-show-hide': 'onShowHide' - }, initialize: function(model) { var self = this; this.el = $(this.el); @@ -77,6 +74,7 @@ my.Fields = Backbone.View.extend({ self.model.getFieldsSummary(); self.render(); }); + this.el.find('.collapse').collapse(); this.render(); }, render: function() { @@ -91,25 +89,7 @@ my.Fields = Backbone.View.extend({ }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - this.el.find('.collapse').collapse('hide'); - }, - onShowHide: function(e) { - e.preventDefault(); - var $target = $(e.target); - // weird collapse class seems to have been removed (can watch this happen - // if you watch dom) but could not work why. Absence of collapse then meant - // we could not toggle. - // This seems to fix the problem. - this.el.find('.accordion-body').addClass('collapse');; - if ($target.text() === '+') { - this.el.find('.collapse').collapse('show'); - $target.text('-'); - } else { - this.el.find('.collapse').collapse('hide'); - $target.text('+'); - } } }); })(jQuery, recline.View); - From bb28ef2347c65c458f7b015c760cb8fdbdd39c49 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 11 Feb 2013 14:27:22 +0100 Subject: [PATCH 04/46] [#309] Small JS fix for IE7. Extra comma in object literal removed. --- src/backend.ckan.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend.ckan.js b/src/backend.ckan.js index 62fc91e7..1b59bf8c 100644 --- a/src/backend.ckan.js +++ b/src/backend.ckan.js @@ -76,7 +76,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; actualQuery.sort = _tmp.join(','); } return actualQuery; - } + }; my.query = function(queryObj, dataset) { if (dataset.endpoint) { @@ -92,7 +92,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; jqxhr.done(function(results) { var out = { total: results.result.total, - hits: results.result.records, + hits: results.result.records }; dfd.resolve(out); }); @@ -116,7 +116,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; dataType: 'json' }); return jqxhr; - } + }; return that; }; @@ -130,7 +130,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; return { resource_id: parts[len-1], endpoint: parts.slice(0,[len-4]).join('/') + '/api' - } + }; }; var CKAN_TYPES_MAP = { From 64c9e488ddf9046803e3ac5c04d844e34335c88c Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 11 Feb 2013 14:53:46 +0000 Subject: [PATCH 05/46] [#314,be/solr][s]: minor extra solr reference to remove missed in /6f82c4c5ccd2367384ef8f1cb4fc296ed4a34e97. --- _includes/backend-list.html | 2 +- _includes/recline-deps.html | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/_includes/backend-list.html b/_includes/backend-list.html index 60848916..54fe15c2 100644 --- a/_includes/backend-list.html +++ b/_includes/backend-list.html @@ -1,7 +1,7 @@ diff --git a/demos/couchdb/app.js b/demos/couchdb/app.js deleted file mode 100755 index c02fe9e7..00000000 --- a/demos/couchdb/app.js +++ /dev/null @@ -1,73 +0,0 @@ -jQuery(function($) { - window.dataExplorer = null; - window.explorerDiv = $('.data-explorer-here'); - - var queryParameters = recline.View.parseQueryString(decodeURIComponent(window.location.search)); - - var dataset = new recline.Model.Dataset({ - db_url: queryParameters['url'] || '/couchdb/yourcouchdb', - view_url: queryParameters['view_url'] || '/couchdb/yourcouchdb/_design/yourdesigndoc/_view/yourview', - backend: 'couchdb', - query_options: { - 'key': '_id' - } - }); - - dataset.fetch().done(function(dataset) { - console.log('records: ' + dataset.records); - }); - - createExplorer(dataset); -}); - -// make Explorer creation / initialization in a function so we can call it -// again and again -var createExplorer = function(dataset, state) { - // remove existing data explorer view - var reload = false; - if (window.dataExplorer) { - window.dataExplorer.remove(); - reload = true; - } - window.dataExplorer = null; - var $el = $('
'); - $el.appendTo(window.explorerDiv); - - var views = [ - { - id: 'grid', - label: 'Grid', - view: new recline.View.SlickGrid({ - model: dataset - }), - }, - { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset - }), - }, - { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset - }), - }, - { - id: 'transform', - label: 'Transform', - view: new recline.View.Transform({ - model: dataset - }) - } - ]; - - window.dataExplorer = new recline.View.MultiView({ - model: dataset, - el: $el, - state: state, - views: views - }); -} diff --git a/demos/couchdb/index.html b/demos/couchdb/index.html deleted file mode 100755 index 953ef283..00000000 --- a/demos/couchdb/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: container -title: CouchDB Multiview - Demos -recline-deps: true -root: ../../ ---- - - - -
-

Instructions

-

To use this demo you will need a CouchDB instance running and accessible over HTTP. You should then pass the following 2 query parameters to this page:

- -
url: url-to-your-couchdb-instance
-view_url: url-to-your-couchdb-view
-
-Example:
-http://path-to-this-page/?url=/mycouchdb/&view_url=/mycouchdb/_design/yourdesigndoc/_view/yourview
-
- -

Note that if the CouchDB database is not running on the same domain as this page then the host it is on must support CORS – the simplest approach here is probably to set up a reverse proxy or proxy so your CouchDB database appears on the local domain at e.g. /mycouchdb/.

-
- -

Demo

- -
- -
- - - - diff --git a/demos/index.html b/demos/index.html index 4e2f9160..740bde58 100644 --- a/demos/index.html +++ b/demos/index.html @@ -95,13 +95,13 @@ root: ../
-

CouchDB Demo

+

CouchDB Demo

Using CouchDB with Recline Multiview to provide an elegant powerful browser for a CouchDB database and view.

- +
diff --git a/docs/src/backend.couchdb.html b/docs/src/backend.couchdb.html deleted file mode 100644 index 17de2e69..00000000 --- a/docs/src/backend.couchdb.html +++ /dev/null @@ -1,432 +0,0 @@ - backend.couchdb.js

backend.couchdb.js

this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
-
-(function($, my) {  
-my.__type__ = 'couchdb';

CouchDB Wrapper

- -

Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints. -@param {String} endpoint: url for CouchDB database, e.g. for Couchdb running -on localhost:5984 with database // ckan-std it would be:

- -

TODO Add user/password arguments for couchdb authentication support.

- -

See the example how to use this in: "demos/couchdb/"

  my.CouchDBWrapper = function(db_url, view_url, options) { 
-    var self = this;
-    self.endpoint = db_url;
-    self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs';
-    self.options = _.extend({
-        dataType: 'json'
-      },
-      options);
-
-    this._makeRequest = function(data, headers) {
-      var extras = {};
-      if (headers) {
-        extras = {
-          beforeSend: function(req) {
-            _.each(headers, function(value, key) {
-              req.setRequestHeader(key, value);
-            });
-          }
-        };
-      }
-      data = _.extend(extras, data);
-      return $.ajax(data);
-    };

mapping

- -

Get mapping for this database. -Assume all docs in the view have the same schema so -limit query to single result.

- -

@return promise compatible deferred object.

    this.mapping = function() {
-      var schemaUrl = self.view_url + '?limit=1&include_docs=true';
-      var jqxhr = self._makeRequest({
-        url: schemaUrl,
-        dataType: self.options.dataType
-      });
-      return jqxhr;
-    };

get

- -

Get record corresponding to specified id

- -

@return promise compatible deferred object.

    this.get = function(_id) {
-      var base = self.endpoint + '/' + _id;
-      return self._makeRequest({
-        url: base,
-        dataType: 'json'
-      });
-    };

upsert

- -

create / update a record to CouchDB backend

- -

@param {Object} doc an object to insert to the index. -@return deferred supporting promise API

    this.upsert = function(doc) {
-      var data = JSON.stringify(doc);
-      url = self.endpoint;
-      if (doc._id) {
-        url += '/' + doc._id;
-      }

use a PUT, not a POST to update the document: -http://wiki.apache.org/couchdb/HTTPDocumentAPI#POST

      return self._makeRequest({
-        url: url,
-        type: 'PUT',
-        data: data,
-        dataType: 'json',
-        contentType: 'application/json'
-      });
-    };

delete

- -

Delete a record from the CouchDB backend.

- -

@param {Object} id id of object to delete -@return deferred supporting promise API

    this.remove = function(_id) {
-      url = self.endpoint;
-      url += '/' + _id;
-      return self._makeRequest({
-        url: url,
-        type: 'DELETE',
-        dataType: 'json'
-      });
-    };

_normalizeQuery

- -

Convert the query object from Elastic Search format to a -Couchdb View API compatible format. -See: http://wiki.apache.org/couchdb/HTTPviewAPI

    this._normalizeQuery = function(queryObj) {
-      var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
-      delete out.sort;
-      delete out.query;
-      delete out.filters;
-      delete out.fields;
-      delete out.facets;
-      out['skip'] = out.from || 0;
-      out['limit'] = out.size || 100;
-      delete out.from;
-      delete out.size;
-      out['include_docs'] = true;      
-      return out;
-    };

query

- -

@param {Object} recline.Query instance. -@param {Object} additional couchdb view query options. -@return deferred supporting promise API

    this.query = function(query_object, query_options) {
-      var norm_q = self._normalizeQuery(query_object);
-      var url = self.view_url;
-      var q = _.extend(query_options, norm_q);
-
-      var jqxhr = self._makeRequest({
-        url: url,
-        data: JSON.stringify(q),
-        dataType: self.options.dataType,
-      });
-      return jqxhr;
-    }
-  };

CouchDB Backend

- -

Backbone connector for a CouchDB backend.

- -
var dataset = new recline.Model.Dataset({
-  db_url: path-to-couchdb-database e.g. '/couchdb/mydb',        
-  view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1',
-  backend: 'couchdb',
-  query_options: {
-    'key': '_id'
-  }
-});
-
-backend.query(query, dataset.toJSON()).done(function () { ... });
-
- -

Alternatively:

- -
var dataset = new recline.Model.Dataset({ ... }, 'couchdb');
-dataset.fetch();
-var results = dataset.query(query_obj);
-
- -

Additionally, the Dataset instance may define three methods:

- -

function recordupdate (record, document) { ... } - function recorddelete (record, document) { ... } - function record_create (record, document) { ... }

- -

Where record is the JSON representation of the Record/Document instance -and document is the JSON document stored in couchdb. -When alldocs view is used (default), a record is the same as a document -so these methods need not be defined. -They are most useful when using a custom view that performs a map-reduce -operation on each document to yield a record. Hence, when the record is -created, updated or deleted, an inverse operation must be performed on the original -document.

- -

@param {string} url of couchdb database. -@param {string} (optional) url of couchdb view. default:db_url/alldocs -@param {Object} (optional) query options accepted by couchdb views.

  my.couchOptions = {};

fetch

- -

@param {object} dataset json object with the dburl, viewurl, and query_options args. -@return promise object that resolves to the document mapping.

  my.fetch = function (dataset) {
-    var db_url    = dataset.db_url;
-    var view_url  = dataset.view_url;
-    var cdb       = new my.CouchDBWrapper(db_url, view_url);
-    var dfd       = $.Deferred();

if 'doc' attribute is present, return schema of that -else return schema of 'value' attribute which contains -the map-reduced document.

    cdb.mapping().done(function(result) {
-      var row = result.rows[0];
-      var keys = [];
-      if (view_url.search("_all_docs") !== -1) {
-        keys = _.keys(row['doc']);
-        keys = _.filter(keys, function (k) { return k.charAt(0) !== '_' });
-      }
-      else {
-        keys = _.keys(row['value']);
-      }
-
-      var fieldData = _.map(keys, function(k) {
-        return { 'id' : k };
-      });     
-      dfd.resolve({
-        fields: fieldData
-      });
-    })
-    .fail(function(arguments) {
-      dfd.reject(arguments);
-    });
-    return dfd.promise();
-  };

save

- -

Iterate through all the changes and save them to the server. -N.B. This method is asynchronous and attempts to do multiple -operation concurrently. This can be problematic when more than -one operation is requested on the same document (as in the case -of bulk column transforms).

- -

@param {object} lists of create, update, delete. -@param {object} dataset json object.

my.save = function (changes, dataset) {
-  var dfd       = $.Deferred();
-  var total     = changes.creates.length + changes.updates.length + changes.deletes.length;  
-  var results   = {'done': [], 'fail': [] };
-
-  var decr_cb = function () { total -= 1; }
-  var resolve_cb = function () { if (total == 0) dfd.resolve(results); }
-
-  for (var i in changes.creates) {
-    var new_doc = changes.creates[i];
-    var succ_cb = function (msg) {results.done.push({'op': 'create', 'record': new_doc, 'reason': ''}); }
-    var fail_cb = function (msg) {results.fail.push({'op': 'create', 'record': new_doc, 'reason': msg}); }
-
-    _createDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
-  }
-
-  for (var i in changes.updates) {
-    var new_doc = changes.updates[i];
-    var succ_cb = function (msg) {results.done.push({'op': 'update', 'record': new_doc, 'reason': ''}); }
-    var fail_cb = function (msg) {results.fail.push({'op': 'update', 'record': new_doc, 'reason': msg}); }
-
-    _updateDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
-  }
-
-  for (var i in changes.deletes) {
-    var old_doc = changes.deletes[i];
-    var succ_cb = function (msg) {results.done.push({'op': 'delete', 'record': old_doc, 'reason': ''}); }
-    var fail_cb = function (msg) {results.fail.push({'op': 'delete', 'record': old_doc, 'reason': msg}); }
-
-    _deleteDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
-  }
-
-  return dfd.promise();
-};

query

- -

fetch the data from the couchdb view and filter it. -@param {Object} recline.Dataset instance -@param {Object} recline.Query instance.

my.query = function(queryObj, dataset) {
-  var dfd           = $.Deferred();
-  var db_url        = dataset.db_url;
-  var view_url      = dataset.view_url;
-  var query_options = dataset.query_options;
-
-  var cdb = new my.CouchDBWrapper(db_url, view_url); 
-  var cdb_q = cdb._normalizeQuery(queryObj, query_options);
-
-  cdb.query(queryObj, query_options).done(function(records){
-
-    var query_result = { hits: [], total: 0 };
-    _.each(records.rows, function(record) {
-      var doc = {};
-      if (record.hasOwnProperty('doc')) {
-        doc = record['doc'];

couchdb uses _id to identify documents, Backbone models use id. -we add this fix so backbone.Model works correctly.

        doc['id'] = doc['_id'];
-      }
-      else {
-        doc = record['value'];

using dunder to create compound id. need something more robust. -couchdb uses _id to identify documents, Backbone models use id. -we add this fix so backbone.Model works correctly.

        doc['_id'] = doc['id'] = record['id'] + '__' + record['key']; 
-      }
-      query_result.total += 1;
-      query_result.hits.push(doc);
-    });

the following block is borrowed verbatim from recline.backend.Memory -search (with filtering, faceting, and sorting) should be factored -out into a separate library.

    query_result.hits = _applyFilters(query_result.hits, queryObj);
-    query_result.hits = _applyFreeTextQuery(query_result.hits, queryObj);

not complete sorting!

    _.each(queryObj.sort, function(sortObj) {
-      var fieldName = _.keys(sortObj)[0];
-      query_result.hits = _.sortBy(query_result.hits, function(doc) {
-        var _out = doc[fieldName];
-        return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
-      });
-    });
-    query_result.total  = query_result.hits.length;
-    query_result.facets = _computeFacets(query_result.hits, queryObj);
-    query_result.hits = query_result.hits.slice(cdb_q.skip, cdb_q.skip + cdb_q.limit+1);
-    dfd.resolve(query_result);
-
-  });
-  
-  return dfd.promise();
-};

in place filtering

_applyFilters = function(results, queryObj) {
-  _.each(queryObj.filters, function(filter) {
-    results = _.filter(results, function(doc) {
-      var fieldId = _.keys(filter.term)[0];
-      return (doc[fieldId] == filter.term[fieldId]);
-    });
-  });
-  return results;
-};

we OR across fields but AND across terms in query string

_applyFreeTextQuery = function(results, queryObj) {
-  if (queryObj.q) {
-    var terms = queryObj.q.split(' ');
-    results = _.filter(results, function(rawdoc) {
-      var matches = true;
-      _.each(terms, function(term) {
-        var foundmatch = false;      
-        _.each(_.keys(rawdoc), function(field) {
-          var value = rawdoc[field];
-          if (value !== null) { value = value.toString(); }

TODO regexes?

          foundmatch = foundmatch || (value === term);

TODO: early out (once we are true should break to spare unnecessary testing) -if (foundmatch) return true;

        });
-        matches = matches && foundmatch;

TODO: early out (once false should break to spare unnecessary testing) -if (!matches) return false;

      });
-      return matches;
-    });
-  }
-  return results;
-};
-
-_computeFacets = function(records, queryObj) {
-  var facetResults = {};
-  if (!queryObj.facets) {
-    return facetResults;
-  }
-  _.each(queryObj.facets, function(query, facetId) {

TODO: remove dependency on recline.Model

    facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
-    facetResults[facetId].termsall = {};
-  });

faceting

  _.each(records, function(doc) {
-    _.each(queryObj.facets, function(query, facetId) {
-      var fieldId = query.terms.field;
-      var val = doc[fieldId];
-      var tmp = facetResults[facetId];
-      if (val) {
-        tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
-      } else {
-        tmp.missing = tmp.missing + 1;
-      }
-    });
-  });
-  _.each(queryObj.facets, function(query, facetId) {
-    var tmp = facetResults[facetId];
-    var terms = _.map(tmp.termsall, function(count, term) {
-      return { term: term, count: count };
-    });
-    tmp.terms = _.sortBy(terms, function(item) {

want descending order

      return -item.count;
-    });
-    tmp.terms = tmp.terms.slice(0, 10);
-  });
-  return facetResults;
-};
-     
-_createDocument  = function (new_doc, dataset) {
-  var dfd      = $.Deferred();
-  var db_url   = dataset.db_url;
-  var view_url = dataset.view_url;
-  var _id      = new_doc['id'];
-  var cdb      = new my.CouchDBWrapper(db_url, view_url);
-
-  delete new_doc['id']; 
-
-  if (view_url.search('_all_docs') !== -1) {
-    jqxhr = cdb.get(_id);
-  }
-  else {
-    _id = new_doc['_id'].split('__')[0];
-    jqxhr = cdb.get(_id);
-  }
-
-  jqxhr.done(function(old_doc){
-    if (dataset.record_create)
-      new_doc = dataset.record_create(new_doc, old_doc);
-    new_doc = _.extend(old_doc, new_doc);
-    new_doc['_id'] = _id;
-    dfd.resolve(cdb.upsert(new_doc));            
-  }).fail(function(args){
-    dfd.reject(args);
-  });
-
-  return dfd.promise();
-};
-
-_updateDocument = function (new_doc, dataset) {
-  var dfd      = $.Deferred();
-  var db_url   = dataset.db_url;
-  var view_url = dataset.view_url;
-  var _id      = new_doc['id'];
-  var cdb      = new my.CouchDBWrapper(db_url, view_url);
-
-  delete new_doc['id']; 
-
-  if (view_url.search('_all_docs') !== -1) {
-    jqxhr = cdb.get(_id);
-  }
-  else {
-    _id = new_doc['_id'].split('__')[0];
-    jqxhr = cdb.get(_id);
-  }
-
-  jqxhr.done(function(old_doc){
-    if (dataset.record_update)
-      new_doc = dataset.record_update(new_doc, old_doc);
-    new_doc = _.extend(old_doc, new_doc);
-    new_doc['_id'] = _id;
-    dfd.resolve(cdb.upsert(new_doc));            
-  }).fail(function(args){
-    dfd.reject(args);
-  });
-
-  return dfd.promise();
-};
-
-_deleteDocument = function (del_doc, dataset) {
-  var dfd      = $.Deferred();
-  var db_url   = dataset.db_url;
-  var view_url = dataset.view_url;
-  var _id      = del_doc['id'];
-  var cdb      = new my.CouchDBWrapper(db_url, view_url);
-
-  if (view_url.search('_all_docs') !== -1) 
-    return cdb.remove(_id);
-  else {
-    _id = model.get('_id').split('__')[0];
-    var jqxhr = cdb.get(_id);
-
-    jqxhr.done(function(old_doc){
-      if (dataset.record_delete)
-        old_doc = dataset.record_delete(del_doc, old_doc);
-      if (_.isNull(del_doc))
-        dfd.resolve(cdb.remove(_id)); // XXX is this the right thing to do?
-      else {

couchdb uses _id to identify documents, Backbone models use id. -we should remove it before sending it to the server.

        old_doc['_id'] = _id;
-        delete old_doc['id'];
-        dfd.resolve(cdb.upsert(old_doc)); 
-      }
-    }).fail(function(args){
-      dfd.reject(args);
-    });
-    return dfd.promise();
-    }
-};
-}(jQuery, this.recline.Backend.CouchDB));
-
-
\ No newline at end of file diff --git a/src/backend.couchdb.js b/src/backend.couchdb.js deleted file mode 100755 index 994cfc9b..00000000 --- a/src/backend.couchdb.js +++ /dev/null @@ -1,563 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {}; - -(function($, my) { -my.__type__ = 'couchdb'; - -// use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; - - // ## CouchDB Wrapper - // - // Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints. - // @param {String} endpoint: url for CouchDB database, e.g. for Couchdb running - // on localhost:5984 with database // ckan-std it would be: - // - // - // TODO Add user/password arguments for couchdb authentication support. - // - // See the example how to use this in: "demos/couchdb/" - my.CouchDBWrapper = function(db_url, view_url, options) { - var self = this; - self.endpoint = db_url; - self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs'; - self.options = _.extend({ - dataType: 'json' - }, - options); - - this._makeRequest = function(data, headers) { - var extras = {}; - if (headers) { - extras = { - beforeSend: function(req) { - _.each(headers, function(value, key) { - req.setRequestHeader(key, value); - }); - } - }; - } - data = _.extend(extras, data); - return $.ajax(data); - }; - - // ### mapping - // - // Get mapping for this database. - // Assume all docs in the view have the same schema so - // limit query to single result. - // - // @return promise compatible deferred object. - this.mapping = function() { - var schemaUrl = self.view_url + '?limit=1&include_docs=true'; - var jqxhr = self._makeRequest({ - url: schemaUrl, - dataType: self.options.dataType - }); - return jqxhr; - }; - - // ### get - // - // Get record corresponding to specified id - // - // @return promise compatible deferred object. - this.get = function(_id) { - var base = self.endpoint + '/' + _id; - return self._makeRequest({ - url: base, - dataType: 'json' - }); - }; - - // ### upsert - // - // create / update a record to CouchDB backend - // - // @param {Object} doc an object to insert to the index. - // @return deferred supporting promise API - this.upsert = function(doc) { - var data = JSON.stringify(doc); - url = self.endpoint; - if (doc._id) { - url += '/' + doc._id; - } - // use a PUT, not a POST to update the document: - // http://wiki.apache.org/couchdb/HTTP_Document_API#POST - return self._makeRequest({ - url: url, - type: 'PUT', - data: data, - dataType: 'json', - contentType: 'application/json' - }); - }; - - // ### delete - // - // Delete a record from the CouchDB backend. - // - // @param {Object} id id of object to delete - // @return deferred supporting promise API - this.remove = function(_id) { - url = self.endpoint; - url += '/' + _id; - return self._makeRequest({ - url: url, - type: 'DELETE', - dataType: 'json' - }); - }; - - // ### _normalizeQuery - // - // Convert the query object from Elastic Search format to a - // Couchdb View API compatible format. - // See: http://wiki.apache.org/couchdb/HTTP_view_API - // - this._normalizeQuery = function(queryObj) { - var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); - delete out.sort; - delete out.query; - delete out.filters; - delete out.fields; - delete out.facets; - out['skip'] = out.from || 0; - out['limit'] = out.size || 100; - delete out.from; - delete out.size; - out['include_docs'] = true; - return out; - }; - - // ### query - // - // @param {Object} recline.Query instance. - // @param {Object} additional couchdb view query options. - // @return deferred supporting promise API - this.query = function(query_object, query_options) { - var norm_q = self._normalizeQuery(query_object); - var url = self.view_url; - var q = _.extend(query_options, norm_q); - - var jqxhr = self._makeRequest({ - url: url, - data: JSON.stringify(q), - dataType: self.options.dataType, - }); - return jqxhr; - } - }; - - // ## CouchDB Backend - // - // Backbone connector for a CouchDB backend. - // - // var dataset = new recline.Model.Dataset({ - // db_url: path-to-couchdb-database e.g. '/couchdb/mydb', - // view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1', - // backend: 'couchdb', - // query_options: { - // 'key': '_id' - // } - // }); - // - // backend.query(query, dataset.toJSON()).done(function () { ... }); - // - // Alternatively: - // - // var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); - // dataset.fetch(); - // var results = dataset.query(query_obj); - // - // Additionally, the Dataset instance may define three methods: - // - // function record_update (record, document) { ... } - // function record_delete (record, document) { ... } - // function record_create (record, document) { ... } - // - // Where `record` is the JSON representation of the Record/Document instance - // and `document` is the JSON document stored in couchdb. - // When _all_docs view is used (default), a record is the same as a document - // so these methods need not be defined. - // They are most useful when using a custom view that performs a map-reduce - // operation on each document to yield a record. Hence, when the record is - // created, updated or deleted, an inverse operation must be performed on the original - // document. - // - // @param {string} url of couchdb database. - // @param {string} (optional) url of couchdb view. default:`db_url`/_all_docs - // @param {Object} (optional) query options accepted by couchdb views. - // - - my.couchOptions = {}; - - // ### fetch - // @param {object} dataset json object with the db_url, view_url, and query_options args. - // @return promise object that resolves to the document mapping. - my.fetch = function (dataset) { - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var cdb = new my.CouchDBWrapper(db_url, view_url); - var dfd = new Deferred(); - - // if 'doc' attribute is present, return schema of that - // else return schema of 'value' attribute which contains - // the map-reduced document. - cdb.mapping().done(function(result) { - var row = result.rows[0]; - var keys = []; - if (view_url.search("_all_docs") !== -1) { - keys = _.keys(row['doc']); - keys = _.filter(keys, function (k) { return k.charAt(0) !== '_' }); - } - else { - keys = _.keys(row['value']); - } - - var fieldData = _.map(keys, function(k) { - return { 'id' : k }; - }); - dfd.resolve({ - fields: fieldData - }); - }) - .fail(function(arguments) { - dfd.reject(arguments); - }); - return dfd.promise(); - }; - -// ### save -// -// Iterate through all the changes and save them to the server. -// N.B. This method is asynchronous and attempts to do multiple -// operation concurrently. This can be problematic when more than -// one operation is requested on the same document (as in the case -// of bulk column transforms). -// -// @param {object} lists of create, update, delete. -// @param {object} dataset json object. -// -// -my.save = function (changes, dataset) { - var dfd = new Deferred(); - var total = changes.creates.length + changes.updates.length + changes.deletes.length; - var results = {'done': [], 'fail': [] }; - - var decr_cb = function () { total -= 1; } - var resolve_cb = function () { if (total == 0) dfd.resolve(results); } - - for (var i in changes.creates) { - var new_doc = changes.creates[i]; - var succ_cb = function (msg) {results.done.push({'op': 'create', 'record': new_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'create', 'record': new_doc, 'reason': msg}); } - - _createDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - for (var i in changes.updates) { - var new_doc = changes.updates[i]; - var succ_cb = function (msg) {results.done.push({'op': 'update', 'record': new_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'update', 'record': new_doc, 'reason': msg}); } - - _updateDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - for (var i in changes.deletes) { - var old_doc = changes.deletes[i]; - var succ_cb = function (msg) {results.done.push({'op': 'delete', 'record': old_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'delete', 'record': old_doc, 'reason': msg}); } - - _deleteDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - return dfd.promise(); -}; - - -// ### query -// -// fetch the data from the couchdb view and filter it. -// @param {Object} recline.Dataset instance -// @param {Object} recline.Query instance. -my.query = function(queryObj, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var query_options = dataset.query_options; - - var cdb = new my.CouchDBWrapper(db_url, view_url); - var cdb_q = cdb._normalizeQuery(queryObj, query_options); - - cdb.query(queryObj, query_options).done(function(records){ - - var query_result = { hits: [], total: 0 }; - _.each(records.rows, function(record) { - var doc = {}; - if (record.hasOwnProperty('doc')) { - doc = record['doc']; - // couchdb uses _id to identify documents, Backbone models use id. - // we add this fix so backbone.Model works correctly. - doc['id'] = doc['_id']; - } - else { - doc = record['value']; - // using dunder to create compound id. need something more robust. - // couchdb uses _id to identify documents, Backbone models use id. - // we add this fix so backbone.Model works correctly. - doc['_id'] = doc['id'] = record['id'] + '__' + record['key']; - } - query_result.total += 1; - query_result.hits.push(doc); - }); - - // the following block is borrowed verbatim from recline.backend.Memory - // search (with filtering, faceting, and sorting) should be factored - // out into a separate library. - query_result.hits = _applyFilters(query_result.hits, queryObj); - query_result.hits = _applyFreeTextQuery(query_result.hits, queryObj); - // not complete sorting! - _.each(queryObj.sort, function(sortObj) { - var fieldName = sortObj.field; - query_result.hits = _.sortBy(query_result.hits, function(doc) { - var _out = doc[fieldName]; - return _out; - }); - if (sortObj.order == 'desc') { - query_result.hits.reverse(); - } - }); - query_result.total = query_result.hits.length; - query_result.facets = _computeFacets(query_result.hits, queryObj); - query_result.hits = query_result.hits.slice(cdb_q.skip, cdb_q.skip + cdb_q.limit+1); - dfd.resolve(query_result); - - }); - - return dfd.promise(); -}; - -// in place filtering -_applyFilters = function(results, queryObj) { - var filters = queryObj.filters; - // register filters - var filterFunctions = { - term : term, - range : range, - geo_distance : geo_distance - }; - var dataParsers = { - integer: function (e) { return parseFloat(e, 10); }, - 'float': function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString() }, - date : function (e) { return new Date(e).valueOf() }, - datetime : function (e) { return new Date(e).valueOf() } - }; - - function getDataParser(filter) { - //sample = results[0][filter.field]); - var fieldType = 'string'; - return dataParsers[fieldType]; - } - - // filter records - return _.filter(results, function (record) { - var passes = _.map(filters, function (filter) { - return filterFunctions[filter.type](record, filter); - }); - - // return only these records that pass all filters - return _.all(passes, _.identity); - }); - - // filters definitions - function term(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var term = parse(filter.term); - - return (value === term); - } - - function range(record, filter) { - var startnull = (filter.start == null || filter.start === ''); - var stopnull = (filter.stop == null || filter.stop === ''); - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var start = parse(filter.start); - var stop = parse(filter.stop); - - // if at least one end of range is set do not allow '' to get through - // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!startnull || !stopnull) && value === '') { - return false; - } - return ((startnull || value >= start) && (stopnull || value <= stop)); - } - - function geo_distance() { - // TODO code here - } -}; - -// we OR across fields but AND across terms in query string -_applyFreeTextQuery = function(results, queryObj) { - if (queryObj.q) { - var terms = queryObj.q.split(' '); - results = _.filter(results, function(rawdoc) { - var matches = true; - _.each(terms, function(term) { - var foundmatch = false; - _.each(_.keys(rawdoc), function(field) { - var value = rawdoc[field]; - if (value !== null) { value = value.toString(); } - // TODO regexes? - foundmatch = foundmatch || (value === term); - // TODO: early out (once we are true should break to spare unnecessary testing) - // if (foundmatch) return true; - }); - matches = matches && foundmatch; - // TODO: early out (once false should break to spare unnecessary testing) - // if (!matches) return false; - }); - return matches; - }); - } - return results; -}; - -_computeFacets = function(records, queryObj) { - var facetResults = {}; - if (!queryObj.facets) { - return facetResults; - } - _.each(queryObj.facets, function(query, facetId) { - // TODO: remove dependency on recline.Model - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON(); - facetResults[facetId].termsall = {}; - }); - // faceting - _.each(records, function(doc) { - _.each(queryObj.facets, function(query, facetId) { - var fieldId = query.terms.field; - var val = doc[fieldId]; - var tmp = facetResults[facetId]; - if (val) { - tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1; - } else { - tmp.missing = tmp.missing + 1; - } - }); - }); - _.each(queryObj.facets, function(query, facetId) { - var tmp = facetResults[facetId]; - var terms = _.map(tmp.termsall, function(count, term) { - return { term: term, count: count }; - }); - tmp.terms = _.sortBy(terms, function(item) { - // want descending order - return -item.count; - }); - tmp.terms = tmp.terms.slice(0, 10); - }); - return facetResults; -}; - -//Define random Id for new records without _id -function randomId(length, chars) { - var mask = ''; - if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; - if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - if (chars.indexOf('#') > -1) mask += '0123456789'; - if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; - var result = ''; - for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))]; - return result; -} - -_createDocument = function (new_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = new_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - delete new_doc['id']; - - if (dataset.record_create) - new_doc = dataset.record_create(new_doc); - if (_id !== 1 && _id !== undefined) { - new_doc['_id'] = _id; - } - else { - new_doc['_id'] = randomId(32, '#a'); - } - dfd.resolve(cdb.upsert(new_doc)); - - return dfd.promise(); -}; - -_updateDocument = function (new_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = new_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - delete new_doc['id']; - - if (view_url.search('_all_docs') !== -1) { - jqxhr = cdb.get(_id); - } - else { - _id = new_doc['_id'].split('__')[0]; - jqxhr = cdb.get(_id); - } - - jqxhr.done(function(old_doc){ - if (dataset.record_update) - new_doc = dataset.record_update(new_doc, old_doc); - new_doc = _.extend(old_doc, new_doc); - new_doc['_id'] = _id; - dfd.resolve(cdb.upsert(new_doc)); - }).fail(function(args){ - dfd.reject(args); - }); - - return dfd.promise(); -}; - -_deleteDocument = function (del_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = del_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - if (view_url.search('_all_docs') !== -1) - return cdb.remove(_id); - else { - _id = model.get('_id').split('__')[0]; - var jqxhr = cdb.get(_id); - - jqxhr.done(function(old_doc){ - if (dataset.record_delete) - old_doc = dataset.record_delete(del_doc, old_doc); - if (_.isNull(del_doc)) - dfd.resolve(cdb.remove(_id)); // XXX is this the right thing to do? - else { - // couchdb uses _id to identify documents, Backbone models use id. - // we should remove it before sending it to the server. - old_doc['_id'] = _id; - delete old_doc['id']; - dfd.resolve(cdb.upsert(old_doc)); - } - }).fail(function(args){ - dfd.reject(args); - }); - return dfd.promise(); - } -}; -}(jQuery, this.recline.Backend.CouchDB)); From 62bd8ffda7894e2c4e3740661d7d546b9ed81f10 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 20 Feb 2013 11:43:42 +0000 Subject: [PATCH 07/46] [#324] Bug fix: fix tooltip content and placement in flot bar graph. --- src/view.flot.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/view.flot.js b/src/view.flot.js index ea87f40f..834cf63f 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -115,6 +115,11 @@ my.Flot = Backbone.View.extend({ 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), @@ -125,6 +130,9 @@ my.Flot = Backbone.View.extend({ // 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 { From c8523d80f213d7362ed840517a504b4ebcff05f9 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 20 Feb 2013 12:11:28 +0000 Subject: [PATCH 08/46] [xs] Bug fix: dataset is already defined. --- demos/multiview/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/multiview/app.js b/demos/multiview/app.js index 89fe5aed..6ea80a4b 100755 --- a/demos/multiview/app.js +++ b/demos/multiview/app.js @@ -27,7 +27,7 @@ jQuery(function($) { ); dataset = new recline.Model.Dataset(datasetInfo); } else { - var dataset = new recline.Model.Dataset({ + dataset = new recline.Model.Dataset({ records: [ {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', title: 'first', lat:52.56, lon:13.40}, {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', title: 'second', lat:54.97, lon:-1.60}, From aaef39c724ac2eb825c7df6e02a827c45a957de5 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 20 Feb 2013 12:15:43 +0000 Subject: [PATCH 09/46] [#324] Show full labels on flot bar y-axis --- src/view.flot.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index 834cf63f..70493ff7 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -191,7 +191,7 @@ my.Flot = Backbone.View.extend({ if (typeof label !== 'string') { label = label.toString(); } - if (label.length > 8) { + if (self.state.attributes.graphType !== 'bars' && label.length > 8) { label = label.slice(0, 5) + "..."; } @@ -211,11 +211,15 @@ my.Flot = Backbone.View.extend({ x = 1, i = 0; - while (x <= maxTicks) { - if ((numPoints / x) <= maxTicks) { - break; + // show all ticks in bar graphs + // for other graphs only show up to maxTicks ticks + if (self.state.attributes.graphType !== 'bars') { + while (x <= maxTicks) { + if ((numPoints / x) <= maxTicks) { + break; + } + x = x + 1; } - x = x + 1; } for (i = 0; i < numPoints; i = i + x) { From 97f28e3dbabb9b80fbc8985238f6e4b803dd446a Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 20 Feb 2013 18:20:21 +0000 Subject: [PATCH 10/46] [#324] Bug fix: call view.show after determining sidebar visibility Makes sure that views that need to know their maximum width before redrawing don't draw over the sidebar. --- src/view.multiview.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/view.multiview.js b/src/view.multiview.js index 52481e85..c69612b6 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -299,16 +299,14 @@ my.MultiView = Backbone.View.extend({ this.el.find('.navigation a').removeClass('active'); var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); $el.addClass('active'); - // show the specific page + + // add/remove sidebars and hide inactive views _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { view.view.el.show(); if (view.view.elSidebar) { view.view.elSidebar.show(); } - if (view.view.show) { - view.view.show(); - } } else { view.view.el.hide(); if (view.view.elSidebar) { @@ -319,6 +317,18 @@ my.MultiView = Backbone.View.extend({ } } }); + + this._showHideSidebar(); + + // call view.view.show after sidebar visibility has been determined so + // that views can correctly calculate their maximum width + _.each(this.pageViews, function(view, idx) { + if (view.id === pageName) { + if (view.view.show) { + view.view.show(); + } + } + }); }, _onMenuClick: function(e) { @@ -333,7 +343,6 @@ my.MultiView = Backbone.View.extend({ var viewName = $(e.target).attr('data-view'); this.updateNav(viewName); this.state.set({currentView: viewName}); - this._showHideSidebar(); }, // create a state object for this view and do the job of From 9fcedc21897169216407981d1961856637aef162 Mon Sep 17 00:00:00 2001 From: Mark Brough Date: Sat, 23 Feb 2013 16:07:42 +0000 Subject: [PATCH 11/46] Date bug corrected, fixes #327 --- src/view.flot.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index 70493ff7..20d9c374 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -191,8 +191,8 @@ my.Flot = Backbone.View.extend({ if (typeof label !== 'string') { label = label.toString(); } - if (self.state.attributes.graphType !== 'bars' && label.length > 8) { - label = label.slice(0, 5) + "..."; + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; } return label; @@ -322,12 +322,7 @@ my.Flot = Backbone.View.extend({ 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; - } + x = index; } else if (typeof x === 'string') { x = parseFloat(x); if (isNaN(x)) { From accf6518e9fd59d69eefc99973b3023ff208ba35 Mon Sep 17 00:00:00 2001 From: Mark Brough Date: Sat, 23 Feb 2013 16:18:01 +0000 Subject: [PATCH 12/46] Show field labels rather than id on legend --- src/view.flot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index 20d9c374..5506c4f3 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -313,6 +313,7 @@ my.Flot = Backbone.View.extend({ var series = []; _.each(this.state.attributes.series, function(field) { var points = []; + var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); @@ -341,7 +342,7 @@ my.Flot = Backbone.View.extend({ }); series.push({ data: points, - label: field, + label: fieldLabel, hoverable: true }); }); From dc55d53fdd9d16d2480a9c4a2efdc78da90f2572 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 24 Feb 2013 14:34:28 +0000 Subject: [PATCH 13/46] [#330,model][s]: getFieldValue & getFieldValueUnrendered now return '' if field argument is null / undefined - fixes #330. --- src/model.js | 9 ++++++++- test/model.test.js | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/model.js b/src/model.js index 2f405762..b76986e4 100644 --- a/src/model.js +++ b/src/model.js @@ -309,9 +309,11 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding rendered computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { val = this.getFieldValueUnrendered(field); - if (field.renderer) { + if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } return val; @@ -321,7 +323,12 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValueUnrendered: function(field) { + if (!field) { + return ''; + } var val = this.get(field.id); if (field.deriver) { val = field.deriver(val, field, this); diff --git a/test/model.test.js b/test/model.test.js index 0d227908..e4ba3617 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -60,6 +60,20 @@ test('Field: type mapping', function () { }); }); +test('Field: getFieldValue', function () { + var doc = new recline.Model.Record({ + x: 12.3 + }); + var field = new recline.Model.Field({id: 'x'}); + var out = doc.getFieldValue(field); + var exp = 12.3; + equal(out, exp); + + // bad value + var out = doc.getFieldValue(); + equal(out, ''); +}); + test('Field: default renderers', function () { var doc = new recline.Model.Record({ x: 12.3, @@ -113,8 +127,10 @@ test('Field: custom deriver and renderer', function () { var field = new recline.Model.Field({id: 'computed', is_derived: true}, { deriver: deriver }); + var out1 = doc.getFieldValueUnrendered(field); var out = doc.getFieldValue(field); var exp = 246; + equal(out1, exp); equal(out, exp); var field = new recline.Model.Field({id: 'x'}, { From 44ec0a3d5ccd0fe4675561310ddb26926b33fcf8 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 24 Feb 2013 14:36:13 +0000 Subject: [PATCH 14/46] [build][s]: regular build of js. --- dist/recline.css | 23 +++--- dist/recline.dataset.js | 9 +- dist/recline.js | 179 ++++++++++++++-------------------------- 3 files changed, 84 insertions(+), 127 deletions(-) diff --git a/dist/recline.css b/dist/recline.css index bc71116c..e6437a11 100644 --- a/dist/recline.css +++ b/dist/recline.css @@ -40,7 +40,6 @@ .recline-graph .graph .alert { width: 450px; - margin: auto; } .flotr-mouse-value { @@ -60,7 +59,8 @@ .flotr-legend-color-box { padding: 5px; -}/********************************************************** +} +/********************************************************** * (Data) Grid *********************************************************/ @@ -255,7 +255,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-read-only a.row-header-menu { display: none; } - .recline-map .map { height: 500px; } @@ -286,7 +285,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { } .recline-data-explorer .data-view-container { display: block; - margin-right: 225px; } .recline-data-explorer .data-view-sidebar { @@ -384,6 +382,7 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-filter-editor { padding: 8px; + display: none; } .recline-filter-editor .filter-term a { @@ -401,6 +400,10 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { * Fields Widget *********************************************************/ +.recline-fields-view { + display: none; +} + .recline-fields-view .fields-list { padding: 0; } @@ -635,14 +638,17 @@ classes should alter those! cursor: pointer; } -.recline-transform .script { - margin-right: 10px; +.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 { @@ -654,10 +660,6 @@ classes should alter those! margin-top: -2px; } -.recline-transform .preview { - margin-right: 10px; -} - .expression-preview-parsing-status { color: #999; } @@ -673,4 +675,3 @@ classes should alter those! .recline-transform .before-after .after.different { font-weight: bold; } - diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index 92183702..7579c2ec 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -309,9 +309,11 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding rendered computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { val = this.getFieldValueUnrendered(field); - if (field.renderer) { + if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } return val; @@ -321,7 +323,12 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValueUnrendered: function(field) { + if (!field) { + return ''; + } var val = this.get(field.id); if (field.deriver) { val = field.deriver(val, field, this); diff --git a/dist/recline.js b/dist/recline.js index 297cc26b..9e410e6d 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -76,7 +76,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; actualQuery.sort = _tmp.join(','); } return actualQuery; - } + }; my.query = function(queryObj, dataset) { if (dataset.endpoint) { @@ -92,7 +92,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; jqxhr.done(function(results) { var out = { total: results.result.total, - hits: results.result.records, + hits: results.result.records }; dfd.resolve(out); }); @@ -116,7 +116,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; dataType: 'json' }); return jqxhr; - } + }; return that; }; @@ -130,7 +130,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; return { resource_id: parts[len-1], endpoint: parts.slice(0,[len-4]).join('/') + '/api' - } + }; }; var CKAN_TYPES_MAP = { @@ -1207,74 +1207,6 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; }(this.recline.Backend.Memory)); this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.Solr = this.recline.Backend.Solr || {}; - -(function($, my) { - my.__type__ = 'solr'; - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; - - // ### fetch - // - // dataset must have a solr or url attribute pointing to solr endpoint - my.fetch = function(dataset) { - var jqxhr = $.ajax({ - url: dataset.solr || dataset.url, - data: { - rows: 1, - wt: 'json' - }, - dataType: 'jsonp', - jsonp: 'json.wrf' - }); - var dfd = new Deferred(); - jqxhr.done(function(results) { - // if we get 0 results we cannot get fields - var fields = [] - if (results.response.numFound > 0) { - fields = _.map(_.keys(results.response.docs[0]), function(fieldName) { - return { id: fieldName }; - }); - } - var out = { - fields: fields, - useMemoryStore: false - }; - dfd.resolve(out); - }); - return dfd.promise(); - } - - // TODO - much work on proper query support is needed!! - my.query = function(queryObj, dataset) { - var q = queryObj.q || '*:*'; - var data = { - q: q, - rows: queryObj.size, - start: queryObj.from, - wt: 'json' - }; - var jqxhr = $.ajax({ - url: dataset.solr || dataset.url, - data: data, - dataType: 'jsonp', - jsonp: 'json.wrf' - }); - var dfd = new Deferred(); - jqxhr.done(function(results) { - var out = { - total: results.response.numFound, - hits: results.response.docs - }; - dfd.resolve(out); - }); - return dfd.promise(); - }; - -}(jQuery, this.recline.Backend.Solr)); -this.recline = this.recline || {}; this.recline.Data = this.recline.Data || {}; (function(my) { @@ -1718,9 +1650,11 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding rendered computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { val = this.getFieldValueUnrendered(field); - if (field.renderer) { + if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } return val; @@ -1730,7 +1664,12 @@ my.Record = Backbone.Model.extend({ // // For the provided Field get the corresponding computed data value // for this record. + // + // NB: if field is undefined a default '' value will be returned getFieldValueUnrendered: function(field) { + if (!field) { + return ''; + } var val = this.get(field.id); if (field.deriver) { val = field.deriver(val, field, this); @@ -2116,6 +2055,11 @@ my.Flot = Backbone.View.extend({ 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), @@ -2126,6 +2070,9 @@ my.Flot = Backbone.View.extend({ // 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 { @@ -2184,8 +2131,8 @@ my.Flot = Backbone.View.extend({ if (typeof label !== 'string') { label = label.toString(); } - if (label.length > 8) { - label = label.slice(0, 5) + "..."; + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; } return label; @@ -2204,11 +2151,15 @@ my.Flot = Backbone.View.extend({ x = 1, i = 0; - while (x <= maxTicks) { - if ((numPoints / x) <= maxTicks) { - break; + // show all ticks in bar graphs + // for other graphs only show up to maxTicks ticks + if (self.state.attributes.graphType !== 'bars') { + while (x <= maxTicks) { + if ((numPoints / x) <= maxTicks) { + break; + } + x = x + 1; } - x = x + 1; } for (i = 0; i < numPoints; i = i + x) { @@ -2311,12 +2262,7 @@ my.Flot = Backbone.View.extend({ 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; - } + x = index; } else if (typeof x === 'string') { x = parseFloat(x); if (isNaN(x)) { @@ -2964,7 +2910,6 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; this.recline.View.Graph = this.recline.View.Flot; this.recline.View.GraphControls = this.recline.View.FlotControls; - /*jshint multistr:true */ this.recline = this.recline || {}; @@ -4001,7 +3946,7 @@ my.MultiView = Backbone.View.extend({ \ @@ -4092,6 +4037,7 @@ my.MultiView = Backbone.View.extend({ } else { this.updateNav(this.pageViews[0].id); } + this._showHideSidebar(); this.model.bind('query:start', function() { self.notify({loader: true, persist: true}); @@ -4169,20 +4115,32 @@ my.MultiView = Backbone.View.extend({ }, + // hide the sidebar if empty + _showHideSidebar: function() { + var $dataSidebar = this.el.find('.data-view-sidebar'); + var visibleChildren = $dataSidebar.children().filter(function() { + return $(this).css("display") != "none"; + }).length; + + if (visibleChildren > 0) { + $dataSidebar.show(); + } else { + $dataSidebar.hide(); + } + }, + updateNav: function(pageName) { this.el.find('.navigation a').removeClass('active'); var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); $el.addClass('active'); - // show the specific page + + // add/remove sidebars and hide inactive views _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { view.view.el.show(); if (view.view.elSidebar) { view.view.elSidebar.show(); } - if (view.view.show) { - view.view.show(); - } } else { view.view.el.hide(); if (view.view.elSidebar) { @@ -4193,12 +4151,25 @@ my.MultiView = Backbone.View.extend({ } } }); + + this._showHideSidebar(); + + // call view.view.show after sidebar visibility has been determined so + // that views can correctly calculate their maximum width + _.each(this.pageViews, function(view, idx) { + if (view.id === pageName) { + if (view.view.show) { + view.view.show(); + } + } + }); }, _onMenuClick: function(e) { e.preventDefault(); var action = $(e.target).attr('data-action'); this['$'+action].toggle(); + this._showHideSidebar(); }, _onSwitchView: function(e) { @@ -5230,7 +5201,7 @@ my.Fields = Backbone.View.extend({ \ \
\ -
\ +
\
\ {{#facets}} \
\ @@ -5249,9 +5220,6 @@ my.Fields = Backbone.View.extend({
\ ', - events: { - 'click .js-show-hide': 'onShowHide' - }, initialize: function(model) { var self = this; this.el = $(this.el); @@ -5269,6 +5237,7 @@ my.Fields = Backbone.View.extend({ self.model.getFieldsSummary(); self.render(); }); + this.el.find('.collapse').collapse(); this.render(); }, render: function() { @@ -5283,28 +5252,10 @@ my.Fields = Backbone.View.extend({ }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - this.el.find('.collapse').collapse('hide'); - }, - onShowHide: function(e) { - e.preventDefault(); - var $target = $(e.target); - // weird collapse class seems to have been removed (can watch this happen - // if you watch dom) but could not work why. Absence of collapse then meant - // we could not toggle. - // This seems to fix the problem. - this.el.find('.accordion-body').addClass('collapse');; - if ($target.text() === '+') { - this.el.find('.collapse').collapse('show'); - $target.text('-'); - } else { - this.el.find('.collapse').collapse('hide'); - $target.text('+'); - } } }); })(jQuery, recline.View); - /*jshint multistr:true */ this.recline = this.recline || {}; @@ -5430,8 +5381,6 @@ my.FilterEditor = Backbone.View.extend({ var filterType = $target.find('select.filterType').val(); var field = $target.find('select.fields').val(); this.model.queryState.addFilter({type: filterType, field: field}); - // trigger render explicitly as queryState change will not be triggered (as blank value for filter) - this.render(); }, onRemoveFilter: function(e) { e.preventDefault(); From cf115ef79a67ad0fc8b32165a203b64c1117184f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 24 Feb 2013 14:36:52 +0000 Subject: [PATCH 15/46] [build][xs]: . --- dist/recline.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dist/recline.js b/dist/recline.js index 9e410e6d..3876eb06 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -2253,6 +2253,7 @@ my.Flot = Backbone.View.extend({ var series = []; _.each(this.state.attributes.series, function(field) { var points = []; + var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); @@ -2281,7 +2282,7 @@ my.Flot = Backbone.View.extend({ }); series.push({ data: points, - label: field, + label: fieldLabel, hoverable: true }); }); From 7ec2fdb27863ba6dc3941a1b03eb33bbffec0b18 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 8 Mar 2013 10:30:02 +0000 Subject: [PATCH 16/46] [#334,flot,bugfix][s]: find and resolve issue with no ticks on x-axis if values are floats - fixes #334. * Change to explicitly signal the Labels / Dates case (where x values are indices into records array) * No longer always set xticks - let flot do this except in indices case --- src/view.flot.js | 57 +++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index 5506c4f3..f23abc87 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -159,14 +159,19 @@ my.Flot = Backbone.View.extend({ 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(); + 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; }, @@ -201,31 +206,16 @@ my.Flot = Backbone.View.extend({ 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; - - // show all ticks in bar graphs - // for other graphs only show up to maxTicks ticks - if (self.state.attributes.graphType !== 'bars') { - while (x <= maxTicks) { - if ((numPoints / x) <= maxTicks) { - break; - } - x = x + 1; - } + // 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 30 (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 Date: Fri, 8 Mar 2013 10:33:51 +0000 Subject: [PATCH 17/46] [build][xs]: . --- dist/recline.js | 57 ++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index 3876eb06..e741bbbc 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -2099,14 +2099,19 @@ my.Flot = Backbone.View.extend({ 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(); + 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; }, @@ -2141,31 +2146,16 @@ my.Flot = Backbone.View.extend({ 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; - - // show all ticks in bar graphs - // for other graphs only show up to maxTicks ticks - if (self.state.attributes.graphType !== 'bars') { - while (x <= maxTicks) { - if ((numPoints / x) <= maxTicks) { - break; - } - x = x + 1; - } + // 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 30 (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 Date: Thu, 21 Mar 2013 10:55:05 -0600 Subject: [PATCH 18/46] [#286, bugfix]: Fix case for null in Tick labels Trying to run label.toString() on null value in tickFormatter results in error. --- src/view.flot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index f23abc87..018a2963 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -191,7 +191,7 @@ my.Flot = Backbone.View.extend({ // 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); + var label = self._xaxisLabel(x) || ""; if (typeof label !== 'string') { label = label.toString(); From d0f82fd1652d821ad28bb9c5660a2286a617f882 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 27 Mar 2013 19:58:15 +0100 Subject: [PATCH 19/46] [#217] Implement value/term filter for ckan backend --- src/backend.ckan.js | 19 ++++++++++++++----- test/backend.ckan.test.js | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/backend.ckan.js b/src/backend.ckan.js index 1b59bf8c..893067a1 100644 --- a/src/backend.ckan.js +++ b/src/backend.ckan.js @@ -66,15 +66,25 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; 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; }; @@ -105,15 +115,14 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; // // @param endpoint: CKAN api endpoint (e.g. http://datahub.io/api) my.DataStore = function(endpoint) { - var that = { - endpoint: endpoint || my.API_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, - data: data, - dataType: 'json' + type: 'POST', + data: JSON.stringify(data) }); return jqxhr; }; diff --git a/test/backend.ckan.test.js b/test/backend.ckan.test.js index e0598933..e985e656 100644 --- a/test/backend.ckan.test.js +++ b/test/backend.ckan.test.js @@ -30,6 +30,7 @@ test('_normalizeQuery', function() { var exp = { resource_id: dataset.id, q: 'abc', + filters: {}, sort: 'location desc,last ', limit: 10, offset: 0 From 72a65213c0c06b44fb078fd1802af6ac33253ed2 Mon Sep 17 00:00:00 2001 From: John Glover Date: Tue, 2 Apr 2013 17:11:07 +0200 Subject: [PATCH 20/46] [#336] Add valuefilter widget --- _includes/recline-deps.html | 1 + css/multiview.css | 12 ++++ src/widget.valuefilter.js | 115 ++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 src/widget.valuefilter.js diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index 7cd8f7f4..4174fbfb 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -69,6 +69,7 @@ + diff --git a/css/multiview.css b/css/multiview.css index be894449..6837e7c2 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -110,6 +110,18 @@ width: 175px; } +.recline-filter-editor input { + margin-top: 0.5em; +} + +.recline-filter-editor .add-filter { + margin-top: 1em; + margin-bottom: 2em; +} + +.recline-filter-editor .update-filter { + margin-top: 1em; +} /********************************************************** * Fields Widget diff --git a/src/widget.valuefilter.js b/src/widget.valuefilter.js new file mode 100644 index 00000000..60c08e65 --- /dev/null +++ b/src/widget.valuefilter.js @@ -0,0 +1,115 @@ +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + +my.ValueFilter = Backbone.View.extend({ + className: 'recline-filter-editor well', + template: ' \ +
\ +

Filters

\ + \ + \ +
\ + {{#filters}} \ + {{{filterRender}}} \ + {{/filters}} \ + {{#filters.length}} \ + \ + {{/filters.length}} \ +
\ +
\ + ', + filterTemplates: { + term: ' \ +
\ +
\ + {{field}} \ + × \ + \ +
\ +
\ + ' + }, + events: { + 'click .js-remove-filter': 'onRemoveFilter', + 'click .js-add-filter': 'onAddFilterShow', + 'submit form.js-edit': 'onTermFiltersUpdate', + 'submit form.js-add': 'onAddFilter' + }, + initialize: function() { + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.fields.bind('all', this.render); + this.model.queryState.bind('change', this.render); + this.model.queryState.bind('change:filters:new-blank', this.render); + this.render(); + }, + render: function() { + var self = this; + var tmplData = $.extend(true, {}, this.model.queryState.toJSON()); + // we will use idx in list as the id ... + tmplData.filters = _.map(tmplData.filters, function(filter, idx) { + filter.id = idx; + return filter; + }); + tmplData.fields = this.model.fields.toJSON(); + tmplData.filterRender = function() { + return Mustache.render(self.filterTemplates.term, this); + }; + var out = Mustache.render(this.template, tmplData); + this.el.html(out); + }, + updateFilter: function(input) { + var self = this; + var filters = self.model.queryState.get('filters'); + var $input = $(input); + var filterIndex = parseInt($input.attr('data-filter-id'), 10); + var value = $input.val(); + filters[filterIndex].term = value; + }, + onAddFilterShow: function(e) { + e.preventDefault(); + var $target = $(e.target); + $target.hide(); + this.el.find('form.js-add').show(); + }, + onAddFilter: function(e) { + e.preventDefault(); + var $target = $(e.target); + $target.hide(); + var field = $target.find('select.fields').val(); + this.model.queryState.addFilter({type: 'term', field: field}); + }, + onRemoveFilter: function(e) { + e.preventDefault(); + var $target = $(e.target); + var filterId = $target.attr('data-filter-id'); + this.model.queryState.removeFilter(filterId); + }, + onTermFiltersUpdate: function(e) { + var self = this; + e.preventDefault(); + var filters = self.model.queryState.get('filters'); + var $form = $(e.target); + _.each($form.find('input'), function(input) { + self.updateFilter(input); + }); + self.model.queryState.set({filters: filters, from: 0}); + self.model.queryState.trigger('change'); + } +}); + +})(jQuery, recline.View); From 457e39dc1e6dfcc63bf0aa044166c77b87de8531 Mon Sep 17 00:00:00 2001 From: John Glover Date: Tue, 2 Apr 2013 17:24:37 +0200 Subject: [PATCH 21/46] [#336] Add tests for valuefilter widget --- test/index.html | 1 + test/widget.valuefilter.test.js | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 test/widget.valuefilter.test.js diff --git a/test/index.html b/test/index.html index cfa1151e..31b8bb08 100644 --- a/test/index.html +++ b/test/index.html @@ -71,6 +71,7 @@ + diff --git a/test/widget.valuefilter.test.js b/test/widget.valuefilter.test.js new file mode 100644 index 00000000..6b8a86a1 --- /dev/null +++ b/test/widget.valuefilter.test.js @@ -0,0 +1,67 @@ +module("Widget - Value Filter"); + +test('basics', function () { + var dataset = Fixture.getDataset(); + var view = new recline.View.FilterEditor({ + model: dataset + }); + $('.fixtures').append(view.el); + assertPresent('.js-add-filter', view.elSidebar); + var $addForm = view.el.find('form.js-add'); + ok(!$addForm.is(":visible")); + view.el.find('.js-add-filter').click(); + ok(!view.el.find('.js-add-filter').is(":visible")); + ok($addForm.is(":visible")); + + // submit the form + $addForm.find('select.fields').val('country'); + $addForm.submit(); + + // now check we have new filter + ok(!$addForm.is(":visible")); + $editForm = view.el.find('form.js-edit'); + equal($editForm.find('.filter-term').length, 1); + equal(dataset.queryState.attributes.filters[0].field, 'country'); + + // now set filter value and apply + $editForm.find('input').val('UK'); + $editForm.submit(); + equal(dataset.queryState.attributes.filters[0].term, 'UK'); + equal(dataset.records.length, 3); + + // now remove filter + $editForm = view.el.find('form.js-edit'); + $editForm.find('.js-remove-filter').last().click(); + $editForm = view.el.find('form.js-edit'); + equal($editForm.find('.filter').length, 0); + equal(dataset.records.length, 6); + + view.remove(); +}); + +test('add 2 filters', function () { + var dataset = Fixture.getDataset(); + var view = new recline.View.FilterEditor({ + model: dataset + }); + $('.fixtures').append(view.el); + + // add 2 term filters + var $addForm = view.el.find('form.js-add'); + view.el.find('.js-add-filter').click(); + $addForm.find('select.fields').val('country'); + $addForm.submit(); + + $addForm = view.el.find('form.js-add'); + view.el.find('.js-add-filter').click(); + $addForm.find('select.fields').val('id'); + $addForm.submit(); + + var fields = []; + view.el.find('form.js-edit .filter-term input').each(function(idx, item) { + fields.push($(item).attr('data-filter-field')); + }); + deepEqual(fields, ['country', 'id']); + + view.remove(); +}); From d746e87b2aba541185e84e961cc1d9a13804c529 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 15 Apr 2013 11:24:32 +0100 Subject: [PATCH 22/46] Expose Timeline JS initialization options --- src/view.timeline.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view.timeline.js b/src/view.timeline.js index a2e85308..cccf6c60 100644 --- a/src/view.timeline.js +++ b/src/view.timeline.js @@ -38,7 +38,8 @@ my.Timeline = Backbone.View.extend({ }); var stateData = _.extend({ startField: null, - endField: null + endField: null, + timelineJSOptions: {} }, options.state ); @@ -71,9 +72,8 @@ my.Timeline = Backbone.View.extend({ if (width) { $timeline.width(width); } - var config = {}; var data = this._timelineJSON(); - this.timeline.init(data, this.elementId, config); + this.timeline.init(data, this.elementId, this.state.get("timelineJSOptions")); this._timelineIsInitialized = true }, From c4bdca79adb23e6e9f5d44f92afc591a337401b2 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:10:41 +0200 Subject: [PATCH 23/46] [#340] Value of 30 in comment contradicts actual value in code (15) --- src/view.flot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index 018a2963..10bab4eb 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -208,7 +208,7 @@ my.Flot = Backbone.View.extend({ // 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 30 (we assume) + // 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; From bada4a877b7cbded8eaf147a491460ec586a981d Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:11:54 +0200 Subject: [PATCH 24/46] [#340] jshint cleanup of view.flot.js --- src/view.flot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index 10bab4eb..b50c6000 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -214,7 +214,7 @@ my.Flot = Backbone.View.extend({ var increment = this.model.records.length / numTicks; var ticks = []; for (i=0; i Date: Mon, 15 Apr 2013 17:15:08 +0200 Subject: [PATCH 25/46] [#340] jshint cleanup of backend.ckan.js --- src/backend.ckan.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend.ckan.js b/src/backend.ckan.js index 893067a1..19998cab 100644 --- a/src/backend.ckan.js +++ b/src/backend.ckan.js @@ -37,12 +37,13 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; // ### fetch my.fetch = function(dataset) { + var wrapper; if (dataset.endpoint) { - var wrapper = my.DataStore(dataset.endpoint); + wrapper = my.DataStore(dataset.endpoint); } else { var out = my._parseCkanResourceUrl(dataset.url); dataset.id = out.resource_id; - var wrapper = my.DataStore(out.endpoint); + wrapper = my.DataStore(out.endpoint); } var dfd = new Deferred(); var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0}); @@ -89,12 +90,13 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {}; }; my.query = function(queryObj, dataset) { + var wrapper; if (dataset.endpoint) { - var wrapper = my.DataStore(dataset.endpoint); + wrapper = my.DataStore(dataset.endpoint); } else { var out = my._parseCkanResourceUrl(dataset.url); dataset.id = out.resource_id; - var wrapper = my.DataStore(out.endpoint); + wrapper = my.DataStore(out.endpoint); } var actualQuery = my._normalizeQuery(queryObj, dataset); var dfd = new Deferred(); From 6073c4837f57da00c5ce552792a22ff548a212a4 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:17:01 +0200 Subject: [PATCH 26/46] [#340] jshint cleanup of backend.dataproxy.js --- src/backend.dataproxy.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backend.dataproxy.js b/src/backend.dataproxy.js index 44db50cf..d16e6553 100644 --- a/src/backend.dataproxy.js +++ b/src/backend.dataproxy.js @@ -42,8 +42,8 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; useMemoryStore: true }); }) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); return dfd.promise(); }; @@ -60,16 +60,16 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds' }); }, my.timeout); - ourFunction.done(function(arguments) { + ourFunction.done(function(args) { clearTimeout(timer); - dfd.resolve(arguments); + dfd.resolve(args); }) - .fail(function(arguments) { + .fail(function(args) { clearTimeout(timer); - dfd.reject(arguments); + dfd.reject(args); }) ; return dfd.promise(); - } + }; }(this.recline.Backend.DataProxy)); From 9e47ea3ad9f6cde9d37de2578a670ea977489f02 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:35:59 +0200 Subject: [PATCH 27/46] [#340] jshint cleanup of model.js --- src/model.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/model.js b/src/model.js index b76986e4..1e8b0bd9 100644 --- a/src/model.js +++ b/src/model.js @@ -55,8 +55,8 @@ my.Dataset = Backbone.Model.extend({ if (this.backend !== recline.Backend.Memory) { this.backend.fetch(this.toJSON()) .done(handleResults) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); } else { // special case where we have been given data directly @@ -79,8 +79,8 @@ my.Dataset = Backbone.Model.extend({ .done(function() { dfd.resolve(self); }) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); } @@ -198,9 +198,9 @@ my.Dataset = Backbone.Model.extend({ self.trigger('query:done'); dfd.resolve(self.records); }) - .fail(function(arguments) { - self.trigger('query:fail', arguments); - dfd.reject(arguments); + .fail(function(args) { + self.trigger('query:fail', args); + dfd.reject(args); }); return dfd.promise(); }, @@ -450,7 +450,7 @@ my.Field = Backbone.Model.extend({ if (val && typeof val === 'string') { val = val.replace(/(https?:\/\/[^ ]+)/g, '$1'); } - return val + return val; } } } From c77b35d9f21f09a780c1e5a16635d47b7c7f6843 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:36:09 +0200 Subject: [PATCH 28/46] [#340] jshint cleanup of view.grid.js --- src/view.grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view.grid.js b/src/view.grid.js index c13a8c21..2a6a2b55 100644 --- a/src/view.grid.js +++ b/src/view.grid.js @@ -103,12 +103,12 @@ my.Grid = Backbone.View.extend({ var numFields = this.fields.length; // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; - var width = parseInt(Math.max(50, fullWidth / numFields)); + var width = parseInt(Math.max(50, fullWidth / numFields), 10); // if columns extend outside viewport then remainder is 0 var remainder = Math.max(fullWidth - numFields * width,0); _.each(this.fields, function(field, idx) { // add the remainder to the first field width so we make up full col - if (idx == 0) { + if (idx === 0) { field.set({width: width+remainder}); } else { field.set({width: width}); From 522e88fe415eb448db4fa3c442ad8f7d40b15e71 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:36:22 +0200 Subject: [PATCH 29/46] [#340] jshint cleanup of view.multiview.js --- src/view.multiview.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/view.multiview.js b/src/view.multiview.js index c69612b6..d0640ff7 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -464,13 +464,14 @@ my.MultiView = Backbone.View.extend({ // This inverts the state serialization process in Multiview my.MultiView.restore = function(state) { // hack-y - restoring a memory dataset does not mean much ... (but useful for testing!) + var datasetInfo; if (state.backend === 'memory') { - var datasetInfo = { + datasetInfo = { backend: 'memory', records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}] }; } else { - var datasetInfo = _.extend({ + datasetInfo = _.extend({ url: state.url, backend: state.backend }, @@ -483,7 +484,7 @@ my.MultiView.restore = function(state) { state: state }); return explorer; -} +}; // ## Miscellaneous Utilities var urlPathRegex = /^([^?]+)(\?.*)?/; From 5839ab8fa6f6f248e5b1268c2eae99c9e4ec4b31 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 15 Apr 2013 17:58:28 +0200 Subject: [PATCH 30/46] [#340] jshint cleanup of view.slickgrid.js --- src/view.slickgrid.js | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index d9f61deb..88d20319 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -39,7 +39,7 @@ my.SlickGrid = Backbone.View.extend({ this.model.records.bind('add', this.render); this.model.records.bind('reset', this.render); this.model.records.bind('remove', this.render); - this.model.records.bind('change', this.onRecordChanged, this) + this.model.records.bind('change', this.onRecordChanged, this); var state = _.extend({ hiddenColumns: [], @@ -52,7 +52,6 @@ my.SlickGrid = Backbone.View.extend({ }, modelEtc.state ); -// this.grid_options = modelEtc.options; this.state = new recline.Model.ObjectState(state); }, @@ -95,25 +94,25 @@ my.SlickGrid = Backbone.View.extend({ } else { return value; } - } + }; _.each(this.model.fields.toJSON(),function(field){ var column = { - id:field['id'], - name:field['label'], - field:field['id'], + id:field.id, + name:field.label, + field:field.id, sortable: true, minWidth: 80, formatter: formatter }; - var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id}); + var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id;}); if (widthInfo){ - column['width'] = widthInfo.width; + column.width = widthInfo.width; } - var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column == field.id}); + var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column == field.id;}); if (editInfo){ - column['editor'] = editInfo.editor; + column.editor = editInfo.editor; } columns.push(column); }); @@ -159,18 +158,18 @@ my.SlickGrid = Backbone.View.extend({ this.push = function(model, row) { models.push(model); rows.push(row); - } - - this.getLength = function() { return rows.length; } - this.getItem = function(index) { return rows[index];} - this.getItemMetadata= function(index) { return {};} - this.getModel= function(index) { return models[index]; } - this.getModelRow = function(m) { return models.indexOf(m);} - this.updateItem = function(m,i) { - rows[i] = toRow(m); - models[i] = m }; - }; + + this.getLength = function() {return rows.length; }; + this.getItem = function(index) {return rows[index];}; + this.getItemMetadata= function(index) {return {};}; + this.getModel= function(index) {return models[index];}; + this.getModelRow = function(m) {return models.indexOf(m);}; + this.updateItem = function(m,i) { + rows[i] = toRow(m); + models[i] = m; + }; + } var data = new RowSet(); @@ -184,7 +183,7 @@ my.SlickGrid = Backbone.View.extend({ var sortInfo = this.model.queryState.get('sort'); if (sortInfo){ var column = sortInfo[0].field; - var sortAsc = !(sortInfo[0].order == 'desc'); + var sortAsc = sortInfo[0].order !== 'desc'; this.grid.setSortColumn(column, sortAsc); } @@ -218,7 +217,7 @@ my.SlickGrid = Backbone.View.extend({ // var grid = args.grid; var model = data.getModel(args.row); - var field = grid.getColumns()[args.cell]['id']; + var field = grid.getColumns()[args.cell].id; var v = {}; v[field] = args.item[field]; model.set(v); @@ -279,7 +278,7 @@ my.SlickGrid = Backbone.View.extend({ $menu = $(' diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index 6bdfeea9..f4d09e5f 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -55,7 +55,6 @@ - diff --git a/docs/src/backend.ckan.html b/docs/src/backend.ckan.html deleted file mode 100644 index 2130baa2..00000000 --- a/docs/src/backend.ckan.html +++ /dev/null @@ -1,125 +0,0 @@ - backend.ckan.js

backend.ckan.js

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. -
  3. ID of resource for which request is being made
  4. -
- -

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';

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) {
-    if (dataset.endpoint) {
-      var wrapper = my.DataStore(dataset.endpoint);
-    } else {
-      var out = my._parseCkanResourceUrl(dataset.url);
-      dataset.id = out.resource_id;
-      var wrapper = my.DataStore(out.endpoint);
-    }
-    var dfd = $.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,
-      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(',');
-    }
-    return actualQuery;
-  }
-
-  my.query = function(queryObj, dataset) {
-    if (dataset.endpoint) {
-      var wrapper = my.DataStore(dataset.endpoint);
-    } else {
-      var out = my._parseCkanResourceUrl(dataset.url);
-      dataset.id = out.resource_id;
-      var wrapper = my.DataStore(out.endpoint);
-    }
-    var actualQuery = my._normalizeQuery(queryObj, dataset);
-    var dfd = $.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 = $.ajax({
-        url: searchUrl,
-        data: data,
-        dataType: 'json'
-      });
-      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'
-  };
-
-}(jQuery, this.recline.Backend.Ckan));
-
-
\ No newline at end of file diff --git a/src/backend.ckan.js b/src/backend.ckan.js deleted file mode 100644 index 19998cab..00000000 --- a/src/backend.ckan.js +++ /dev/null @@ -1,153 +0,0 @@ -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)); diff --git a/test/backend.ckan.test.js b/test/backend.ckan.test.js deleted file mode 100644 index e985e656..00000000 --- a/test/backend.ckan.test.js +++ /dev/null @@ -1,203 +0,0 @@ -(function ($) { -module("Backend CKAN"); - -test('_parseCkanResourceUrl', function() { - var resid = 'eb23e809-ccbb-4ad1-820a-19586fc4bebd'; - var url = 'http://demo.ckan.org/dataset/some-dataset/resource/' + resid; - var out = recline.Backend.Ckan._parseCkanResourceUrl(url); - var exp = { - resource_id: resid, - endpoint: 'http://demo.ckan.org/api' - } - deepEqual(out, exp); -}); - -test('_normalizeQuery', function() { - var dataset = new recline.Model.Dataset({ - url: 'does-not-matter', - id: 'xyz', - backend: 'ckan' - }); - - var queryObj = { - q: 'abc', - sort: [ - { field: 'location', order: 'desc' }, - { field: 'last' } - ] - }; - var out = recline.Backend.Ckan._normalizeQuery(queryObj, dataset); - var exp = { - resource_id: dataset.id, - q: 'abc', - filters: {}, - sort: 'location desc,last ', - limit: 10, - offset: 0 - }; - deepEqual(out, exp); -}); - -test("fetch", function() { - var dataset = new recline.Model.Dataset({ - url: 'http://localhost:5000/dataset/test-data-viewer/resource/4f1299ab-a100-4e5f-ba81-e6d234a2f3bd', - backend: 'ckan' - }); - - var stub = sinon.stub($, 'ajax', function(options) { - if (options.url.indexOf('datastore_search') != -1) { - return { - done: function(callback) { - callback(sample_data); - return this; - }, - fail: function() { - } - }; - } - }); - - dataset.fetch().done(function(dataset) { - deepEqual( - _.pluck(dataset.fields.toJSON(), 'id'), - _.pluck(sample_data.result.fields, 'id') - ); - // check we've mapped types correctly - equal(dataset.fields.get('x').get('type'), 'integer'); - equal(dataset.fields.get('country').get('type'), 'string'); - - // fetch does a query so we can check for records - equal(dataset.recordCount, 6); - equal(dataset.records.length, 6); - equal(dataset.records.at(0).get('id'), 0); - equal(dataset.records.at(0).get('country'), 'DE'); - }); - $.ajax.restore(); -}); - -var sample_data = { - "help": "", - "result": { - "fields": [ - { - "id": "_id", - "type": "int4" - }, - { - "id": "id", - "type": "int4" - }, - { - "id": "date", - "type": "date" - }, - { - "id": "x", - "type": "int4" - }, - { - "id": "y", - "type": "int4" - }, - { - "id": "z", - "type": "int4" - }, - { - "id": "country", - "type": "text" - }, - { - "id": "title", - "type": "text" - }, - { - "id": "lat", - "type": "float8" - }, - { - "id": "lon", - "type": "float8" - } - ], - "records": [ - { - "_id": 1, - "country": "DE", - "date": "2011-01-01", - "id": 0, - "lat": 52.56, - "lon": 13.4, - "title": "first", - "x": 1, - "y": 2, - "z": 3 - }, - { - "_id": 2, - "country": "UK", - "date": "2011-02-02", - "id": 1, - "lat": 54.97, - "lon": -1.6, - "title": "second", - "x": 2, - "y": 4, - "z": 24 - }, - { - "_id": 3, - "country": "US", - "date": "2011-03-03", - "id": 2, - "lat": 40.0, - "lon": -75.5, - "title": "third", - "x": 3, - "y": 6, - "z": 9 - }, - { - "_id": 4, - "country": "UK", - "date": "2011-04-04", - "id": 3, - "lat": 57.27, - "lon": -6.2, - "title": "fourth", - "x": 4, - "y": 8, - "z": 6 - }, - { - "_id": 5, - "country": "UK", - "date": "2011-05-04", - "id": 4, - "lat": 51.58, - "lon": 0.0, - "title": "fifth", - "x": 5, - "y": 10, - "z": 15 - }, - { - "_id": 6, - "country": "DE", - "date": "2011-06-02", - "id": 5, - "lat": 51.04, - "lon": 7.9, - "title": "sixth", - "x": 6, - "y": 12, - "z": 18 - } - ], - "resource_id": "4f1299ab-a100-4e5f-ba81-e6d234a2f3bd", - "total": 6 - }, - "success": true -}; - -})(this.jQuery); diff --git a/test/index.html b/test/index.html index a60fa4fa..d728917b 100644 --- a/test/index.html +++ b/test/index.html @@ -37,7 +37,6 @@ - @@ -45,7 +44,6 @@ - From 8164c68d0c02a3bd8cfa6fb294e58d7eafa073a0 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 5 May 2013 19:01:51 +0100 Subject: [PATCH 42/46] [#331,view/flotr2][s]: remove flotr2 as now in separate repo https://github.com/okfn/recline.view.flotr2 - fixes #331. --- css/flotr2.css | 36 --- src/view.flotr2.js | 462 --------------------------------------- test/view.flotr2.test.js | 86 -------- 3 files changed, 584 deletions(-) delete mode 100644 css/flotr2.css delete mode 100644 src/view.flotr2.js delete mode 100644 test/view.flotr2.test.js diff --git a/css/flotr2.css b/css/flotr2.css deleted file mode 100644 index 7b1c085b..00000000 --- a/css/flotr2.css +++ /dev/null @@ -1,36 +0,0 @@ -.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; -} diff --git a/src/view.flotr2.js b/src/view.flotr2.js deleted file mode 100644 index 9259e046..00000000 --- a/src/view.flotr2.js +++ /dev/null @@ -1,462 +0,0 @@ -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - -// ## Graph view for a Dataset using Flotr2 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)} -// } -// -// 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({ - 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'); - 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.editor = new my.Flotr2Controls({ - 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'); - 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 - // - // 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() - 20); - var series = this.createSeries(); - var options = this.getGraphOptions(this.state.attributes.graphType); - this.plot = Flotr.draw(this.$graph.get(0), series, options); - } - }, - - show: function() { - // because we cannot redraw when hidden we may need to when becoming visible - if (this.needToRedraw) { - this.redraw(); - } - }, - - // ### getGraphOptions - // - // Get options for Flotr2 Graph - // - // needs to be function as can depend on state - // - // @param typeId graphType id (lines, lines-and-points etc) - getGraphOptions: function(typeId) { - 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; - } - - 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; - }; - - 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'); - - 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(); - } - return x; - } - - var xaxis = {}; - xaxis.tickFormatter = tickFormatter; - - var yaxis = {}; - yaxis.autoscale = true; - yaxis.autoscaleMargin = 0.02; - - var mouse = {}; - mouse.track = true; - mouse.relative = true; - mouse.trackFormatter = trackFormatter; - - var legend = {}; - legend.position = 'ne'; - - // mouse.lineColor is set in createSeries - var optionsPerGraphType = { - lines: { - legend: legend, - colors: this.graphColors, - lines: { show: true }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse - }, - points: { - legend: legend, - colors: this.graphColors, - points: { show: true, hitRadius: 5 }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse, - grid: { hoverable: true, clickable: true } - }, - 'lines-and-points': { - legend: legend, - colors: this.graphColors, - points: { show: true, hitRadius: 5 }, - lines: { show: true }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse, - grid: { hoverable: true, clickable: true } - }, - bars: { - legend: legend, - colors: this.graphColors, - lines: { show: false }, - xaxis: yaxis, - yaxis: xaxis, - mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' - }, - bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 - } - }, - columns: { - legend: legend, - colors: this.graphColors, - lines: { show: false }, - xaxis: xaxis, - yaxis: yaxis, - mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'n' - }, - bars: { - show: true, - horizontal: false, - shadowSize: 0, - barWidth: 0.8 - } - }, - grid: { hoverable: true, clickable: true } - }; - - 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) { - // datetime - if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') { - // not bar or column - x = new Date(x).getTime(); - } else { - // bar or column - x = index; - } - } else if (typeof x === 'string') { - // string - x = parseFloat(x); - if (isNaN(x)) { - x = index; - } - } - - var yfield = self.model.fields.get(field); - var y = doc.getFieldValue(yfield); - - // horizontal bar chart - if (self.state.attributes.graphType == 'bars') { - points.push([y, x]); - } else { - points.push([x, y]); - } - }); - series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}}); - }); - return series; - } -}); - -my.Flotr2Controls = 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); - diff --git a/test/view.flotr2.test.js b/test/view.flotr2.test.js deleted file mode 100644 index 89edfada..00000000 --- a/test/view.flotr2.test.js +++ /dev/null @@ -1,86 +0,0 @@ -module("View - Flotr2"); - -test('basics', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.Flotr2({ - model: dataset - }); - $('.fixtures').append(view.el); - equal(view.state.get('graphType'), 'lines-and-points'); - // view will auto render ... - assertPresent('.editor', view.elSidebar); - view.remove(); -}); - -test('initialize', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.Flotr2({ - model: dataset, - state: { - 'graphType': 'lines', - 'group': 'x', - 'series': ['y', 'z'] - } - }); - $('.fixtures').append(view.el); - equal(view.state.get('graphType'), 'lines'); - deepEqual(view.state.get('series'), ['y', 'z']); - - // check we have updated editor with state info - equal(view.elSidebar.find('.editor-type select').val(), 'lines'); - equal(view.elSidebar.find('.editor-group select').val(), 'x'); - var out = _.map(view.elSidebar.find('.editor-series select'), function($el) { - return $($el).val(); - }); - deepEqual(out, ['y', 'z']); - - view.remove(); -}); - -test('dates in graph view', function () { - expect(0); - var dataset = Fixture.getDataset(); - var view = new recline.View.Flotr2({ - model: dataset, - state: { - 'graphType': 'lines', - 'group': 'date', - 'series': ['y', 'z'] - } - }); - $('.fixtures').append(view.el); - - view.remove(); -}); - -test('Flotr2Controls basics', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.Flotr2Controls({ - model: dataset, - state: { - graphType: 'bars', - series: [] - } - }); - $('.fixtures').append(view.el); - equal(view.state.get('graphType'), 'bars'); - // view will auto render ... - assertPresent('.editor', view.el); - view.remove(); -}); - -test('Overriding graph options', function () { - var dataset = Fixture.getDataset(); - var randomWidth = Math.random(); - var view = new recline.View.Flotr2({ - model: dataset, - state: { - 'graphType': 'bars', - 'group': 'date', - 'series': ['y', 'z'], - 'graphOptions': { bars: {barWidth: randomWidth}} - } - }); - equal(view.getGraphOptions('bars').bars.barWidth, randomWidth) - view.remove(); -}); From 49daa84ad934ef381ac282ca9d3182257bf109e8 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 5 May 2013 19:04:01 +0100 Subject: [PATCH 43/46] [#314,test][xs]: forgot to commit changes to test index.html. --- test/index.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/index.html b/test/index.html index d728917b..5027e979 100644 --- a/test/index.html +++ b/test/index.html @@ -49,7 +49,6 @@ - @@ -61,7 +60,6 @@ - From aaa865d78b24f3924470dd4811c13ad049879d82 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 5 May 2013 19:07:30 +0100 Subject: [PATCH 44/46] [docs][s]: correct flotr2 refs to flot. --- docs/tutorial-views.markdown | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown index 069862fb..48f460f9 100644 --- a/docs/tutorial-views.markdown +++ b/docs/tutorial-views.markdown @@ -120,13 +120,16 @@ grid.render(); Let's create a graph view to display a line graph for this dataset. First, add the additional dependencies for this view. These are the Flot -library and the Recline Graph view: +library and the Recline Flot Graph view: {% highlight html %} - + - + + {% endhighlight %} From 5e9e7aacba7b8dfe81465543ebc8160ce817a12f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 5 May 2013 20:08:46 +0100 Subject: [PATCH 45/46] [build/docs][m]: build docs (first time in a while). --- docs/index.html | 2 +- docs/src/backend.csv.html | 63 ++-- docs/src/backend.dataproxy.html | 34 ++- docs/src/backend.elasticsearch.html | 34 +-- docs/src/backend.gdocs.html | 37 +-- docs/src/backend.memory.html | 72 ++--- docs/src/ecma-fixes.html | 2 +- docs/src/model.html | 82 ++--- docs/src/view.flot.html | 452 ++++++++++++++++++++++++++++ docs/src/view.graph.html | 403 +------------------------ docs/src/view.grid.html | 6 +- docs/src/view.map.html | 124 +++++--- docs/src/view.multiview.html | 74 +++-- docs/src/view.slickgrid.html | 130 ++++++-- docs/src/view.timeline.html | 24 +- docs/src/widget.facetviewer.html | 2 +- docs/src/widget.fields.html | 23 +- docs/src/widget.filtereditor.html | 8 +- docs/src/widget.pager.html | 5 +- docs/src/widget.queryeditor.html | 2 +- 20 files changed, 861 insertions(+), 718 deletions(-) create mode 100644 docs/src/view.flot.html diff --git a/docs/index.html b/docs/index.html index a76db5e8..94b500d5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -62,7 +62,7 @@ root: ../
  • MultiView View (plus common view code)
  • Grid View (using the excellent Slickgrid)
  • Grid View (no dependencies)
  • -
  • Graph View (based on Flot)
  • +
  • Graph View (based on Flot)
  • Map View (based on Leaflet)
  • Timeline View (using the excellent Verite Timeline)
  • diff --git a/docs/src/backend.csv.html b/docs/src/backend.csv.html index 091b5ace..e8596ea9 100644 --- a/docs/src/backend.csv.html +++ b/docs/src/backend.csv.html @@ -1,13 +1,14 @@ - backend.csv.js

    backend.csv.js

    this.recline = this.recline || {};
    +      backend.csv.js           

    backend.csv.js

    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)

    (function(my, $) {

    fetch

    +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';

    use either jQuery or Underscore Deferred depending on what is available

      var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    fetch

    fetch supports 3 options depending on the attribute provided on the dataset argument

    1. dataset.file: file is an HTML5 file object. This is opened and parsed with the CSV parser.
    2. dataset.data: data is a string in CSV format. This is passed directly to the CSV parser
    3. -
    4. dataset.url: a url to an online CSV file that is ajax accessible (note this usually requires either local or on a server that is CORS enabled). The file is then loaded using $.ajax and parsed using the CSV parser (NB: this requires jQuery)
    5. +
    6. dataset.url: a url to an online CSV file that is ajax accessible (note this usually requires either local or on a server that is CORS enabled). The file is then loaded using jQuery.ajax and parsed using the CSV parser (NB: this requires jQuery)

    All options generates similar data and use the memory store outcome, that is they return something like:

    @@ -19,7 +20,7 @@ useMemoryStore: true }
      my.fetch = function(dataset) {
    -    var dfd = $.Deferred();
    +    var dfd = new Deferred();
         if (dataset.file) {
           var reader = new FileReader();
           var encoding = dataset.encoding || 'UTF-8';
    @@ -44,7 +45,7 @@
             useMemoryStore: true
           });
         } else if (dataset.url) {
    -      $.get(dataset.url).done(function(data) {
    +      jQuery.get(dataset.url).done(function(data) {
             var rows = my.parseCSV(data, dataset);
             dfd.resolve({
               records: rows,
    @@ -53,7 +54,7 @@
           });
         }
         return dfd.promise();
    -  };

    parseCSV

    + };

    parseCSV

    Converts a Comma Separated Values string into an array of arrays. Each line in the CSV becomes an array.

    @@ -74,7 +75,7 @@ Each line in the CSV becomes an array.

    quotechar, or which contain new-line characters. It defaults to '"'

    Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

      my.parseCSV= function(s, options) {

    Get rid of any trailing \n

        s = chomp(s);
    +http://www.uselesscode.org/javascript/csv/

      my.parseCSV= function(s, options) {

    Get rid of any trailing \n

        s = chomp(s);
     
         var options = options || {};
         var trm = (options.trim === false) ? false : true;
    @@ -91,10 +92,10 @@ http://www.uselesscode.org/javascript/csv/

    processField; processField = function (field) { - if (fieldQuoted !== true) {

    If field is empty set to null

            if (field === '') {
    -          field = null;

    If the field was not quoted and we are trimming fields, trim it

            } else if (trm === true) {
    +      if (fieldQuoted !== true) {

    If field is empty set to null

            if (field === '') {
    +          field = null;

    If the field was not quoted and we are trimming fields, trim it

            } else if (trm === true) {
               field = trim(field);
    -        }

    Convert unquoted numbers to their appropriate types

            if (rxIsInt.test(field)) {
    +        }

    Convert unquoted numbers to their appropriate types

            if (rxIsInt.test(field)) {
               field = parseInt(field, 10);
             } else if (rxIsFloat.test(field)) {
               field = parseFloat(field, 10);
    @@ -104,30 +105,30 @@ http://www.uselesscode.org/javascript/csv/

    }; for (i = 0; i < s.length; i += 1) { - cur = s.charAt(i);

    If we are at a EOF or EOR

          if (inQuote === false && (cur === delimiter || cur === "\n")) {
    -	field = processField(field);

    Add the current field to the current row

            row.push(field);

    If this is EOR append row to output and flush row

            if (cur === "\n") {
    +      cur = s.charAt(i);

    If we are at a EOF or EOR

          if (inQuote === false && (cur === delimiter || cur === "\n")) {
    +  field = processField(field);

    Add the current field to the current row

            row.push(field);

    If this is EOR append row to output and flush row

            if (cur === "\n") {
               out.push(row);
               row = [];
    -        }

    Flush the field buffer

            field = '';
    +        }

    Flush the field buffer

            field = '';
             fieldQuoted = false;
    -      } else {

    If it's not a quotechar, add it to the field buffer

            if (cur !== quotechar) {
    +      } else {

    If it's not a quotechar, add it to the field buffer

            if (cur !== quotechar) {
               field += cur;
             } else {
    -          if (!inQuote) {

    We are not in a quote, start a quote

                inQuote = true;
    +          if (!inQuote) {

    We are not in a quote, start a quote

                inQuote = true;
                 fieldQuoted = true;
    -          } else {

    Next char is quotechar, this is an escaped quotechar

                if (s.charAt(i + 1) === quotechar) {
    -              field += quotechar;

    Skip the next char

                  i += 1;
    -            } else {

    It's not escaping, so end quote

                  inQuote = false;
    +          } else {

    Next char is quotechar, this is an escaped quotechar

                if (s.charAt(i + 1) === quotechar) {
    +              field += quotechar;

    Skip the next char

                  i += 1;
    +            } else {

    It's not escaping, so end quote

                  inQuote = false;
                 }
               }
             }
           }
    -    }

    Add the last field

        field = processField(field);
    +    }

    Add the last field

        field = processField(field);
         row.push(field);
         out.push(row);
     
         return out;
    -  };

    serializeCSV

    + };

    serializeCSV

    Convert an Object or a simple array of arrays into a Comma Separated Values string.

    @@ -179,9 +180,9 @@ http://www.uselesscode.org/javascript/csv/

    processField; processField = function (field) { - if (field === null) {

    If field is null set to empty string

            field = '';
    -      } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {

    Convert string to delimited string

            field = quotechar + field + quotechar;
    -      } else if (typeof field === "number") {

    Convert number to string

            field = field.toString(10);
    +      if (field === null) {

    If field is null set to empty string

            field = '';
    +      } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {

    Convert string to delimited string

            field = quotechar + field + quotechar;
    +      } else if (typeof field === "number") {

    Convert number to string

            field = field.toString(10);
           }
     
           return field;
    @@ -191,12 +192,12 @@ http://www.uselesscode.org/javascript/csv/

    cur = a[i]; for (j = 0; j < cur.length; j += 1) { - field = processField(cur[j]);

    If this is EOR append row to output and flush row

            if (j === (cur.length - 1)) {
    +        field = processField(cur[j]);

    If this is EOR append row to output and flush row

            if (j === (cur.length - 1)) {
               row += field;
               out += row + "\n";
               row = '';
    -        } else {

    Add the current field to the current row

              row += field + delimiter;
    -        }

    Flush the field buffer

            field = '';
    +        } else {

    Add the current field to the current row

              row += field + delimiter;
    +        }

    Flush the field buffer

            field = '';
           }
         }
     
    @@ -204,10 +205,10 @@ http://www.uselesscode.org/javascript/csv/

    }; var rxIsInt = /^\d+$/, - rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

    If a string has leading or trailing space, + rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

    If a string has leading or trailing space, contains a comma double quote or a newline it needs to be quoted in CSV output

        rxNeedsQuoting = /^\s|\s$|,|"|\n/,
    -    trim = (function () {

    Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

          if (String.prototype.trim) {
    +    trim = (function () {

    Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

          if (String.prototype.trim) {
             return function (s) {
               return s.trim();
             };
    @@ -219,12 +220,12 @@ it needs to be quoted in CSV output

    }()); function chomp(s) { - if (s.charAt(s.length - 1) !== "\n") {

    Does not end with \n, just return string

          return s;
    -    } else {

    Remove the \n

          return s.substring(0, s.length - 1);
    +    if (s.charAt(s.length - 1) !== "\n") {

    Does not end with \n, just return string

          return s;
    +    } else {

    Remove the \n

          return s.substring(0, s.length - 1);
         }
       }
     
     
    -}(this.recline.Backend.CSV, jQuery));
    +}(this.recline.Backend.CSV));
     
     
    \ No newline at end of file diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html index cc30890c..d52d17db 100644 --- a/docs/src/backend.dataproxy.html +++ b/docs/src/backend.dataproxy.html @@ -1,10 +1,12 @@ - backend.dataproxy.js

    backend.dataproxy.js

    this.recline = this.recline || {};
    +      backend.dataproxy.js           
    'max-results':dataset.size||dataset.rows||1000,type:dataset.format||''}; - varjqxhr=$.ajax({ + varjqxhr=jQuery.ajax({url:my.dataproxy_url,data:data,dataType:'jsonp'}); - vardfd=$.Deferred(); + vardfd=newDeferred();_wrapInTimeout(jqxhr).done(function(results){if(results.error){dfd.reject(results.error); @@ -31,33 +33,33 @@ Needed because use JSONP so do not receive e.g. 500 errors

    useMemoryStore:true});}) - .fail(function(arguments){ - dfd.reject(arguments); + .fail(function(args){ + dfd.reject(args);});returndfd.promise(); - };

    backend.dataproxy.js

    this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
     this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
     
    -(function($, my) {
    -  my.__type__ = 'dataproxy';

    URL for the dataproxy

      my.dataproxy_url = 'http://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;

    load

    +(function(my) { + my.__type__ = 'dataproxy';

    URL for the dataproxy

      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;
    +
    +  

    use either jQuery or Underscore Deferred depending on what is available

      var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    load

    Load data from a URL via the DataProxy.

    @@ -14,12 +16,12 @@ Needed because use JSONP so do not receive e.g. 500 errors

    _wrapInTimeout

    + };

    _wrapInTimeout

    Convenience method providing a crude way to catch backend errors on JSONP calls. Many of backends use JSONP and so will not get error messages and this is a crude way to catch those errors.

      var _wrapInTimeout = function(ourFunction) {
    -    var dfd = $.Deferred();
    +    var dfd = new Deferred();
         var timer = setTimeout(function() {
           dfd.reject({
             message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
           });
         }, my.timeout);
    -    ourFunction.done(function(arguments) {
    +    ourFunction.done(function(args) {
             clearTimeout(timer);
    -        dfd.resolve(arguments);
    +        dfd.resolve(args);
           })
    -      .fail(function(arguments) {
    +      .fail(function(args) {
             clearTimeout(timer);
    -        dfd.reject(arguments);
    +        dfd.reject(args);
           })
           ;
         return dfd.promise();
    -  }
    +  };
     
    -}(jQuery, this.recline.Backend.DataProxy));
    +}(this.recline.Backend.DataProxy));
     
     
    \ No newline at end of file diff --git a/docs/src/backend.elasticsearch.html b/docs/src/backend.elasticsearch.html index 5927a78d..61487458 100644 --- a/docs/src/backend.elasticsearch.html +++ b/docs/src/backend.elasticsearch.html @@ -1,9 +1,9 @@ - backend.elasticsearch.js

    backend.elasticsearch.js

    this.recline = this.recline || {};
    +      backend.elasticsearch.js           

    backend.elasticsearch.js

    this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
     this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
     
     (function($, my) {
    -  my.__type__ = 'elasticsearch';

    ElasticSearch Wrapper

    + my.__type__ = 'elasticsearch';

    use either jQuery or Underscore Deferred depending on what is available

      var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    ElasticSearch Wrapper

    A simple JS wrapper around an ElasticSearch endpoints.

    @@ -23,7 +23,7 @@ on http://localhost:9200 with index twitter and type tweet it would be:

    this.options = _.extend({ dataType: 'json' }, - options);

    mapping

    + options);

    mapping

    Get ES mapping for this type/table

    @@ -34,7 +34,7 @@ on http://localhost:9200 with index twitter and type tweet it would be:

    dataType: this.options.dataType }); return jqxhr; - };

    get

    + };

    get

    Get record corresponding to specified id

    @@ -44,7 +44,7 @@ on http://localhost:9200 with index twitter and type tweet it would be:

    url: base, dataType: 'json' }); - };

    upsert

    + };

    upsert

    create / update a record to ElasticSearch backend

    @@ -61,7 +61,7 @@ on http://localhost:9200 with index twitter and type tweet it would be:

    data: data, dataType: 'json' }); - };

    delete

    + };

    delete

    Delete a record from the ElasticSearch backend.

    @@ -104,7 +104,7 @@ on http://localhost:9200 with index twitter and type tweet it would be:

    }); } return out; - },

    convert from Recline sort structure to ES form + },

    convert from Recline sort structure to ES form http://www.elasticsearch.org/guide/reference/api/search/sort.html

        this._normalizeSort = function(sort) {
           var out = _.map(sort, function(sortObj) {
             var _tmp = {};
    @@ -127,7 +127,7 @@ http://www.elasticsearch.org/guide/reference/api/search/sort.html

    out.geo_distance.unit = filter.unit; } return out; - },

    query

    + },

    query

    @return deferred supporting promise API

        this.query = function(queryObj) {
           var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
    @@ -146,18 +146,18 @@ http://www.elasticsearch.org/guide/reference/api/search/sort.html

    }); return jqxhr; } - };

    Recline Connectors

    + };

    Recline Connectors

    Requires URL of ElasticSearch endpoint to be specified on the dataset -via the url attribute.

    ES options which are passed through to options on Wrapper (see Wrapper for details)

      my.esOptions = {};

    fetch

      my.fetch = function(dataset) {
    +via the url attribute.

    ES options which are passed through to options on Wrapper (see Wrapper for details)

      my.esOptions = {};

    fetch

      my.fetch = function(dataset) {
         var es = new my.Wrapper(dataset.url, my.esOptions);
    -    var dfd = $.Deferred();
    +    var dfd = new Deferred();
         es.mapping().done(function(schema) {
     
           if (!schema){
             dfd.reject({'message':'Elastic Search did not return a mapping'});
             return;
    -      }

    only one top level key in ES = the type so we can ignore it

          var key = _.keys(schema)[0];
    +      }

    only one top level key in ES = the type so we can ignore it

          var key = _.keys(schema)[0];
           var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
             dict.id = fieldName;
             return dict;
    @@ -170,10 +170,10 @@ via the url attribute.

    dfd.reject(arguments); }); return dfd.promise(); - };

    save

      my.save = function(changes, dataset) {
    +  };

    save

      my.save = function(changes, dataset) {
         var es = new my.Wrapper(dataset.url, my.esOptions);
         if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
    -      var dfd = $.Deferred();
    +      var dfd = new Deferred();
           msg = 'Saving more than one item at a time not yet supported';
           alert(msg);
           dfd.reject(msg);
    @@ -187,8 +187,8 @@ via the url attribute.

    } else if (changes.deletes.length > 0) { return es.remove(changes.deletes[0].id); } - };

    query

      my.query = function(queryObj, dataset) {
    -    var dfd = $.Deferred();
    +  };

    query

      my.query = function(queryObj, dataset) {
    +    var dfd = new Deferred();
         var es = new my.Wrapper(dataset.url, my.esOptions);
         var jqxhr = es.query(queryObj);
         jqxhr.done(function(results) {
    @@ -213,7 +213,7 @@ via the url attribute.

    dfd.reject(out); }); return dfd.promise(); - };

    makeRequest

    + };

    makeRequest

    Just $.ajax but in any headers in the 'headers' attribute of this Backend instance. Example:

    diff --git a/docs/src/backend.gdocs.html b/docs/src/backend.gdocs.html index 25c6a7c0..7d07578d 100644 --- a/docs/src/backend.gdocs.html +++ b/docs/src/backend.gdocs.html @@ -1,9 +1,9 @@ - backend.gdocs.js

    backend.gdocs.js

    this.recline = this.recline || {};
    +      backend.gdocs.js           
    results.worksheetTitle=gdocsSpreadsheet.feed.title.$t;returnresults; - };returnurls;}; -}(jQuery,this.recline.Backend.GDocs)); +}(this.recline.Backend.GDocs));

    backend.gdocs.js

    this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
     this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
     
    -(function($, my) {
    -  my.__type__ = 'gdocs';

    Google spreadsheet backend

    +(function(my) { + my.__type__ = 'gdocs';

    use either jQuery or Underscore Deferred depending on what is available

      var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    Google spreadsheet backend

    Fetch data from a Google Docs spreadsheet.

    @@ -29,19 +29,19 @@ var dataset = new recline.Model.Dataset({
  • fields: array of Field objects
  • records: array of objects for each row
  •   my.fetch = function(dataset) {
    -    var dfd  = $.Deferred(); 
    -    var urls = my.getGDocsAPIUrls(dataset.url);

    TODO cover it with tests + var dfd = new Deferred(); + var urls = my.getGDocsAPIUrls(dataset.url);

    TODO cover it with tests get the spreadsheet title

        (function () {
    -      var titleDfd = $.Deferred();
    +      var titleDfd = new Deferred();
     
    -      $.getJSON(urls.spreadsheet, function (d) {
    +      jQuery.getJSON(urls.spreadsheet, function (d) {
               titleDfd.resolve({
                   spreadsheetTitle: d.feed.title.$t
               });
           });
     
           return titleDfd.promise();
    -    }()).then(function (response) {

    get the actual worksheet data

          $.getJSON(urls.worksheet, function(d) {
    +    }()).then(function (response) {

    get the actual worksheet data

          jQuery.getJSON(urls.worksheet, function(d) {
             var result = my.parseData(d);
             var fields = _.map(result.fields, function(fieldId) {
               return {id: fieldId};
    @@ -61,7 +61,7 @@ get the spreadsheet title

    }); return dfd.promise(); - };

    parseData

    + };

    parseData

    Parse data from Google Docs API into a reasonable form

    @@ -79,20 +79,20 @@ colTypes: dictionary (with column names as keys) specifying types (e.g. range, p }; var entries = gdocsSpreadsheet.feed.entry || []; var key; - var colName;

    percentage values (e.g. 23.3%)

        var rep = /^([\d\.\-]+)\%$/;
    +    var colName;

    percentage values (e.g. 23.3%)

        var rep = /^([\d\.\-]+)\%$/;
     
    -    for(key in entries[0]) {

    it's barely possible it has inherited keys starting with 'gsx$'

          if(/^gsx/.test(key)) {
    +    for(key in entries[0]) {

    it's barely possible it has inherited keys starting with 'gsx$'

          if(/^gsx/.test(key)) {
             colName = key.substr(4);
             results.fields.push(colName);
           }
    -    }

    converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

        results.records = _.map(entries, function(entry) {
    +    }

    converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

        results.records = _.map(entries, function(entry) {
           var row = {};
     
           _.each(results.fields, function(col) {
             var _keyname = 'gsx$' + col;
             var value = entry[_keyname].$t;
             var num;
    - 

    TODO cover this part of code with test +

    TODO cover this part of code with test TODO use the regexp only once if labelled as % and value contains %, convert

            if(colTypes[col] === 'percent' && rep.test(value)) {
               num   = rep.exec(value)[1];
    @@ -107,20 +107,23 @@ if labelled as % and value contains %, convert

    Convenience function to get GDocs JSON API Url from standard URL

      my.getGDocsAPIUrls = function(url) {

    https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY

        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*gid=([\d]+).*/;
    +  };

    Convenience function to get GDocs JSON API Url from standard URL

      my.getGDocsAPIUrls = function(url) {

    https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY

        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+)[^#]*(#gid=([\d]+).*)?/;
         var matches = url.match(regex);
         var key;
         var worksheet;
         var urls;
         
         if(!!matches) {
    -        key = matches[1];

    the gid in url is 0-based and feed url is 1-based

            worksheet = parseInt(matches[2]) + 1;
    +        key = matches[1];

    the gid in url is 0-based and feed url is 1-based

            worksheet = parseInt(matches[3]) + 1;
    +        if (isNaN(worksheet)) {
    +          worksheet = 1;
    +        }
             urls = {
               worksheet  : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
               spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
             }
         }
    -    else {

    we assume that it's one of the feeds urls

            key = url.split('/')[5];

    by default then, take first worksheet

            worksheet = 1;
    +    else {

    we assume that it's one of the feeds urls

            key = url.split('/')[5];

    by default then, take first worksheet

            worksheet = 1;
             urls = {
               worksheet  : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
               spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
    @@ -129,6 +132,6 @@ if labelled as % and value contains %, convert

    \ No newline at end of file diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html index 3cdd26fc..5585712e 100644 --- a/docs/src/backend.memory.html +++ b/docs/src/backend.memory.html @@ -1,49 +1,49 @@ - backend.memory.js

    backend.memory.js

    this.recline = this.recline || {};
    +      backend.memory.js           

    backend.memory.js

    this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
     this.recline.Backend.Memory = this.recline.Backend.Memory || {};
     
    -(function($, my) {
    -  my.__type__ = 'memory';

    Data Wrapper

    +(function(my) { + my.__type__ = 'memory';

    private data - use either jQuery or Underscore Deferred depending on what is available

      var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    Data Wrapper

    Turn a simple array of JS objects into a mini data-store with functionality like querying, faceting, updating (by ID) and deleting (by ID).

    -

    @param data list of hashes for each record/row in the data ({key: +

    @param records list of hashes for each record/row in the data ({key: value, key: value}) @param fields (optional) list of field hashes (each hash defining a field as per recline.Model.Field). If fields not specified they will be taken -from the data.

      my.Store = function(data, fields) {
    +from the data.

      my.Store = function(records, fields) {
         var self = this;
    -    this.data = data;
    +    this.records = records;

    backwards compatability (in v0.5 records was named data)

        this.data = this.records;
         if (fields) {
           this.fields = fields;
         } else {
    -      if (data) {
    -        this.fields = _.map(data[0], function(value, key) {
    +      if (records) {
    +        this.fields = _.map(records[0], function(value, key) {
               return {id: key, type: 'string'};
             });
           }
         }
     
         this.update = function(doc) {
    -      _.each(self.data, function(internalDoc, idx) {
    +      _.each(self.records, function(internalDoc, idx) {
             if(doc.id === internalDoc.id) {
    -          self.data[idx] = doc;
    +          self.records[idx] = doc;
             }
           });
         };
     
         this.remove = function(doc) {
    -      var newdocs = _.reject(self.data, function(internalDoc) {
    +      var newdocs = _.reject(self.records, function(internalDoc) {
             return (doc.id === internalDoc.id);
           });
    -      this.data = newdocs;
    +      this.records = newdocs;
         };
     
         this.save = function(changes, dataset) {
           var self = this;
    -      var dfd = $.Deferred();

    TODO _.each(changes.creates) { ... }

          _.each(changes.updates, function(record) {
    +      var dfd = new Deferred();

    TODO _.each(changes.creates) { ... }

          _.each(changes.updates, function(record) {
             self.update(record);
           });
           _.each(changes.deletes, function(record) {
    @@ -54,13 +54,13 @@ from the data.

    }, this.query = function(queryObj) { - var dfd = $.Deferred(); - var numRows = queryObj.size || this.data.length; + var dfd = new Deferred(); + var numRows = queryObj.size || this.records.length; var start = queryObj.from || 0; - var results = this.data; + var results = this.records; results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj);

    TODO: this is not complete sorting! + results = this._applyFreeTextQuery(results, queryObj);

    TODO: this is not complete sorting! What's wrong is we sort on the last entry in the sort list if there are multiple sort criteria

          _.each(queryObj.sort, function(sortObj) {
             var fieldName = sortObj.field;
             results = _.sortBy(results, function(doc) {
    @@ -79,8 +79,8 @@ What's wrong is we sort on the last entry in the sort list if there are
           };
           dfd.resolve(out);
           return dfd.promise();
    -    };

    in place filtering

        this._applyFilters = function(results, queryObj) {
    -      var filters = queryObj.filters;

    register filters

          var filterFunctions = {
    +    };

    in place filtering

        this._applyFilters = function(results, queryObj) {
    +      var filters = queryObj.filters;

    register filters

          var filterFunctions = {
             term         : term,
             range        : range,
             geo_distance : geo_distance
    @@ -88,6 +88,7 @@ What's wrong is we sort on the last entry in the sort list if there are
           var dataParsers = {
             integer: function (e) { return parseFloat(e, 10); },
             'float': function (e) { return parseFloat(e, 10); },
    +        number: function (e) { return parseFloat(e, 10); },
             string : function (e) { return e.toString() },
             date   : function (e) { return new Date(e).valueOf() },
             datetime   : function (e) { return new Date(e).valueOf() }
    @@ -99,11 +100,11 @@ What's wrong is we sort on the last entry in the sort list if there are
           function getDataParser(filter) {
             var fieldType = keyedFields[filter.field].type || 'string';
             return dataParsers[fieldType];
    -      }

    filter records

          return _.filter(results, function (record) {
    +      }

    filter records

          return _.filter(results, function (record) {
             var passes = _.map(filters, function (filter) {
               return filterFunctions[filter.type](record, filter);
    -        });

    return only these records that pass all filters

            return _.all(passes, _.identity);
    -      });

    filters definitions

          function term(record, filter) {
    +        });

    return only these records that pass all filters

            return _.all(passes, _.identity);
    +      });

    filters definitions

          function term(record, filter) {
             var parse = getDataParser(filter);
             var value = parse(record[filter.field]);
             var term  = parse(filter.term);
    @@ -117,15 +118,15 @@ What's wrong is we sort on the last entry in the sort list if there are
             var parse = getDataParser(filter);
             var value = parse(record[filter.field]);
             var start = parse(filter.start);
    -        var stop  = parse(filter.stop);

    if at least one end of range is set do not allow '' to get through + var stop = parse(filter.stop);

    if at least one end of range is set do not allow '' to get through note that for strings '' <= {any-character} e.g. '' <= 'a'

            if ((!startnull || !stopnull) && value === '') {
               return false;
             }
             return ((startnull || value >= start) && (stopnull || value <= stop));
           }
     
    -      function geo_distance() {

    TODO code here

          }
    -    };

    we OR across fields but AND across terms in query string

        this._applyFreeTextQuery = function(results, queryObj) {
    +      function geo_distance() {

    TODO code here

          }
    +    };

    we OR across fields but AND across terms in query string

        this._applyFreeTextQuery = function(results, queryObj) {
           if (queryObj.q) {
             var terms = queryObj.q.split(' ');
             var patterns=_.map(terms, function(term) {
    @@ -139,10 +140,10 @@ note that for strings '' <= {any-character} e.g. '' <= 'a'

    var value = rawdoc[field.id]; if ((value !== null) && (value !== undefined)) { value = value.toString(); - } else {

    value can be null (apparently in some cases)

                    value = '';
    -              }

    TODO regexes?

                  foundmatch = foundmatch || (pattern.test(value.toLowerCase()));

    TODO: early out (once we are true should break to spare unnecessary testing) + } else {

    value can be null (apparently in some cases)

                    value = '';
    +              }

    TODO regexes?

                  foundmatch = foundmatch || (pattern.test(value.toLowerCase()));

    TODO: early out (once we are true should break to spare unnecessary testing) if (foundmatch) return true;

                });
    -            matches = matches && foundmatch;

    TODO: early out (once false should break to spare unnecessary testing) + matches = matches && foundmatch;

    TODO: early out (once false should break to spare unnecessary testing) if (!matches) return false;

              });
               return matches;
             });
    @@ -155,9 +156,9 @@ if (!matches) return false;

    if (!queryObj.facets) { return facetResults; } - _.each(queryObj.facets, function(query, facetId) {

    TODO: remove dependency on recline.Model

            facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
    +      _.each(queryObj.facets, function(query, facetId) {

    TODO: remove dependency on recline.Model

            facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
             facetResults[facetId].termsall = {};
    -      });

    faceting

          _.each(records, function(doc) {
    +      });

    faceting

          _.each(records, function(doc) {
             _.each(queryObj.facets, function(query, facetId) {
               var fieldId = query.terms.field;
               var val = doc[fieldId];
    @@ -174,21 +175,14 @@ if (!matches) return false;

    var terms = _.map(tmp.termsall, function(count, term) { return { term: term, count: count }; }); - tmp.terms = _.sortBy(terms, function(item) {

    want descending order

              return -item.count;
    +        tmp.terms = _.sortBy(terms, function(item) {

    want descending order

              return -item.count;
             });
             tmp.terms = tmp.terms.slice(0, 10);
           });
           return facetResults;
         };
    -
    -    this.transform = function(editFunc) {
    -      var toUpdate = recline.Data.Transform.mapDocs(this.data, editFunc);

    TODO: very inefficient -- could probably just walk the documents and updates in tandem and update

          _.each(toUpdate.updates, function(record, idx) {
    -        self.data[idx] = record;
    -      });
    -      return this.save(toUpdate);
    -    };
       };
     
    -}(jQuery, this.recline.Backend.Memory));
    +}(this.recline.Backend.Memory));
     
     
    \ No newline at end of file diff --git a/docs/src/ecma-fixes.html b/docs/src/ecma-fixes.html index 212ecf5d..3df0df09 100644 --- a/docs/src/ecma-fixes.html +++ b/docs/src/ecma-fixes.html @@ -1,4 +1,4 @@ - ecma-fixes.js

    ecma-fixes.js

    This file adds in full array method support in browsers that don't support it + ecma-fixes.js

    ecma-fixes.js

    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

    Add ECMA262-5 Array methods if not supported natively

    if (!('indexOf' in Array.prototype)) {
         Array.prototype.indexOf= function(find, i /*opt*/) {
             if (i===undefined) i= 0;
    diff --git a/docs/src/model.html b/docs/src/model.html
    index a177a460..80f8bc8f 100644
    --- a/docs/src/model.html
    +++ b/docs/src/model.html
    @@ -1,10 +1,10 @@
    -      model.js           

    model.js

    Recline Backbone Models

    this.recline = this.recline || {};
    +      model.js           
    },save:function(){ - varself=this;

    model.js

    Recline Backbone Models

    this.recline = this.recline || {};
     this.recline.Model = this.recline.Model || {};
     
    -(function($, my) {

    Dataset

    my.Dataset = Backbone.Model.extend({
    +(function(my) {

    use either jQuery or Underscore Deferred depending on what is available

    var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

    Dataset

    my.Dataset = Backbone.Model.extend({
       constructor: function Dataset() {
         Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },

    initialize

      initialize: function() {
    +  },

    initialize

      initialize: function() {
         _.bindAll(this, 'query');
         this.backend = null;
         if (this.get('backend')) {
    @@ -25,25 +25,25 @@
         this.recordCount = null;
         this.queryState = new my.Query();
         this.queryState.bind('change', this.query);
    -    this.queryState.bind('facet:add', this.query);

    store is what we query and save against + this.queryState.bind('facet:add', this.query);

    store is what we query and save against store will either be the backend or be a memory store if Backend fetch tells us to use memory store

        this._store = this.backend;
         if (this.backend == recline.Backend.Memory) {
           this.fetch();
         }
    -  },

    fetch

    + },

    fetch

    Retrieve dataset and (some) records from the backend.

      fetch: function() {
         var self = this;
    -    var dfd = $.Deferred();
    +    var dfd = new Deferred();
     
         if (this.backend !== recline.Backend.Memory) {
           this.backend.fetch(this.toJSON())
             .done(handleResults)
    -        .fail(function(arguments) {
    -          dfd.reject(arguments);
    +        .fail(function(args) {
    +          dfd.reject(args);
             });
    -    } else {

    special case where we have been given data directly

          handleResults({
    +    } else {

    special case where we have been given data directly

          handleResults({
             records: this.get('records'),
             fields: this.get('fields'),
             useMemoryStore: true
    @@ -62,18 +62,18 @@ tells us to use memory store

    .done(function() { dfd.resolve(self); }) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); } return dfd.promise(); - },

    _normalizeRecordsAndFields

    + },

    _normalizeRecordsAndFields

    Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects

    e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] => -fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]

      _normalizeRecordsAndFields: function(records, fields) {

    if no fields get them from records

        if (!fields && records && records.length > 0) {

    records is array then fields is first row of records ...

          if (records[0] instanceof Array) {
    +fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]

      _normalizeRecordsAndFields: function(records, fields) {

    if no fields get them from records

        if (!fields && records && records.length > 0) {

    records is array then fields is first row of records ...

          if (records[0] instanceof Array) {
             fields = records[0];
             records = records.slice(1);
           } else {
    @@ -81,9 +81,14 @@ fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]

    return {id: key}; }); } - }

    fields is an array of strings (i.e. list of field headings/ids)

        if (fields && fields.length > 0 && typeof fields[0] === 'string') {

    Rename duplicate fieldIds as each field name needs to be + }

    fields is an array of strings (i.e. list of field headings/ids)

        if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) {

    Rename duplicate fieldIds as each field name needs to be unique.

          var seen = {};
    -      fields = _.map(fields, function(field, index) {

    cannot use trim as not supported by IE7

            var fieldId = field.replace(/^\s+|\s+$/g, '');
    +      fields = _.map(fields, function(field, index) {
    +        if (field === null) {
    +          field = '';
    +        } else {
    +          field = field.toString();
    +        }

    cannot use trim as not supported by IE7

            var fieldId = field.replace(/^\s+|\s+$/g, '');
             if (fieldId === '') {
               fieldId = '_noname_';
               field = fieldId;
    @@ -94,10 +99,10 @@ unique.

    } if (!(field in seen)) { seen[field] = 0; - }

    TODO: decide whether to keep original name as label ... + }

    TODO: decide whether to keep original name as label ... return { id: fieldId, label: field || fieldId }

            return { id: fieldId };
           });
    -    }

    records is provided as arrays so need to zip together with fields + }

    records is provided as arrays so need to zip together with fields NB: this requires you to have fields to match arrays

        if (records && records.length > 0 && records[0] instanceof Array) {
           records = _.map(records, function(doc) {
             var tmp = {};
    @@ -114,19 +119,7 @@ NB: this requires you to have fields to match arrays

    TODO: need to reset the changes ...

        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"});
    -    });
    +    var self = this;

    TODO: need to reset the changes ...

        return this._store.save(this._changes, this.toJSON());
       },

    query

    AJAX method with promise API to get records from the backend.

    @@ -137,7 +130,7 @@ updated by queryObj (if provided).

    Resulting RecordList are used to reset this.records and are also returned.

      query: function(queryObj) {
         var self = this;
    -    var dfd = $.Deferred();
    +    var dfd = new Deferred();
         this.trigger('query:start');
     
         if (queryObj) {
    @@ -151,9 +144,9 @@ also returned.

    self.trigger('query:done'); dfd.resolve(self.records); }) - .fail(function(arguments) { - self.trigger('query:fail', arguments); - dfd.reject(arguments); + .fail(function(args) { + self.trigger('query:fail', args); + dfd.reject(args); }); return dfd.promise(); }, @@ -198,7 +191,7 @@ also returned.

    this.fields.each(function(field) { query.addFacet(field.id); }); - var dfd = $.Deferred(); + var dfd = new Deferred(); this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { if (queryResult.facets) { _.each(queryResult.facets, function(facetResult, facetId) { @@ -241,16 +234,23 @@ Dataset e.g. in query method

    },

    getFieldValue

    For the provided Field get the corresponding rendered computed data value -for this record.

      getFieldValue: function(field) {
    +for this record.

    + +

    NB: if field is undefined a default '' value will be returned

      getFieldValue: function(field) {
         val = this.getFieldValueUnrendered(field);
    -    if (field.renderer) {
    +    if (field && !_.isUndefined(field.renderer)) {
           val = field.renderer(val, field, this.toJSON());
         }
         return val;
       },

    getFieldValueUnrendered

    For the provided Field get the corresponding computed data value -for this record.

      getFieldValueUnrendered: function(field) {
    +for this record.

    + +

    NB: if field is undefined a default '' value will be returned

      getFieldValueUnrendered: function(field) {
    +    if (!field) {
    +      return '';
    +    }
         var val = this.get(field.id);
         if (field.deriver) {
           val = field.deriver(val, field, this);
    @@ -350,7 +350,7 @@ WARNING: these will not persist unless you call save on Dataset

    here that are not actually strings

            if (val && typeof val === 'string') {
               val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
             }
    -        return val
    +        return val;
           }
         }
       }
    @@ -398,7 +398,7 @@ here that are not actually strings

      addFilter: function(filter) {

    crude deep copy

        var ourfilter = JSON.parse(JSON.stringify(filter));

    not fully specified so use template and over-write

        if (_.keys(filter).length <= 3) {
    -      ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
    +      ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]);
         }
         var filters = this.get('filters');
         filters.push(ourfilter);
    @@ -464,6 +464,6 @@ here that are not actually strings

    return model.backend.sync(method, model, options); }; -}(jQuery, this.recline.Model)); +}(this.recline.Model));
    \ No newline at end of file diff --git a/docs/src/view.flot.html b/docs/src/view.flot.html new file mode 100644 index 00000000..1ed374bd --- /dev/null +++ b/docs/src/view.flot.html @@ -0,0 +1,452 @@ + view.flot.js

    view.flot.js

    /*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: ' \
    +    <div class="recline-flot"> \
    +      <div class="panel graph" style="display: block;"> \
    +        <div class="js-temp-notice alert alert-block"> \
    +          <h3 class="alert-heading">Hey there!</h3> \
    +          <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
    +          <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
    +        </div> \
    +      </div> \
    +    </div> \
    +',
    +
    +  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;
    +        }
    +
    +        $('<div id="recline-flot-tooltip">' + content + '</div>').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<numTicks; i++) {
    +        ticks.push(parseInt(i*increment, 10));
    +      }
    +      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;
    +    self.xvaluesAreIndex = false;
    +    var series = [];
    +    _.each(this.state.attributes.series, function(field) {
    +      var points = [];
    +      var fieldLabel = self.model.fields.get(field).get('label');
    +      _.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) {
    +          self.xvaluesAreIndex = true;
    +          x = index;
    +        } else if (typeof x === 'string') {
    +          x = parseFloat(x);
    +          if (isNaN(x)) { // assume this is a string label
    +            x = index;
    +            self.xvaluesAreIndex = true;
    +          }
    +        }
    +
    +        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: fieldLabel,
    +        hoverable: true
    +      });
    +    });
    +    return series;
    +  }
    +});
    +
    +my.FlotControls = Backbone.View.extend({
    +  className: "editor",
    +  template: ' \
    +  <div class="editor"> \
    +    <form class="form-stacked"> \
    +      <div class="clearfix"> \
    +        <label>Graph Type</label> \
    +        <div class="input editor-type"> \
    +          <select> \
    +          <option value="lines-and-points">Lines and Points</option> \
    +          <option value="lines">Lines</option> \
    +          <option value="points">Points</option> \
    +          <option value="bars">Bars</option> \
    +          <option value="columns">Columns</option> \
    +          </select> \
    +        </div> \
    +        <label>Group Column (Axis 1)</label> \
    +        <div class="input editor-group"> \
    +          <select> \
    +          <option value="">Please choose ...</option> \
    +          {{#fields}} \
    +          <option value="{{id}}">{{label}}</option> \
    +          {{/fields}} \
    +          </select> \
    +        </div> \
    +        <div class="editor-series-group"> \
    +        </div> \
    +      </div> \
    +      <div class="editor-buttons"> \
    +        <button class="btn editor-add">Add Series</button> \
    +      </div> \
    +      <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
    +        <button class="editor-save">Save</button> \
    +        <input type="hidden" class="editor-id" value="chart-1" /> \
    +      </div> \
    +    </form> \
    +  </div> \
    +',
    +  templateSeriesEditor: ' \
    +    <div class="editor-series js-series-{{seriesIndex}}"> \
    +      <label>Series <span>{{seriesName}} (Axis 2)</span> \
    +        [<a href="#remove" class="action-remove-series">Remove</a>] \
    +      </label> \
    +      <div class="input"> \
    +        <select> \
    +        {{#fields}} \
    +        <option value="{{id}}">{{label}}</option> \
    +        {{/fields}} \
    +        </select> \
    +      </div> \
    +    </div> \
    +  ',
    +  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);
    +
    +
    \ No newline at end of file diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html index fd87f132..4088506a 100644 --- a/docs/src/view.graph.html +++ b/docs/src/view.graph.html @@ -1,403 +1,6 @@ - view.graph.js

    view.graph.js

    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    +      view.graph.js           

    view.graph.js

    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' - }

    • -
    - -

    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.Graph = Backbone.View.extend({
    -  template: ' \
    -    <div class="recline-graph"> \
    -      <div class="panel graph" style="display: block;"> \
    -        <div class="js-temp-notice alert alert-block"> \
    -          <h3 class="alert-heading">Hey there!</h3> \
    -          <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
    -          <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
    -        </div> \
    -      </div> \
    -    </div> \
    -',
    -
    -  initialize: function(options) {
    -    var self = this;
    -    this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
    -
    -    this.el = $(this.el);
    -    _.bindAll(this, 'render', 'redraw');
    -    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.editor = new my.GraphControls({
    -      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');
    -    return this;
    -  },
    -
    -  redraw: function() {

    There appear to be 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() - 20);
    -      var series = this.createSeries();
    -      var options = this.getGraphOptions(this.state.attributes.graphType);
    -      this.plot = Flotr.draw(this.$graph.get(0), series, options);
    -    }
    -  },
    -
    -  show: function() {

    because we cannot redraw when hidden we may need to when becoming visible

        if (this.needToRedraw) {
    -      this.redraw();
    -    }
    -  },

    getGraphOptions

    - -

    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) { 
    -    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;
    -      }
    -      
    -      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;
    -    };
    -    
    -    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');
    -
    -      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();
    -      }
    -      return x;    
    -    }
    -    
    -    var xaxis = {};
    -    xaxis.tickFormatter = tickFormatter;
    -
    -    var yaxis = {};
    -    yaxis.autoscale = true;
    -    yaxis.autoscaleMargin = 0.02;
    -    
    -    var mouse = {};
    -    mouse.track = true;
    -    mouse.relative = true;
    -    mouse.trackFormatter = trackFormatter;
    -    
    -    var legend = {};
    -    legend.position = 'ne';
    -    

    mouse.lineColor is set in createSeries

        var optionsPerGraphType = { 
    -      lines: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: true },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        mouse: mouse
    -      },
    -      points: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        points: { show: true, hitRadius: 5 },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        mouse: mouse,
    -        grid: { hoverable: true, clickable: true }
    -      },
    -      'lines-and-points': {
    -        legend: legend,
    -        colors: this.graphColors,
    -        points: { show: true, hitRadius: 5 },
    -        lines: { show: true },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        mouse: mouse,
    -        grid: { hoverable: true, clickable: true }
    -      },
    -      bars: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: false },
    -        xaxis: yaxis,
    -        yaxis: xaxis,
    -        mouse: { 
    -          track: true,
    -          relative: true,
    -          trackFormatter: trackFormatter,
    -          fillColor: '#FFFFFF',
    -          fillOpacity: 0.3,
    -          position: 'e'
    -        },
    -        bars: {
    -          show: true,
    -          horizontal: true,
    -          shadowSize: 0,
    -          barWidth: 0.8         
    -        }
    -      },
    -      columns: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: false },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        mouse: { 
    -            track: true,
    -            relative: true,
    -            trackFormatter: trackFormatter,
    -            fillColor: '#FFFFFF',
    -            fillOpacity: 0.3,
    -            position: 'n'
    -        },
    -        bars: {
    -            show: true,
    -            horizontal: false,
    -            shadowSize: 0,
    -            barWidth: 0.8         
    -        }
    -      },
    -      grid: { hoverable: true, clickable: true }
    -    };
    -    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) {

    datetime

              if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') {

    not bar or column

                x = new Date(x).getTime();
    -          } else {

    bar or column

                x = index;
    -          }
    -        } else if (typeof x === 'string') {

    string

              x = parseFloat(x);
    -          if (isNaN(x)) {
    -            x = index;
    -          }
    -        }
    -
    -        var yfield = self.model.fields.get(field);
    -        var y = doc.getFieldValue(yfield);
    -        

    horizontal bar chart

            if (self.state.attributes.graphType == 'bars') {
    -          points.push([y, x]);
    -        } else {
    -          points.push([x, y]);
    -        }
    -      });
    -      series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}});
    -    });
    -    return series;
    -  }
    -});
    -
    -my.GraphControls = Backbone.View.extend({
    -  className: "editor",
    -  template: ' \
    -  <div class="editor"> \
    -    <form class="form-stacked"> \
    -      <div class="clearfix"> \
    -        <label>Graph Type</label> \
    -        <div class="input editor-type"> \
    -          <select> \
    -          <option value="lines-and-points">Lines and Points</option> \
    -          <option value="lines">Lines</option> \
    -          <option value="points">Points</option> \
    -          <option value="bars">Bars</option> \
    -          <option value="columns">Columns</option> \
    -          </select> \
    -        </div> \
    -        <label>Group Column (x-axis)</label> \
    -        <div class="input editor-group"> \
    -          <select> \
    -          <option value="">Please choose ...</option> \
    -          {{#fields}} \
    -          <option value="{{id}}">{{label}}</option> \
    -          {{/fields}} \
    -          </select> \
    -        </div> \
    -        <div class="editor-series-group"> \
    -        </div> \
    -      </div> \
    -      <div class="editor-buttons"> \
    -        <button class="btn editor-add">Add Series</button> \
    -      </div> \
    -      <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
    -        <button class="editor-save">Save</button> \
    -        <input type="hidden" class="editor-id" value="chart-1" /> \
    -      </div> \
    -    </form> \
    -  </div> \
    -',
    -  templateSeriesEditor: ' \
    -    <div class="editor-series js-series-{{seriesIndex}}"> \
    -      <label>Series <span>{{seriesName}} (y-axis)</span> \
    -        [<a href="#remove" class="action-remove-series">Remove</a>] \
    -      </label> \
    -      <div class="input"> \
    -        <select> \
    -        {{#fields}} \
    -        <option value="{{id}}">{{label}}</option> \
    -        {{/fields}} \
    -        </select> \
    -      </div> \
    -    </div> \
    -  ',
    -  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);
    +this.recline.View.Graph = this.recline.View.Flot;
    +this.recline.View.GraphControls = this.recline.View.FlotControls;
     
     
    \ No newline at end of file diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html index 13efa9b7..fc0d31e4 100644 --- a/docs/src/view.grid.html +++ b/docs/src/view.grid.html @@ -1,4 +1,4 @@ - view.grid.js

    view.grid.js

    /*jshint multistr:true */
    +      view.grid.js           

    view.grid.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -85,8 +85,8 @@ Column and row menus

    }); this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions var numFields = this.fields.length;

    compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar)

        var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
    -    var width = parseInt(Math.max(50, fullWidth / numFields));

    if columns extend outside viewport then remainder is 0

        var remainder = Math.max(fullWidth - numFields * width,0);
    -    _.each(this.fields, function(field, idx) {

    add the remainder to the first field width so we make up full col

          if (idx == 0) {
    +    var width = parseInt(Math.max(50, fullWidth / numFields), 10);

    if columns extend outside viewport then remainder is 0

        var remainder = Math.max(fullWidth - numFields * width,0);
    +    _.each(this.fields, function(field, idx) {

    add the remainder to the first field width so we make up full col

          if (idx === 0) {
             field.set({width: width+remainder});
           } else {
             field.set({width: width});
    diff --git a/docs/src/view.map.html b/docs/src/view.map.html
    index d9f49d17..c37709b7 100644
    --- a/docs/src/view.map.html
    +++ b/docs/src/view.map.html
    @@ -1,4 +1,4 @@
    -      view.map.js           

    view.map.js

    /*jshint multistr:true */
    +      view.map.js           
    varbg=newL.TileLayer(mapUrl,{maxZoom:18,attribution:osmAttribution,subdomains:'1234'});this.map.addLayer(bg); - this.markers=newL.MarkerClusterGroup(this._clusterOptions); - - this.features=newL.GeoJSON(null,{ - pointToLayer:function(feature,latlng){ - varmarker=newL.marker(latlng); - marker.bindPopup(feature.properties.popupContent); - self.markers.addLayer(marker); - returnmarker; - } - }); + this.markers=newL.MarkerClusterGroup(this._clusterOptions); <input type="hidden" class="editor-id" value="map-1" /> \ </div> \ </form> \ - ',this.state=newrecline.Model.ObjectState(options.state);this.state.bind('change',this.render);this.render(); - },_geomReady:function(){returnBoolean(this.state.get('geomField')||(this.state.get('latField')&&this.state.get('lonField'))); - },

    view.map.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -6,9 +6,16 @@
     (function($, my) {

    Map view for a Dataset using Leaflet mapping library.

    This view allows to plot gereferenced records on a map. The location -information can be provided either via a field with -GeoJSON objects or two fields with latitude and -longitude coordinates.

    +information can be provided in 2 ways:

    + +
      +
    1. Via a single field. This field must be either a geo_point or +GeoJSON object
    2. +
    3. Via two fields with latitude and longitude coordinates.
    4. +
    + +

    Which fields in the data these correspond to can be configured via the state +(and are guessed if no info is provided).

    Initialization arguments are as standard for Dataset Views. State object may have the following (optional) configuration options:

    @@ -19,6 +26,9 @@ have the following (optional) configuration options:

    geomField: {id of field containing geometry in the dataset} lonField: {id of field containing longitude in the dataset} latField: {id of field containing latitude in the dataset} + autoZoom: true, + // use cluster support + cluster: false } @@ -103,7 +113,32 @@ view.infobox = function(record) { } } return html; - },

    END: Customization section

    Public: Adds the necessary elements to the page.

    + },

    Options to use for the Leaflet GeoJSON layer +See also http://leaflet.cloudmade.com/examples/geojson.html

    + +

    e.g.

    + +
    pointToLayer: function(feature, latLng)
    +onEachFeature: function(feature, layer)
    +
    + +

    See defaults for examples

      geoJsonLayerOptions: {

    pointToLayer function to use when creating points

    + +

    Default behaviour shown here is to create a marker using the +popupContent set on the feature properties (created via infobox function +during feature generation)

    + +

    NB: inside pointToLayer this will be set to point to this map view +instance (which allows e.g. this.markers to work in this default case)

        pointToLayer: function (feature, latlng) {
    +      var marker = new L.Marker(latlng);
    +      marker.bindPopup(feature.properties.popupContent);

    this is for cluster case

          this.markers.addLayer(marker);
    +      return marker;
    +    },

    onEachFeature default which adds popup in

        onEachFeature: function(feature, layer) {
    +      if (feature.properties && feature.properties.popupContent) {
    +        layer.bindPopup(feature.properties.popupContent);
    +      }
    +    }
    +  },

    END: Customization section

    Public: Adds the necessary elements to the page.

    Also sets up the editor fields and the map if necessary.

      render: function() {
         var self = this;
    @@ -113,7 +148,7 @@ view.infobox = function(record) {
         this.$map = this.el.find('.panel.map');
         this.redraw();
         return this;
    -  },

    Public: Redraws the features on the map according to the action provided

    + },

    Public: Redraws the features on the map according to the action provided

    Actions can be:

    @@ -124,33 +159,39 @@ view.infobox = function(record) {
  • refresh: Clear existing features and add all current records
  •   redraw: function(action, doc){
         var self = this;
    -    action = action || 'refresh';

    try to set things up if not already

        if (!self._geomReady()){
    +    action = action || 'refresh';

    try to set things up if not already

        if (!self._geomReady()){
           self._setupGeometryField();
         }
         if (!self.mapReady){
           self._setupMap();
         }
     
    -    if (this._geomReady() && this.mapReady){

    removing ad re-adding the layer enables faster bulk loading

          this.map.removeLayer(this.features);
    +    if (this._geomReady() && this.mapReady){

    removing ad re-adding the layer enables faster bulk loading

          this.map.removeLayer(this.features);
           this.map.removeLayer(this.markers);
     
           var countBefore = 0;
           this.features.eachLayer(function(){countBefore++;});
     
           if (action == 'refresh' || action == 'reset') {
    -        this.features.clearLayers();

    recreate cluster group because of issues with clearLayer

            this.map.removeLayer(this.markers);
    +        this.features.clearLayers();

    recreate cluster group because of issues with clearLayer

            this.map.removeLayer(this.markers);
             this.markers = new L.MarkerClusterGroup(this._clusterOptions);
             this._add(this.model.records.models);
           } else if (action == 'add' && doc){
             this._add(doc);
           } else if (action == 'remove' && doc){
             this._remove(doc);
    -      }

    enable clustering if there is a large number of markers

          var countAfter = 0;
    +      }

    enable clustering if there is a large number of markers

          var countAfter = 0;
           this.features.eachLayer(function(){countAfter++;});
           var sizeIncreased = countAfter - countBefore > 0;
           if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) {
             this.state.set({cluster: true});
             return;
    +      }

    this must come before zooming! +if not: errors when using e.g. circle markers like +"Cannot call method 'project' of undefined"

          if (this.state.get('cluster')) {
    +        this.map.addLayer(this.markers);
    +      } else {
    +        this.map.addLayer(this.features);
           }
     
           if (this.state.get('autoZoom')){
    @@ -160,15 +201,10 @@ view.infobox = function(record) {
               this._zoomPending = true;
             }
           }
    -      if (this.state.get('cluster')) {
    -        this.map.addLayer(this.markers);
    -      } else {
    -        this.map.addLayer(this.features);
    -      }
         }
       },
     
    -  show: function() {

    If the div was hidden, Leaflet needs to recalculate some sizes + show: function() {

    If the div was hidden, Leaflet needs to recalculate some sizes to display properly

        if (this.map){
           this.map.invalidateSize();
           if (this._zoomPending && this.state.get('autoZoom')) {
    @@ -185,7 +221,7 @@ to display properly

    _geomReady: function() { return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - },

    Private: Add one or n features to the map

    + },

    Private: Add one or n features to the map

    For each record passed, a GeoJSON geometry will be extracted and added to the features layer. If an exception is thrown, the process will be @@ -201,10 +237,10 @@ stopped and an error notification shown.

    _.every(docs, function(doc){ count += 1; var feature = self._getGeometryFromRecord(doc); - if (typeof feature === 'undefined' || feature === null){

    Empty field

            return true;
    +      if (typeof feature === 'undefined' || feature === null){

    Empty field

            return true;
           } else if (feature instanceof Object){
             feature.properties = {
    -          popupContent: self.infobox(doc),

    Add a reference to the model id, which will allow us to + popupContent: self.infobox(doc),

    Add a reference to the model id, which will allow us to link this Leaflet layer to a Recline doc

              cid: doc.cid
             };
     
    @@ -226,7 +262,7 @@ link this Leaflet layer to a Recline doc

    } return true; }); - },

    Private: Remove one or n features from the map

      _remove: function(docs){
    +  },

    Private: Remove one or n features from the map

      _remove: function(docs){
     
         var self = this;
     
    @@ -240,10 +276,10 @@ link this Leaflet layer to a Recline doc

    } }); - },

    Private: Return a GeoJSON geomtry extracted from the record fields

      _getGeometryFromRecord: function(doc){
    +  },

    Private: Return a GeoJSON geomtry extracted from the record fields

      _getGeometryFromRecord: function(doc){
         if (this.state.get('geomField')){
           var value = doc.get(this.state.get('geomField'));
    -      if (typeof(value) === 'string'){

    We may have a GeoJSON string representation

            try {
    +      if (typeof(value) === 'string'){

    We may have a GeoJSON string representation

            try {
               value = $.parseJSON(value);
             } catch(e) {}
           }
    @@ -261,16 +297,16 @@ link this Leaflet layer to a Recline doc

    } else { return null; } - } else if (value && value.slice) {

    [ lon, lat ]

            return {
    +      } else if (value && _.isArray(value)) {

    [ lon, lat ]

            return {
               "type": "Point",
               "coordinates": [value[0], value[1]]
             };
    -      } else if (value && value.lat) {

    of form { lat: ..., lon: ...}

            return {
    +      } else if (value && value.lat) {

    of form { lat: ..., lon: ...}

            return {
               "type": "Point",
               "coordinates": [value.lon || value.lng, value.lat]
             };
    -      }

    We o/w assume that contents of the field are a valid GeoJSON object

          return value;
    -    } else if (this.state.get('lonField') && this.state.get('latField')){

    We'll create a GeoJSON like point object from the two lat/lon fields

          var lon = doc.get(this.state.get('lonField'));
    +      }

    We o/w assume that contents of the field are a valid GeoJSON object

          return value;
    +    } else if (this.state.get('lonField') && this.state.get('latField')){

    We'll create a GeoJSON like point object from the two lat/lon fields

          var lon = doc.get(this.state.get('lonField'));
           var lat = doc.get(this.state.get('latField'));
           if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
             return {
    @@ -280,10 +316,10 @@ link this Leaflet layer to a Recline doc

    } } return null; - },

    Private: Check if there is a field with GeoJSON geometries or alternatively, + },

    Private: Check if there is a field with GeoJSON geometries or alternatively, two fields with lat/lon values.

    -

    If not found, the user can define them via the UI form.

      _setupGeometryField: function(){

    should not overwrite if we have already set this (e.g. explicitly via state)

        if (!this._geomReady()) {
    +

    If not found, the user can define them via the UI form.

      _setupGeometryField: function(){

    should not overwrite if we have already set this (e.g. explicitly via state)

        if (!this._geomReady()) {
           this.state.set({
             geomField: this._checkField(this.geometryFieldNames),
             latField: this._checkField(this.latitudeFieldNames),
    @@ -291,7 +327,7 @@ two fields with lat/lon values.

    }); this.menu.state.set(this.state.toJSON()); } - },

    Private: Check if a field in the current model exists in the provided + },

    Private: Check if a field in the current model exists in the provided list of names.

      _checkField: function(fieldNames){
         var field;
         var modelFieldNames = this.model.fields.pluck('id');
    @@ -302,7 +338,7 @@ list of names.

    } } return null; - },

    Private: Zoom to map to current features extent if any, or to the full + },

    Private: Zoom to map to current features extent if any, or to the full extent if none.

      _zoomToFeatures: function(){
         var bounds = this.features.getBounds();
         if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
    @@ -310,7 +346,7 @@ extent if none.

    } else { this.map.setView([0, 0], 2); } - },

    Private: Sets up the Leaflet map control and the features layer.

    + },

    Private: Sets up the Leaflet map control and the features layer.

    The map uses a base layer from MapQuest based on OpenStreetMap.

      _setupMap: function(){
    @@ -322,21 +358,15 @@ on OpenStreetMap.

    rebind this (as needed in e.g. default case above)

        this.geoJsonLayerOptions.pointToLayer =  _.bind(
    +        this.geoJsonLayerOptions.pointToLayer,
    +        this);
    +    this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
     
         this.map.setView([0, 0], 2);
     
         this.mapReady = true;
    -  },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
         var options = $('.' + id + ' > select > option');
         if (options){
           options.each(function(opt){
    @@ -409,7 +439,7 @@ on OpenStreetMap.

    Define here events for UI elements

      events: {
    +  ',

    Define here events for UI elements

      events: {
         'click .editor-update-map': 'onEditorSubmit',
         'change .editor-field-type': 'onFieldTypeChange',
         'click #editor-auto-zoom': 'onAutoZoomChange',
    @@ -424,7 +454,7 @@ on OpenStreetMap.

    Public: Adds the necessary elements to the page.

    + },

    Public: Adds the necessary elements to the page.

    Also sets up the editor fields and the map if necessary.

      render: function() {
         var self = this;
    @@ -456,7 +486,7 @@ on OpenStreetMap.

    UI Event handlers

    Public: Update map with user options

    + },

    UI Event handlers

    Public: Update map with user options

    Right now the only configurable option is what field(s) contains the location information.

      onEditorSubmit: function(e){
    @@ -475,7 +505,7 @@ location information.

    }); } return false; - },

    Public: Shows the relevant select lists depending on the location field + },

    Public: Shows the relevant select lists depending on the location field type selected.

      onFieldTypeChange: function(e){
         if (e.target.value == 'geom'){
             this.el.find('.editor-field-type-geom').show();
    @@ -492,7 +522,7 @@ type selected.

    onClusteringChange: function(e){ this.state.set({cluster: !this.state.get('cluster')}); - },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    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){
    diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
    index d1f2f383..0bba6c7f 100644
    --- a/docs/src/view.multiview.html
    +++ b/docs/src/view.multiview.html
    @@ -1,4 +1,4 @@
    -      view.multiview.js           

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

    this.recline = this.recline || {};
    +      view.multiview.js           

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

    this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
     
     (function($, my) {

    MultiView

    @@ -107,7 +107,7 @@ initialized the MultiView with the relevant views themselves.

    <div class="menu-right"> \ <div class="btn-group" data-toggle="buttons-checkbox"> \ {{#sidebarViews}} \ - <a href="#" data-action="{{id}}" class="btn active">{{label}}</a> \ + <a href="#" data-action="{{id}}" class="btn">{{label}}</a> \ {{/sidebarViews}} \ </div> \ </div> \ @@ -156,12 +156,6 @@ initialized the MultiView with the relevant views themselves.

    model: this.model, state: this.state.get('view-timeline') }) - }, { - id: 'transform', - label: 'Transform', - view: new my.Transform({ - model: this.model - }) }]; }

    Hashes of sidebar elements

        if(options.sidebarViews) {
           this.sidebarViews = options.sidebarViews;
    @@ -189,6 +183,7 @@ initialized the MultiView with the relevant views themselves.

    } else { this.updateNav(this.pageViews[0].id); } + this._showHideSidebar(); this.model.bind('query:start', function() { self.notify({loader: true, persist: true}); @@ -255,20 +250,28 @@ TODO: set query state ...?

    }); this.el.find('.query-editor-here').append(queryEditor.el); + },

    hide the sidebar if empty

      _showHideSidebar: function() {
    +    var $dataSidebar = this.el.find('.data-view-sidebar');
    +    var visibleChildren = $dataSidebar.children().filter(function() {
    +      return $(this).css("display") != "none";
    +    }).length;
    +
    +    if (visibleChildren > 0) {
    +      $dataSidebar.show();
    +    } else {
    +      $dataSidebar.hide();
    +    }
       },
     
       updateNav: function(pageName) {
         this.el.find('.navigation a').removeClass('active');
         var $el = this.el.find('.navigation a[data-view="' + pageName + '"]');
    -    $el.addClass('active');

    show the specific page

        _.each(this.pageViews, function(view, idx) {
    +    $el.addClass('active');

    add/remove sidebars and hide inactive views

        _.each(this.pageViews, function(view, idx) {
           if (view.id === pageName) {
             view.view.el.show();
             if (view.view.elSidebar) {
               view.view.elSidebar.show();
             }
    -        if (view.view.show) {
    -          view.view.show();
    -        }
           } else {
             view.view.el.hide();
             if (view.view.elSidebar) {
    @@ -279,12 +282,22 @@ TODO: set query state ...?

    } } }); + + this._showHideSidebar();

    call view.view.show after sidebar visibility has been determined so +that views can correctly calculate their maximum width

        _.each(this.pageViews, function(view, idx) {
    +      if (view.id === pageName) {
    +        if (view.view.show) {
    +          view.view.show();
    +        }
    +      }
    +    });
       },
     
       _onMenuClick: function(e) {
         e.preventDefault();
         var action = $(e.target).attr('data-action');
         this['$'+action].toggle();
    +    this._showHideSidebar();
       },
     
       _onSwitchView: function(e) {
    @@ -292,15 +305,15 @@ TODO: set query state ...?

    var viewName = $(e.target).attr('data-view'); this.updateNav(viewName); this.state.set({currentView: viewName}); - },

    create a state object for this view and do the job of

    + },

    create a state object for this view and do the job of

    a) initializing it from both data passed in and other sources (e.g. hash url)

    b) ensure the state object is updated in responese to changes in subviews, query etc.

      _setupState: function(initialState) {
    -    var self = this;

    get data from the query string / hash url plus some defaults

        var qs = my.parseHashQueryString();
    +    var self = this;

    get data from the query string / hash url plus some defaults

        var qs = my.parseHashQueryString();
         var query = qs.reclineQuery;
    -    query = query ? JSON.parse(query) : self.model.queryState.toJSON();

    backwards compatability (now named view-graph but was named graph)

        var graphState = qs['view-graph'] || qs.graph;
    -    graphState = graphState ? JSON.parse(graphState) : {};

    now get default data + hash url plus initial state and initial our state object with it

        var stateData = _.extend({
    +    query = query ? JSON.parse(query) : self.model.queryState.toJSON();

    backwards compatability (now named view-graph but was named graph)

        var graphState = qs['view-graph'] || qs.graph;
    +    graphState = graphState ? JSON.parse(graphState) : {};

    now get default data + hash url plus initial state and initial our state object with it

        var stateData = _.extend({
             query: query,
             'view-graph': graphState,
             backend: this.model.backend.__type__,
    @@ -314,7 +327,7 @@ TODO: set query state ...?

    }, _bindStateChanges: function() { - var self = this;

    finally ensure we update our state object when state of sub-object changes so that state is always up to date

        this.model.queryState.bind('change', function() {
    +    var self = this;

    finally ensure we update our state object when state of sub-object changes so that state is always up to date

        this.model.queryState.bind('change', function() {
           self.state.set({query: self.model.queryState.toJSON()});
         });
         _.each(this.pageViews, function(pageView) {
    @@ -324,7 +337,7 @@ TODO: set query state ...?

    self.state.set(update); pageView.view.state.bind('change', function() { var update = {}; - update['view-' + pageView.id] = pageView.view.state.toJSON();

    had problems where change not being triggered for e.g. grid view so let's do it explicitly

              self.state.set(update, {silent: true});
    +          update['view-' + pageView.id] = pageView.view.state.toJSON();

    had problems where change not being triggered for e.g. grid view so let's do it explicitly

              self.state.set(update, {silent: true});
               self.state.trigger('change');
             });
           }
    @@ -338,7 +351,7 @@ TODO: set query state ...?

    self.notify(flash); }); }); - },

    notify

    + },

    notify

    Create a notification (a div.alert in div.alert-messsages) using provided flash object. Flash attributes (all are optional):

    @@ -378,7 +391,7 @@ flash object. Flash attributes (all are optional):

    }); }, 1000); } - },

    clearNotifications

    + },

    clearNotifications

    Clear all existing notifications

      clearNotifications: function() {
         var $notifications = $('.recline-data-explorer .alert-messages .alert');
    @@ -386,17 +399,18 @@ flash object. Flash attributes (all are optional):

    $(this).remove(); }); } -});

    MultiView.restore

    +});

    MultiView.restore

    Restore a MultiView instance from a serialized state including the associated dataset

    -

    This inverts the state serialization process in Multiview

    my.MultiView.restore = function(state) {

    hack-y - restoring a memory dataset does not mean much ... (but useful for testing!)

      if (state.backend === 'memory') {
    -    var datasetInfo = {
    +

    This inverts the state serialization process in Multiview

    my.MultiView.restore = function(state) {

    hack-y - restoring a memory dataset does not mean much ... (but useful for testing!)

      var datasetInfo;
    +  if (state.backend === 'memory') {
    +    datasetInfo = {
           backend: 'memory',
           records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
         };
       } else {
    -    var datasetInfo = _.extend({
    +    datasetInfo = _.extend({
             url: state.url,
             backend: state.backend
           },
    @@ -409,7 +423,7 @@ flash object. Flash attributes (all are optional):

    state: state }); return explorer; -}

    Miscellaneous Utilities

    var urlPathRegex = /^([^?]+)(\?.*)?/;

    Parse the Hash section of a URL into path and query string

    my.parseHashUrl = function(hashUrl) {
    +};

    Miscellaneous Utilities

    var urlPathRegex = /^([^?]+)(\?.*)?/;

    Parse the Hash section of a URL into path and query string

    my.parseHashUrl = function(hashUrl) {
       var parsed = urlPathRegex.exec(hashUrl);
       if (parsed === null) {
         return {};
    @@ -419,7 +433,7 @@ flash object. Flash attributes (all are optional):

    query: parsed[2] || '' }; } -};

    Parse a URL query string (?xyz=abc...) into a dictionary.

    my.parseQueryString = function(q) {
    +};

    Parse a URL query string (?xyz=abc...) into a dictionary.

    my.parseQueryString = function(q) {
       if (!q) {
         return {};
       }
    @@ -432,13 +446,13 @@ flash object. Flash attributes (all are optional):

    if (q && q.length && q[0] === '?') { q = q.slice(1); } - while (e = r.exec(q)) {

    TODO: have values be array as query string allow repetition of keys

        urlParams[d(e[1])] = d(e[2]);
    +  while (e = r.exec(q)) {

    TODO: have values be array as query string allow repetition of keys

        urlParams[d(e[1])] = d(e[2]);
       }
       return urlParams;
    -};

    Parse the query string out of the URL hash

    my.parseHashQueryString = function() {
    +};

    Parse the query string out of the URL hash

    my.parseHashQueryString = function() {
       q = my.parseHashUrl(window.location.hash).query;
       return my.parseQueryString(q);
    -};

    Compse a Query String

    my.composeQueryString = function(queryParams) {
    +};

    Compse a Query String

    my.composeQueryString = function(queryParams) {
       var queryString = '?';
       var items = [];
       $.each(queryParams, function(key, value) {
    @@ -453,7 +467,7 @@ flash object. Flash attributes (all are optional):

    my.getNewHashForQueryString = function(queryParams) { var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) {

    slice(1) to remove # at start

        return window.location.hash.split('?')[0].slice(1) + queryPart;
    +  if (window.location.hash) {

    slice(1) to remove # at start

        return window.location.hash.split('?')[0].slice(1) + queryPart;
       } else {
         return queryPart;
       }
    diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html
    index 481b5571..08cab3a1 100644
    --- a/docs/src/view.slickgrid.html
    +++ b/docs/src/view.slickgrid.html
    @@ -1,4 +1,4 @@
    -      view.slickgrid.js           

    view.slickgrid.js

    /*jshint multistr:true */
    +      view.slickgrid.js           

    view.slickgrid.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -11,7 +11,24 @@
     
     

    Initialize it with a recline.Model.Dataset.

    -

    NB: you need an explicit height on the element for slickgrid to work

    my.SlickGrid = Backbone.View.extend({
    +

    Additional options to drive SlickGrid grid can be given through state. +The following keys allow for customization: +* gridOptions: to add options at grid level +* columnsEditor: to add editor for editable columns

    + +

    For example: + var grid = new recline.View.SlickGrid({ + model: dataset, + el: $el, + state: { + gridOptions: {editable: true}, + columnsEditor: [ + {column: 'date', editor: Slick.Editors.Date }, + {column: 'title', editor: Slick.Editors.Text} + ] + } + }); +// NB: you need an explicit height on the element for slickgrid to work

    my.SlickGrid = Backbone.View.extend({
       initialize: function(modelEtc) {
         var self = this;
         this.el = $(this.el);
    @@ -20,14 +37,18 @@
         this.model.records.bind('add', this.render);
         this.model.records.bind('reset', this.render);
         this.model.records.bind('remove', this.render);
    +    this.model.records.bind('change', this.onRecordChanged, this);
     
         var state = _.extend({
             hiddenColumns: [],
             columnsOrder: [],
             columnsSort: {},
             columnsWidth: [],
    +        columnsEditor: [],
    +        options: {},
             fitColumns: false
           }, modelEtc.state
    +
         );
         this.state = new recline.Model.ObjectState(state);
       },
    @@ -35,16 +56,24 @@
       events: {
       },
     
    +  onRecordChanged: function(record) {

    Ignore if the grid is not yet drawn

        if (!this.grid) {
    +      return;
    +    }

    Let's find the row corresponding to the index

        var row_index = this.grid.getData().getModelRow( record );
    +    this.grid.invalidateRow(row_index);
    +    this.grid.getData().updateItem(record, row_index);
    +    this.grid.render();
    +  },
    +
       render: function() {
         var self = this;
     
    -    var options = {
    +    var options = _.extend({
           enableCellNavigation: true,
           enableColumnReorder: true,
           explicitInitialization: true,
           syncColumnCellResize: true,
           forceFitColumns: this.state.get('fitColumns')
    -    };

    We need all columns, even the hidden ones, to show on the column picker

        var columns = [];

    custom formatter as default one escapes html + }, self.state.get('gridOptions'));

    We need all columns, even the hidden ones, to show on the column picker

        var columns = [];

    custom formatter as default one escapes html plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values

        var formatter = function(row, cell, value, columnDef, dataContext) {
           var field = self.model.fields.get(columnDef.id);
    @@ -53,55 +82,81 @@ row = row index, cell = cell index, value = value, columnDef = column definition
           } else {
             return value;
           }
    -    }
    +    };
         _.each(this.model.fields.toJSON(),function(field){
           var column = {
    -        id:field['id'],
    -        name:field['label'],
    -        field:field['id'],
    +        id: field.id,
    +        name: field.label,
    +        field: field.id,
             sortable: true,
             minWidth: 80,
             formatter: formatter
           };
     
    -      var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id});
    +      var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
           if (widthInfo){
    -        column['width'] = widthInfo.width;
    +        column.width = widthInfo.width;
           }
     
    +      var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
    +      if (editInfo){
    +        column.editor = editInfo.editor;
    +      }
           columns.push(column);
    -    });

    Restrict the visible columns

        var visibleColumns = columns.filter(function(column) {
    -      return _.indexOf(self.state.get('hiddenColumns'), column.id) == -1;
    -    });

    Order them if there is ordering info on the state

        if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
    +    });

    Restrict the visible columns

        var visibleColumns = columns.filter(function(column) {
    +      return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
    +    });

    Order them if there is ordering info on the state

        if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
           visibleColumns = visibleColumns.sort(function(a,b){
             return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
           });
           columns = columns.sort(function(a,b){
             return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
           });
    -    }

    Move hidden columns to the end, so they appear at the bottom of the + }

    Move hidden columns to the end, so they appear at the bottom of the column picker

        var tempHiddenColumns = [];
         for (var i = columns.length -1; i >= 0; i--){
    -      if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) == -1){
    +      if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
             tempHiddenColumns.push(columns.splice(i,1)[0]);
           }
         }
    -    columns = columns.concat(tempHiddenColumns);
    -
    -    var data = [];
    -
    -    this.model.records.each(function(doc){
    +    columns = columns.concat(tempHiddenColumns);

    Transform a model object into a row

        function toRow(m) {
           var row = {};
           self.model.fields.each(function(field){
    -        row[field.id] = doc.getFieldValueUnrendered(field);
    +        row[field.id] = m.getFieldValueUnrendered(field);
           });
    -      data.push(row);
    +      return row;
    +    }
    +
    +    function RowSet() {
    +      var models = [];
    +      var rows = [];
    +
    +      this.push = function(model, row) {
    +        models.push(model);
    +        rows.push(row);
    +      };
    +
    +      this.getLength = function() {return rows.length; };
    +      this.getItem = function(index) {return rows[index];};
    +      this.getItemMetadata = function(index) {return {};};
    +      this.getModel = function(index) {return models[index];};
    +      this.getModelRow = function(m) {return models.indexOf(m);};
    +      this.updateItem = function(m,i) {
    +        rows[i] = toRow(m);
    +        models[i] = m;
    +      };
    +    }
    +
    +    var data = new RowSet();
    +
    +    this.model.records.each(function(doc){
    +      data.push(doc, toRow(doc));
         });
     
    -    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

    Column sorting

        var sortInfo = this.model.queryState.get('sort');
    +    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

    Column sorting

        var sortInfo = this.model.queryState.get('sort');
         if (sortInfo){
           var column = sortInfo[0].field;
    -      var sortAsc = !(sortInfo[0].order == 'desc');
    +      var sortAsc = sortInfo[0].order !== 'desc';
           this.grid.setSortColumn(column, sortAsc);
         }
     
    @@ -130,19 +185,27 @@ column picker

    self.state.set({columnsWidth:columnsWidth}); }); + this.grid.onCellChange.subscribe(function (e, args) {

    We need to change the model associated value

          var grid = args.grid;
    +      var model = data.getModel(args.row);
    +      var field = grid.getColumns()[args.cell].id;
    +      var v = {};
    +      v[field] = args.item[field];
    +      model.set(v);
    +    });
    +
         var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
                                                            _.extend(options,{state:this.state}));
     
         if (self.visible){
           self.grid.init();
           self.rendered = true;
    -    } else {

    Defer rendering until the view is visible

          self.rendered = false;
    +    } else {

    Defer rendering until the view is visible

          self.rendered = false;
         }
     
         return this;
      },
     
    -  show: function() {

    If the div is hidden, SlickGrid will calculate wrongly some + show: function() {

    If the div is hidden, SlickGrid will calculate wrongly some sizes so we must render it explicitly when the view is visible

        if (!this.rendered){
           if (!this.grid){
             this.render();
    @@ -181,7 +244,7 @@ sizes so we must render it explicitly when the view is visible

    < $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body); $menu.bind('mouseleave', function (e) { - $(this).fadeOut(options.fadeSpeed) + $(this).fadeOut(options.fadeSpeed); }); $menu.bind('click', updateColumn); @@ -198,7 +261,7 @@ sizes so we must render it explicitly when the view is visible

    < $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id); columnCheckboxes.push($input); - if (grid.getColumnIndex(columns[i].id) != null) { + if (grid.getColumnIndex(columns[i].id) !== null) { $input.attr('checked', 'checked'); } $input.appendTo($li); @@ -225,10 +288,12 @@ sizes so we must render it explicitly when the view is visible

    < } function updateColumn(e) { - if ($(e.target).data('option') == 'autoresize') { + var checkbox; + + if ($(e.target).data('option') === 'autoresize') { var checked; if ($(e.target).is('li')){ - var checkbox = $(e.target).find('input').first(); + checkbox = $(e.target).find('input').first(); checked = !checkbox.is(':checked'); checkbox.attr('checked',checked); } else { @@ -248,7 +313,7 @@ sizes so we must render it explicitly when the view is visible

    < if (($(e.target).is('li') && !$(e.target).hasClass('divider')) || $(e.target).is('input')) { if ($(e.target).is('li')){ - var checkbox = $(e.target).find('input').first(); + checkbox = $(e.target).find('input').first(); checkbox.attr('checked',!checkbox.is(':checked')); } var visibleColumns = []; @@ -261,7 +326,6 @@ sizes so we must render it explicitly when the view is visible

    < } }); - if (!visibleColumns.length) { $(e.target).attr('checked', 'checked'); return; @@ -272,7 +336,7 @@ sizes so we must render it explicitly when the view is visible

    < } } init(); - }

    Slick.Controls.ColumnPicker

      $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
    +  }

    Slick.Controls.ColumnPicker

      $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
     })(jQuery);
     
     
    \ No newline at end of file diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html index cc25f584..6f903f8b 100644 --- a/docs/src/view.timeline.html +++ b/docs/src/view.timeline.html @@ -1,4 +1,4 @@ - view.timeline.js

    view.timeline.js

    /*jshint multistr:true */
    +      view.timeline.js           

    view.timeline.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -30,7 +30,8 @@ If not found, the user will need to define these fields on initialization

    }); var stateData = _.extend({ startField: null, - endField: null + endField: null, + timelineJSOptions: {} }, options.state ); @@ -53,13 +54,9 @@ internally to look up element

    }, _initTimeline: function() { - var $timeline = this.el.find(this.elementId);

    set width explicitly o/w timeline goes wider that screen for some reason

        var width = Math.max(this.el.width(), this.el.find('.recline-timeline').width());
    -    if (width) {
    -      $timeline.width(width);
    -    }
    -    var config = {};
    +    var $timeline = this.el.find(this.elementId);
         var data = this._timelineJSON();
    -    this.timeline.init(data, this.elementId, config);
    +    this.timeline.init(data, this.elementId, this.state.get("timelineJSOptions"));
         this._timelineIsInitialized = true
       },
     
    @@ -68,11 +65,11 @@ internally to look up element

    var data = this._timelineJSON(); this.timeline.reload(data); } - },

    Convert record to JSON for timeline

    + },

    Convert record to JSON for timeline

    Designed to be overridden in client apps

      convertRecord: function(record, fields) {
         return this._convertRecord(record, fields);
    -  },

    Internal method to generate a Timeline formatted entry

      _convertRecord: function(record, fields) {
    +  },

    Internal method to generate a Timeline formatted entry

      _convertRecord: function(record, fields) {
         var start = this._parseDate(record.get(this.state.get('startField')));
         var end = this._parseDate(record.get(this.state.get('endField')));
         if (start) {
    @@ -103,7 +100,7 @@ internally to look up element

    if (newEntry) { out.timeline.date.push(newEntry); } - });

    if no entries create a placeholder entry to prevent Timeline crashing with error

        if (out.timeline.date.length === 0) {
    +    });

    if no entries create a placeholder entry to prevent Timeline crashing with error

        if (out.timeline.date.length === 0) {
           var tlEntry = {
             "startDate": '2000,1,1',
             "headline": 'No data to show!'
    @@ -123,10 +120,7 @@ internally to look up element

    out = out.trim() ? moment(out) : null; if (out.toDate() == 'Invalid Date') { return null; - } else {

    fix for moment weirdness around date parsing and time zones -moment('1914-08-01').toDate() => 1914-08-01 00:00 +01:00 -which in iso format (with 0 time offset) is 31 July 1914 23:00 -meanwhile native new Date('1914-08-01') => 1914-08-01 01:00 +01:00

          out = out.subtract('minutes', out.zone());
    +    } else {
           return out.toDate();
         }
       },
    diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html
    index 61ec2ff5..57ef47af 100644
    --- a/docs/src/widget.facetviewer.html
    +++ b/docs/src/widget.facetviewer.html
    @@ -1,4 +1,4 @@
    -      widget.facetviewer.js           

    widget.facetviewer.js

    /*jshint multistr:true */
    +      widget.facetviewer.js           

    widget.facetviewer.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html
    index 5d2b2cfd..c98b3c4b 100644
    --- a/docs/src/widget.fields.html
    +++ b/docs/src/widget.fields.html
    @@ -1,4 +1,4 @@
    -      widget.fields.js           

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    + widget.fields.js

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    For each field

    @@ -28,7 +28,7 @@ If number: max, min average ...

    </small> \ </h4> \ </div> \ - <div id="collapse{{id}}" class="accordion-body collapse in"> \ + <div id="collapse{{id}}" class="accordion-body collapse"> \ <div class="accordion-inner"> \ {{#facets}} \ <div class="facet-summary" data-facet="{{id}}"> \ @@ -47,9 +47,6 @@ If number: max, min average ...

    </div> \ ', - events: { - 'click .js-show-hide': 'onShowHide' - }, initialize: function(model) { var self = this; this.el = $(this.el); @@ -62,6 +59,7 @@ being more liberal (e.g. binding to all) can lead to being called a lot (e.g. fo });

    fields can get reset or changed in which case we need to recalculate

          self.model.getFieldsSummary();
           self.render();
         });
    +    this.el.find('.collapse').collapse();
         this.render();
       },
       render: function() {
    @@ -76,21 +74,6 @@ being more liberal (e.g. binding to all) can lead to being called a lot (e.g. fo
         });
         var templated = Mustache.render(this.template, tmplData);
         this.el.html(templated);
    -    this.el.find('.collapse').collapse('hide');
    -  },
    -  onShowHide: function(e) {
    -    e.preventDefault();
    -    var $target  = $(e.target);

    weird collapse class seems to have been removed (can watch this happen -if you watch dom) but could not work why. Absence of collapse then meant -we could not toggle. -This seems to fix the problem.

        this.el.find('.accordion-body').addClass('collapse');;
    -    if ($target.text() === '+') {
    -      this.el.find('.collapse').collapse('show');
    -      $target.text('-');
    -    } else {
    -      this.el.find('.collapse').collapse('hide');
    -      $target.text('+');
    -    }
       }
     });
     
    diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html
    index 128e953e..4d0079b3 100644
    --- a/docs/src/widget.filtereditor.html
    +++ b/docs/src/widget.filtereditor.html
    @@ -1,4 +1,4 @@
    -      widget.filtereditor.js           

    widget.filtereditor.js

    /*jshint multistr:true */
    +      widget.filtereditor.js           

    widget.filtereditor.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -120,7 +120,7 @@
         $target.hide();
         var filterType = $target.find('select.filterType').val();
         var field      = $target.find('select.fields').val();
    -    this.model.queryState.addFilter({type: filterType, field: field});

    trigger render explicitly as queryState change will not be triggered (as blank value for filter)

        this.render();
    +    this.model.queryState.addFilter({type: filterType, field: field});
       },
       onRemoveFilter: function(e) {
         e.preventDefault();
    @@ -137,7 +137,7 @@
           var $input = $(input);
           var filterType  = $input.attr('data-filter-type');
           var fieldId     = $input.attr('data-filter-field');
    -      var filterIndex = parseInt($input.attr('data-filter-id'));
    +      var filterIndex = parseInt($input.attr('data-filter-id'), 10);
           var name        = $input.attr('name');
           var value       = $input.val();
     
    @@ -158,7 +158,7 @@
               break;
           }
         });
    -    self.model.queryState.set({filters: filters});
    +    self.model.queryState.set({filters: filters, from: 0});
         self.model.queryState.trigger('change');
       }
     });
    diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
    index 78a0925d..5956b87e 100644
    --- a/docs/src/widget.pager.html
    +++ b/docs/src/widget.pager.html
    @@ -1,4 +1,4 @@
    -      widget.pager.js           

    widget.pager.js

    /*jshint multistr:true */
    +      widget.pager.js           

    widget.pager.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -32,6 +32,8 @@
         e.preventDefault();
         var newFrom = parseInt(this.el.find('input[name="from"]').val());
         var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
    +    newFrom = Math.max(newFrom, 0);
    +    newSize = Math.max(newSize, 1);
         this.model.set({size: newSize, from: newFrom});
       },
       onPaginationUpdate: function(e) {
    @@ -43,6 +45,7 @@
         } else {
           newFrom = this.model.get('from') + this.model.get('size');
         }
    +    newFrom = Math.max(newFrom, 0);
         this.model.set({from: newFrom});
       },
       render: function() {
    diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html
    index 38f6e2f3..0684637d 100644
    --- a/docs/src/widget.queryeditor.html
    +++ b/docs/src/widget.queryeditor.html
    @@ -1,4 +1,4 @@
    -      widget.queryeditor.js           

    widget.queryeditor.js

    /*jshint multistr:true */
    +      widget.queryeditor.js           

    widget.queryeditor.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    
    From 773e03c9c751a0691683fa8e8f6207af721bfdee Mon Sep 17 00:00:00 2001
    From: Rufus Pollock 
    Date: Sun, 5 May 2013 20:09:11 +0100
    Subject: [PATCH 46/46] [build/dist][s]: build current.
    
    ---
     dist/recline.css        |  73 ----
     dist/recline.dataset.js |  26 --
     dist/recline.js         | 848 +---------------------------------------
     3 files changed, 1 insertion(+), 946 deletions(-)
    
    diff --git a/dist/recline.css b/dist/recline.css
    index 13cd3858..b8bdd15a 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 cdd33a0e..3ca6fd41 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 70d1931b..9df9cacc 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.
    @@ -2449,468 +2203,6 @@ my.FlotControls = Backbone.View.extend({
     });
     
     })(jQuery, recline.View);
    -/*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -
    -// ## Graph view for a Dataset using Flotr2 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)}
    -//        }
    -// 
    -// 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({
    -  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'); - 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.editor = new my.Flotr2Controls({ - 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'); - 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 - // - // 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() - 20); - var series = this.createSeries(); - var options = this.getGraphOptions(this.state.attributes.graphType); - this.plot = Flotr.draw(this.$graph.get(0), series, options); - } - }, - - show: function() { - // because we cannot redraw when hidden we may need to when becoming visible - if (this.needToRedraw) { - this.redraw(); - } - }, - - // ### getGraphOptions - // - // Get options for Flotr2 Graph - // - // needs to be function as can depend on state - // - // @param typeId graphType id (lines, lines-and-points etc) - getGraphOptions: function(typeId) { - 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; - } - - 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; - }; - - 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'); - - 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(); - } - return x; - } - - var xaxis = {}; - xaxis.tickFormatter = tickFormatter; - - var yaxis = {}; - yaxis.autoscale = true; - yaxis.autoscaleMargin = 0.02; - - var mouse = {}; - mouse.track = true; - mouse.relative = true; - mouse.trackFormatter = trackFormatter; - - var legend = {}; - legend.position = 'ne'; - - // mouse.lineColor is set in createSeries - var optionsPerGraphType = { - lines: { - legend: legend, - colors: this.graphColors, - lines: { show: true }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse - }, - points: { - legend: legend, - colors: this.graphColors, - points: { show: true, hitRadius: 5 }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse, - grid: { hoverable: true, clickable: true } - }, - 'lines-and-points': { - legend: legend, - colors: this.graphColors, - points: { show: true, hitRadius: 5 }, - lines: { show: true }, - xaxis: xaxis, - yaxis: yaxis, - mouse: mouse, - grid: { hoverable: true, clickable: true } - }, - bars: { - legend: legend, - colors: this.graphColors, - lines: { show: false }, - xaxis: yaxis, - yaxis: xaxis, - mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' - }, - bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 - } - }, - columns: { - legend: legend, - colors: this.graphColors, - lines: { show: false }, - xaxis: xaxis, - yaxis: yaxis, - mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'n' - }, - bars: { - show: true, - horizontal: false, - shadowSize: 0, - barWidth: 0.8 - } - }, - grid: { hoverable: true, clickable: true } - }; - - 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) { - // datetime - if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') { - // not bar or column - x = new Date(x).getTime(); - } else { - // bar or column - x = index; - } - } else if (typeof x === 'string') { - // string - x = parseFloat(x); - if (isNaN(x)) { - x = index; - } - } - - var yfield = self.model.fields.get(field); - var y = doc.getFieldValue(yfield); - - // horizontal bar chart - if (self.state.attributes.graphType == 'bars') { - points.push([y, x]); - } else { - points.push([x, y]); - } - }); - series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}}); - }); - return series; - } -}); - -my.Flotr2Controls = 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); - 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