From 9f0c0d753ade13ea6fec74032f9b5896ad779295 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 10 Apr 2012 13:16:37 +0100 Subject: [PATCH 01/48] [#64,view/map] Listen to changes in documents to refresh the map Added new listeners to refresh the markers whenever a document(s) is added or removed, or all documents are reset. Instead of recreating everytime all markers, only the necessary ones are added or removed. --- src/view-map.js | 86 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/src/view-map.js b/src/view-map.js index 1d985a6e..948126eb 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -35,12 +35,16 @@ my.Map = Backbone.View.extend({ var self = this; this.el = $(this.el); - this.render(); this.model.bind('change', function() { self._setupGeometryField(); }); + this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); + this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); + this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); this.mapReady = false; + + this.render(); }, render: function() { @@ -65,35 +69,73 @@ my.Map = Backbone.View.extend({ return this; }, - redraw: function(){ + redraw: function(action,doc){ var self = this; + action = action || 'refresh'; + if (this.geomReady){ - if (this.model.currentDocuments.length > 0){ + if (action == 'reset'){ + // Clear all features this.features.clearLayers(); - var bounds = new L.LatLngBounds(); + } else if (action == 'add' && doc){ + // Add one or n features + this._add(doc); + } else if (action == 'remove' && doc){ + // Remove one or n features + this._remove(doc); + } else if (action == 'refresh'){ + // Clear and rebuild all features + this.features.clearLayers(); + this._add(this.model.currentDocuments.models); - this.model.currentDocuments.forEach(function(doc){ - var feature = self._getGeometryFromDocument(doc); - if (feature){ - // Build popup contents - // TODO: mustache? - html = '' - for (key in doc.attributes){ - html += '
' + key + ': '+ doc.attributes[key] + '
' - } - feature.properties = {popupContent: html}; - - self.features.addGeoJSON(feature); - - // TODO: bounds and center map - } - }); } } }, + _add: function(doc){ + + var self = this; + + if (!(doc instanceof Array)) doc = [doc]; + + doc.forEach(function(doc){ + var feature = self._getGeometryFromDocument(doc); + if (feature){ + // Build popup contents + // TODO: mustache? + html = '' + for (key in doc.attributes){ + html += '
' + key + ': '+ doc.attributes[key] + '
' + } + feature.properties = {popupContent: html}; + + // Add a reference to the model id, which will allow us to + // link this Leaflet layer to a Recline doc + feature.properties.cid = doc.cid; + + self.features.addGeoJSON(feature); + } + }); + }, + + _remove: function(doc){ + + var self = this; + + if (!(doc instanceof Array)) doc = [doc]; + + doc.forEach(function(doc){ + for (key in self.features._layers){ + if (self.features._layers[key].cid == doc.cid){ + self.features.removeLayer(self.features._layers[key]); + } + } + }); + + }, + _getGeometryFromDocument: function(doc){ if (this.geomReady){ if (this._geomFieldName){ @@ -152,6 +194,10 @@ my.Map = Backbone.View.extend({ if (e.properties && e.properties.popupContent){ e.layer.bindPopup(e.properties.popupContent); } + if (e.properties && e.properties.cid){ + e.layer.cid = e.properties.cid; + } + }); this.map.addLayer(this.features); From 5ff726039630cf888223fbf08415d2dc7411d925 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 10 Apr 2012 17:09:06 +0100 Subject: [PATCH 02/48] [#64,view/map] More flexible geom field detection (case insensitive and checks 'location') --- src/view-map.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/view-map.js b/src/view-map.js index 948126eb..b33bbc86 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -12,7 +12,7 @@ my.Map = Backbone.View.extend({ latitudeFieldNames: ['lat','latitude'], longitudeFieldNames: ['lon','longitude'], - geometryFieldNames: ['geom','the_geom','geometry','spatial'], + geometryFieldNames: ['geom','the_geom','geometry','spatial','location'], //TODO: In case we want to change the default markers /* @@ -171,9 +171,12 @@ my.Map = Backbone.View.extend({ _checkField: function(fieldNames){ var field; + var modelFieldNames = this.model.fields.pluck('id'); for (var i = 0; i < fieldNames.length; i++){ - field = this.model.fields.get(fieldNames[i]); - if (field) return field.id; + for (var j = 0; j < modelFieldNames.length; j++){ + if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase()) + return modelFieldNames[j]; + } } return null; }, From 5891149087bde5d05ad89208631d8221cb91c719 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 10 Apr 2012 17:12:53 +0100 Subject: [PATCH 03/48] [#86,view] Fire view show/hide events --- src/view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view.js b/src/view.js index 84754124..48c1e18f 100644 --- a/src/view.js +++ b/src/view.js @@ -204,8 +204,10 @@ my.DataExplorer = Backbone.View.extend({ _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { view.view.el.show(); + view.view.trigger('view:show'); } else { view.view.el.hide(); + view.view.trigger('view:hide'); } }); } From 23c6a6e81b74650f161f4495546fbece89647f59 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 10 Apr 2012 17:14:22 +0100 Subject: [PATCH 04/48] [view/map][s] Recalculate Leaflet sizes when showing the container div --- src/view-map.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/view-map.js b/src/view-map.js index b33bbc86..5886aa51 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -42,6 +42,12 @@ my.Map = Backbone.View.extend({ this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); + // If the div is hidden, Leaflet needs to recalculate some sizes + // to display properly + this.bind('view:show',function(){ + self.map.invalidateSize(); + }); + this.mapReady = false; this.render(); From a8018c4e18c6e952a6a95e36372c34339c5130aa Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 11 Apr 2012 14:15:39 +0100 Subject: [PATCH 05/48] [#64,view/map] Add small editor for user input Right now the form lets you define the fields where the geometries are stored (either in a single GeoJSON field or a pair of lat/lon fields). When adding features, if wrong geometries are found (ie wrong field values), the process will stop and an error notice will be shown. --- css/map.css | 19 +++++++ src/view-map.js | 132 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 5 deletions(-) diff --git a/css/map.css b/css/map.css index 705feebc..c8adde77 100644 --- a/css/map.css +++ b/css/map.css @@ -2,3 +2,22 @@ height: 500px; } +/********************************************************** + * Editor + *********************************************************/ + +.data-map-container .editor { + float: right; + width: 200px; + padding-left: 0px; + margin-left: 10px; +} + +.data-map-container .editor form { + padding-left: 4px; +} + +.data-map-container .editor select { + width: 100%; +} + diff --git a/src/view-map.js b/src/view-map.js index dc05b717..d39aebed 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -27,10 +27,66 @@ my.Map = Backbone.View.extend({ */ template: ' \ +
\ +
\ +
\ +
\ + \ + \ +
\ +
\ + \ +
\ + \ +
\ + \ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ + \ +
\ + \ + \
\
\ ', + events: { + 'click .editor-update-map': 'onEditorSubmit', + 'change .editor-field-type': 'onFieldTypeChange' + }, + + initialize: function(options, config) { var self = this; @@ -38,6 +94,11 @@ my.Map = Backbone.View.extend({ this.model.bind('change', function() { self._setupGeometryField(); }); + this.model.fields.bind('add', this.render); + this.model.fields.bind('reset', function(){ + self._setupGeometryField() + self.render() + }); this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); @@ -58,9 +119,22 @@ my.Map = Backbone.View.extend({ var self = this; htmls = $.mustache(this.template, this.model.toTemplateJSON()); + $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); + // Setup editor fields + if (this.geomReady && this.model.fields.length){ + if (this._geomFieldName){ + this._selectOption('editor-geom-field',this._geomFieldName); + $('#editor-field-type-geom').attr('checked','checked').change(); + } else{ + this._selectOption('editor-lon-field',this._lonFieldName); + this._selectOption('editor-lat-field',this._latFieldName); + $('#editor-field-type-latlon').attr('checked','checked').change(); + } + } + this.model.bind('query:done', function() { if (!self.geomReady){ self._setupGeometryField(); @@ -81,7 +155,7 @@ my.Map = Backbone.View.extend({ action = action || 'refresh'; - if (this.geomReady){ + if (this.geomReady && this.mapReady){ if (action == 'reset'){ // Clear all features this.features.clearLayers(); @@ -99,6 +173,34 @@ my.Map = Backbone.View.extend({ } }, + /* UI Event handlers */ + + onEditorSubmit: function(e){ + e.preventDefault(); + if ($('#editor-field-type-geom').attr('checked')){ + this._geomFieldName = $('.editor-geom-field > select > option:selected').val(); + this._latFieldName = this._lonFieldName = false; + } else { + this._geomFieldName = false; + this._latFieldName = $('.editor-lat-field > select > option:selected').val(); + this._lonFieldName = $('.editor-lon-field > select > option:selected').val(); + } + this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); + this.redraw(); + + return false; + }, + + onFieldTypeChange: function(e){ + if (e.target.value == 'geom'){ + $('.editor-field-type-geom').show(); + $('.editor-field-type-latlon').hide(); + } else { + $('.editor-field-type-geom').hide(); + $('.editor-field-type-latlon').show(); + } + }, + _add: function(doc){ var self = this; @@ -107,7 +209,7 @@ my.Map = Backbone.View.extend({ doc.forEach(function(doc){ var feature = self._getGeometryFromDocument(doc); - if (feature){ + if (feature instanceof Object){ // Build popup contents // TODO: mustache? html = '' @@ -120,7 +222,17 @@ my.Map = Backbone.View.extend({ // link this Leaflet layer to a Recline doc feature.properties.cid = doc.cid; - self.features.addGeoJSON(feature); + try { + self.features.addGeoJSON(feature); + } catch (except) { + var msg = 'Wrong geometry value'; + if (except.message) msg += ' (' + except.message + ')'; + my.notify(msg,{category:'error'}); + _.breakLoop(); + } + } else { + my.notify('Wrong geometry value',{category:'error'}); + _.breakLoop(); } }); }, @@ -169,8 +281,6 @@ my.Map = Backbone.View.extend({ this._latFieldName = this._checkField(this.latitudeFieldNames); this._lonFieldName = this._checkField(this.longitudeFieldNames); - // TODO: Allow users to choose the fields - this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); }, @@ -212,6 +322,18 @@ my.Map = Backbone.View.extend({ this.map.setView(new L.LatLng(0, 0), 2); this.mapReady = true; + }, + + _selectOption: function(id,value){ + var options = $('.' + id + ' > select > option'); + if (options){ + options.each(function(opt){ + if (this.value == value) { + $(this).attr('selected','selected'); + return false; + } + }); + } } }); From 9080fb8c6b0396df8b8282361483385a908c34d7 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 11 Apr 2012 17:33:17 +0100 Subject: [PATCH 06/48] [#64,view/map] Add source docs and links from home page --- docs/backend/base.html | 143 ++++++-- docs/backend/dataproxy.html | 81 ++++- docs/backend/elasticsearch.html | 113 ++++-- docs/backend/gdocs.html | 218 ++++++++++-- docs/backend/localcsv.html | 317 +++++++++++++++-- docs/backend/memory.html | 117 ++++++- docs/backend/pycco.css | 190 ++++++++++ docs/model.html | 455 +++++++++++++++++++----- docs/pycco.css | 190 ++++++++++ docs/view-flot-graph.html | 332 +++++++++++++++--- docs/view-grid.html | 216 +++++++++--- docs/view-map.html | 604 ++++++++++++++++++++++++++++++++ docs/view.html | 364 ++++++++++++++++--- index.html | 4 +- src/view-map.js | 107 ++++-- 15 files changed, 3023 insertions(+), 428 deletions(-) create mode 100644 docs/backend/pycco.css create mode 100644 docs/pycco.css create mode 100644 docs/view-map.html diff --git a/docs/backend/base.html b/docs/backend/base.html index 61eab0d5..2be99510 100644 --- a/docs/backend/base.html +++ b/docs/backend/base.html @@ -1,59 +1,109 @@ - base.js

base.js

Recline Backends

- + + + + + base.js + + + +
+
+
+

base.js

+
+
+
+
+
+ # +
+

Recline Backends

Backends are connectors to backend data sources and stores

- -

This is just the base module containing a template Base class and convenience methods.

this.recline = this.recline || {};
+

This is just the base module containing a template Base class and convenience methods.

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

Backbone.sync

- -

Override Backbone.sync to hand off to sync function in relevant backend

  Backbone.sync = function(method, model, options) {
+(function($, my) {
+ + +
+
+
+
+ # +
+

Backbone.sync

+

Override Backbone.sync to hand off to sync function in relevant backend

+
+
+
  Backbone.sync = function(method, model, options) {
     return model.backend.sync(method, model, options);
-  }

recline.Backend.Base

- + }; + + +
+
+
+
+ # +
+

recline.Backend.Base

Base class for backends providing a template and convenience functions. You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.

- -

Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.

  my.Base = Backbone.Model.extend({

sync

- +

Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.

+ +
+
  my.Base = Backbone.Model.extend({
+
+ +
+
+
+
+ # +
+

sync

An implementation of Backbone.sync that will be used to override Backbone.sync on operations for Datasets and Documents which are using this backend.

-

For read-only implementations you will need only to implement read method for Dataset models (and even this can be a null operation). The read method should return relevant metadata for the Dataset. We do not require read support for Documents because they are loaded in bulk by the query method.

-

For backends supporting write operations you must implement update and delete support for Document objects.

- -

All code paths should return an object conforming to the jquery promise API.

    sync: function(method, model, options) {
-    },
-    

query

- +

All code paths should return an object conforming to the jquery promise API.

+ +
+
    sync: function(method, model, options) {
+    },
+
+ +
+
+
+
+ # +
+

query

Query the backend for documents returning them in bulk. This method will be used by the Dataset.query method to search the backend for documents, retrieving the results in bulk.

-

@param {recline.model.Dataset} model: Dataset model.

-

@param {Object} queryObj: object describing a query (usually produced by using recline.Model.Query and calling toJSON on it).

-

The structure of data in the Query object or Hash should follow that defined in issue 34. (Of course, if you are writing your own backend, and hence have control over the interpretation of the query object, you can use whatever structure you like).

-

@returns {Promise} promise API object. The promise resolve method will be called on query completion with a QueryResult object.

-

A QueryResult has the following structure (modelled closely on ElasticSearch - see this issue for more details):

-
 {
   total: // (required) total number of results (can be null)
@@ -68,8 +118,23 @@ details):

// facet results (as per ) } } -
    query: function(model, queryObj) {
-    },

convenience method to convert simple set of documents / rows to a QueryResult

    _docsToQueryResult: function(rows) {
+
+
+
+
    query: function(model, queryObj) {
+    },
+
+ +
+
+
+
+ # +
+

convenience method to convert simple set of documents / rows to a QueryResult

+
+
+
    _docsToQueryResult: function(rows) {
       var hits = _.map(rows, function(row) {
         return { _source: row };
       });
@@ -77,11 +142,22 @@ details):

total: null, hits: hits }; - },

_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.

    _wrapInTimeout: function(ourFunction) {
+a crude way to catch those errors.

+
+
+
    _wrapInTimeout: function(ourFunction) {
       var dfd = $.Deferred();
       var timeout = 5000;
       var timer = setTimeout(function() {
@@ -104,4 +180,9 @@ a crude way to catch those errors.

}(jQuery, this.recline.Backend)); -
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/dataproxy.html b/docs/backend/dataproxy.html index 303d2286..30e64829 100644 --- a/docs/backend/dataproxy.html +++ b/docs/backend/dataproxy.html @@ -1,32 +1,72 @@ - dataproxy.js

dataproxy.js

this.recline = this.recline || {};
+
+
+
+  
+  dataproxy.js
+  
+
+
+
+
+
+

dataproxy.js

+
+
+
+
+
+ # +
+ +
+
+
this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
-(function($, my) {

DataProxy Backend

- +(function($, my) { + + +
+
+
+
+ # +
+

DataProxy Backend

For connecting to DataProxy-s.

-

When initializing the DataProxy backend you can set the following attributes:

-
  • dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com
-

Datasets using using this backend should set the following attributes:

-
  • url: (required) url-of-data-to-proxy
  • format: (optional) csv | xls (defaults to csv if not specified)
- -

Note that this is a read-only backend.

  my.DataProxy = my.Base.extend({
+

Note that this is a read-only backend.

+
+
+
  my.DataProxy = my.Base.extend({
     defaults: {
       dataproxy_url: 'http://jsonpdataproxy.appspot.com'
     },
     sync: function(method, model, options) {
       var self = this;
       if (method === "read") {
-        if (model.__type__ == 'Dataset') {

Do nothing as we will get fields in query step (and no metadata to -retrieve)

          var dfd = $.Deferred();
+        if (model.__type__ == 'Dataset') {
+ + +
+
+
+
+ # +
+

Do nothing as we will get fields in query step (and no metadata to +retrieve)

+
+
+
          var dfd = $.Deferred();
           dfd.resolve(model);
           return dfd.promise();
         }
@@ -38,14 +78,14 @@ retrieve)

var self = this; var base = this.get('dataproxy_url'); var data = { - url: dataset.get('url') - , 'max-results': queryObj.size - , type: dataset.get('format') + url: dataset.get('url'), + 'max-results': queryObj.size, + type: dataset.get('format') }; var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' + url: base, + data: data, + dataType: 'jsonp' }); var dfd = $.Deferred(); this._wrapInTimeout(jqxhr).done(function(results) { @@ -76,4 +116,9 @@ retrieve)

}(jQuery, this.recline.Backend)); -
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html index bb338392..6c296914 100644 --- a/docs/backend/elasticsearch.html +++ b/docs/backend/elasticsearch.html @@ -1,13 +1,41 @@ - elasticsearch.js

elasticsearch.js

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

elasticsearch.js

+
+
+
+
+
+ # +
+ +
+
+
this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
-(function($, my) {

ElasticSearch Backend

- +(function($, my) { + + +
+
+
+
+ # +
+

ElasticSearch Backend

Connecting to ElasticSearch.

-

To use this backend ensure your Dataset has one of the following attributes (first one found is used):

-
 elasticsearch_url
 webstore_url
@@ -16,8 +44,10 @@ url
 
 

This should point to the ES type url. E.G. for ES running on localhost:9200 with index twitter and type tweet it would be

- -
http://localhost:9200/twitter/tweet
  my.ElasticSearch = my.Base.extend({
+
http://localhost:9200/twitter/tweet
+
+
+
  my.ElasticSearch = my.Base.extend({
     _getESUrl: function(dataset) {
       var out = dataset.get('elasticsearch_url');
       if (out) return out;
@@ -37,7 +67,19 @@ localhost:9200 with index twitter and type tweet it would be

dataType: 'jsonp' }); var dfd = $.Deferred(); - this._wrapInTimeout(jqxhr).done(function(schema) {

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

            var key = _.keys(schema)[0];
+          this._wrapInTimeout(jqxhr).done(function(schema) {
+ + +
+
+
+
+ # +
+

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;
@@ -55,35 +97,43 @@ localhost:9200 with index twitter and type tweet it would be

} }, _normalizeQuery: function(queryObj) { - if (queryObj.toJSON) { - var out = queryObj.toJSON(); - } else { - var out = _.extend({}, queryObj); - } - if (out.q != undefined && out.q.trim() === '') { + var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); + if (out.q !== undefined && out.q.trim() === '') { delete out.q; } if (!out.q) { out.query = { match_all: {} - } + }; } else { out.query = { query_string: { query: out.q } - } + }; delete out.q; - }

now do filters (note the plural)

      if (out.filters && out.filters.length) {
+      }
+ + +
+
+
+
+ # +
+

now do filters (note the plural)

+
+
+
      if (out.filters && out.filters.length) {
         if (!out.filter) {
-          out.filter = {}
+          out.filter = {};
         }
         if (!out.filter.and) {
           out.filter.and = [];
         }
         out.filter.and = out.filter.and.concat(out.filters);
       }
-      if (out.filters != undefined) {
+      if (out.filters !== undefined) {
         delete out.filters;
       }
       return out;
@@ -97,12 +147,24 @@ localhost:9200 with index twitter and type tweet it would be

data: data, dataType: 'jsonp' }); - var dfd = $.Deferred();

TODO: fail case

      jqxhr.done(function(results) {
+      var dfd = $.Deferred();
+ + +
+
+
+
+ # +
+

TODO: fail case

+
+
+
      jqxhr.done(function(results) {
         _.each(results.hits.hits, function(hit) {
-          if (!'id' in hit._source && hit._id) {
+          if (!('id' in hit._source) && hit._id) {
             hit._source.id = hit._id;
           }
-        })
+        });
         if (results.facets) {
           results.hits.facets = results.facets;
         }
@@ -115,4 +177,9 @@ localhost:9200 with index twitter and type tweet it would be

}(jQuery, this.recline.Backend)); -
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html index 10963ee7..60fba435 100644 --- a/docs/backend/gdocs.html +++ b/docs/backend/gdocs.html @@ -1,30 +1,73 @@ - gdocs.js

gdocs.js

this.recline = this.recline || {};
+
+
+
+  
+  gdocs.js
+  
+
+
+
+
+
+

gdocs.js

+
+
+
+
+
+ # +
+ +
+
+
this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
-(function($, my) {

Google spreadsheet backend

- +(function($, my) { + + +
+
+
+
+ # +
+

Google spreadsheet backend

Connect to Google Docs spreadsheet.

-

Dataset must have a url attribute pointing to the Gdocs spreadsheet's JSON feed e.g.

-
 var dataset = new recline.Model.Dataset({
     url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
   },
   'gdocs'
 );
-
  my.GDoc = my.Base.extend({
+
+
+
+
  my.GDoc = my.Base.extend({
     getUrl: function(dataset) {
       var url = dataset.get('url');
       if (url.indexOf('feeds/list') != -1) {
         return url;
-      } else {

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

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

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

+
+
+
        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
         var matches = url.match(regex);
         if (matches) {
           var key = matches[1];
           var worksheet = 1;
-          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
+          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
           return out;
         } else {
           alert('Failed to extract gdocs key from ' + url);
@@ -44,58 +87,164 @@ var dataset = new recline.Model.Dataset({
           model.fields.reset(_.map(result.field, function(fieldId) {
               return {id: fieldId};
             })
-          );

cache data onto dataset (we have loaded whole gdoc it seems!)

          model._dataCache = result.data;
+          );
+ + +
+
+
+
+ # +
+

cache data onto dataset (we have loaded whole gdoc it seems!)

+
+
+
          model._dataCache = result.data;
           dfd.resolve(model);
-        })
-        return dfd.promise(); }
+        });
+        return dfd.promise();
+      }
     },
 
     query: function(dataset, queryObj) { 
       var dfd = $.Deferred();
-      var fields = _.pluck(dataset.fields.toJSON(), 'id');

zip the fields with the data rows to produce js objs -TODO: factor this out as a common method with other backends

      var objs = _.map(dataset._dataCache, function (d) { 
+      var fields = _.pluck(dataset.fields.toJSON(), 'id');
+ + +
+
+
+
+ # +
+

zip the fields with the data rows to produce js objs +TODO: factor this out as a common method with other backends

+
+
+
      var objs = _.map(dataset._dataCache, function (d) { 
         var obj = {};
-        _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
+        _.each(_.zip(fields, d), function (x) {
+          obj[x[0]] = x[1];
+        });
         return obj;
       });
       dfd.resolve(this._docsToQueryResult(objs));
       return dfd;
     },
-    gdocsToJavascript:  function(gdocsSpreadsheet) {
-      /*
-         :options: (optional) optional argument dictionary:
-         columnsToUse: list of columns to use (specified by field names)
-         colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
-         :return: tabular data object (hash with keys: field and data).
-
-         Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
-         */
-      var options = {};
+    gdocsToJavascript:  function(gdocsSpreadsheet) {
+
+
+
+
+
+
+ # +
+

:options: (optional) optional argument dictionary: + columnsToUse: list of columns to use (specified by field names) + colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). + :return: tabular data object (hash with keys: field and data).

+

Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.

+
+
+
      var options = {};
       if (arguments.length > 1) {
         options = arguments[1];
       }
       var results = {
         'field': [],
         'data': []
-      };

default is no special info on type of columns

      var colTypes = {};
+      };
+ + +
+
+
+
+ # +
+

default is no special info on type of columns

+
+
+
      var colTypes = {};
       if (options.colTypes) {
         colTypes = options.colTypes;
-      }

either extract column headings from spreadsheet directly, or used supplied ones

      if (options.columnsToUse) {

columns set to subset supplied

        results.field = options.columnsToUse;
-      } else {

set columns to use to be all available

        if (gdocsSpreadsheet.feed.entry.length > 0) {
+      }
+ + +
+
+
+
+ # +
+

either extract column headings from spreadsheet directly, or used supplied ones

+
+
+
      if (options.columnsToUse) {
+
+
+
+
+
+
+ # +
+

columns set to subset supplied

+
+
+
        results.field = options.columnsToUse;
+      } else {
+
+
+
+
+
+
+ # +
+

set columns to use to be all available

+
+
+
        if (gdocsSpreadsheet.feed.entry.length > 0) {
           for (var k in gdocsSpreadsheet.feed.entry[0]) {
             if (k.substr(0, 3) == 'gsx') {
-              var col = k.substr(4)
-                results.field.push(col);
+              var col = k.substr(4);
+              results.field.push(col);
             }
           }
         }
-      }

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

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

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

+
+
+
      var rep = /^([\d\.\-]+)\%$/;
       $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
         var row = [];
         for (var k in results.field) {
           var col = results.field[k];
           var _keyname = 'gsx$' + col;
-          var value = entry[_keyname]['$t'];

if labelled as % and value contains %, convert

          if (colTypes[col] == 'percent') {
+          var value = entry[_keyname]['$t'];
+ + +
+
+
+
+ # +
+

if labelled as % and value contains %, convert

+
+
+
          if (colTypes[col] == 'percent') {
             if (rep.test(value)) {
               var value2 = rep.exec(value);
               var value3 = parseFloat(value2);
@@ -113,4 +262,9 @@ TODO: factor this out as a common method with other backends

}(jQuery, this.recline.Backend)); -
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/localcsv.html b/docs/backend/localcsv.html index 1abf5bf3..69dfb80b 100644 --- a/docs/backend/localcsv.html +++ b/docs/backend/localcsv.html @@ -1,4 +1,26 @@ - localcsv.js
processField; processField = function (field) { - if (fieldQuoted !== true) { }; for (i = 0; i < s.length; i += 1) { - cur = s.charAt(i); }; var rxIsInt = /^\d+$/, - rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

localcsv.js

this.recline = this.recline || {};
+
+
+
+  
+  localcsv.js
+  
+
+
+
+
+
+

localcsv.js

+
+
+
+
+
+ # +
+ +
+
+
this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
 (function($, my) {
@@ -7,13 +29,25 @@
       id: file.name,
       file: file
     };
-    var reader = new FileReader();

TODO

    reader.onload = function(e) {
+    var reader = new FileReader();
+ + +
+
+
+
+ # +
+

TODO

+
+
+
    reader.onload = function(e) {
       var dataset = my.csvToDataset(e.target.result);
       callback(dataset);
     };
     reader.onerror = function (e) {
       alert('Failed to load file. Code: ' + e.target.error.code);
-    }
+    };
     reader.readAsText(file);
   };
 
@@ -31,19 +65,39 @@
     });
     var dataset = recline.Backend.createDataset(data, fields);
     return dataset;
-  }

Converts a Comma Separated Values string into an array of arrays. + }; + + +

+
+
+
+ # +
+

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

-

Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.

-

@return The CSV parsed as an array @type Array

-

@param {String} s The string to convert @param {Boolean} [trm=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported

-

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

	my.parseCSV= function(s, trm) {

Get rid of any trailing \n

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

+
+
+
	my.parseCSV= function(s, trm) {
+
+ +
+
+
+
+ # +
+

Get rid of any trailing \n

+
+
+
		s = chomp(s);
 
 		var cur = '', // The character we are currently processing.
 			inQuote = false,
@@ -55,10 +109,46 @@ thttp://www.uselesscode.org/javascript/csv/

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);
@@ -68,25 +158,145 @@ thttp://www.uselesscode.org/javascript/csv/

If we are at a EOF or EOR

			if (inQuote === false && (cur === ',' || 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 === ',' || 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 ", add it to the field buffer

				if (cur !== '"') {
+			} else {
+ + +
+
+
+
+ # +
+

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

+
+
+
				if (cur !== '"') {
 					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 ", this is an escaped "

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

Skip the next char

							i += 1;
-						} else {

It's not escaping, so end quote

							inQuote = false;
+					} else {
+ + +
+
+
+
+ # +
+

Next char is ", this is an escaped "

+
+
+
						if (s.charAt(i + 1) === '"') {
+							field += '"';
+
+
+
+
+
+
+ # +
+

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);
 
@@ -94,10 +304,34 @@ thttp://www.uselesscode.org/javascript/csv/

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) {
+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) {
 				return function (s) {
 					return s.trim();
 				};
@@ -109,12 +343,41 @@ 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);
 		}
 	}
 
 
 }(jQuery, this.recline.Backend));
 
-
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/memory.html b/docs/backend/memory.html index 3f03bd6a..5403926c 100644 --- a/docs/backend/memory.html +++ b/docs/backend/memory.html @@ -1,19 +1,51 @@ - memory.js

memory.js

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

memory.js

+
+
+
+
+
+ # +
+ +
+
+
this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
-(function($, my) {

createDataset

- +(function($, my) { + + +
+
+
+
+ # +
+

createDataset

Convenience function to create a simple 'in-memory' dataset in one step.

-

@param data: list of hashes for each document/row in the data ({key: value, key: value}) @param fields: (optional) list of field hashes (each hash defining a hash as per recline.Model.Field). If fields not specified they will be taken from the data. @param metadata: (optional) dataset metadata - see recline.Model.Dataset. -If not defined (or id not provided) id will be autogenerated.

  my.createDataset = function(data, fields, metadata) {
+If not defined (or id not provided) id will be autogenerated.

+
+
+
  my.createDataset = function(data, fields, metadata) {
     if (!metadata) {
-      var metadata = {};
+      metadata = {};
     }
     if (!metadata.id) {
       metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
@@ -36,17 +68,22 @@ If not defined (or id not provided) id will be autogenerated.

var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory'); dataset.fetch(); return dataset; - };

Memory Backend - uses in-memory data

- + }; + + +
+
+
+
+ # +
+

Memory Backend - uses in-memory data

To use it you should provide in your constructor data:

-
  • metadata (including fields array)
  • documents: list of hashes, each hash being one doc. A doc must have an id attribute which is unique.
-

Example:

-

  // Backend setup
  var backend = recline.Backend.Memory();
@@ -65,7 +102,10 @@ If not defined (or id not provided) id will be autogenerated.

  my.Memory = my.Base.extend({
+ 

+
+
+
  my.Memory = my.Base.extend({
     initialize: function() {
       this.datasets = {};
     },
@@ -74,8 +114,8 @@ If not defined (or id not provided) id will be autogenerated.

}, sync: function(method, model, options) { var self = this; + var dfd = $.Deferred(); if (method === "read") { - var dfd = $.Deferred(); if (model.__type__ == 'Dataset') { var rawDataset = this.datasets[model.id]; model.set(rawDataset.metadata); @@ -85,7 +125,6 @@ If not defined (or id not provided) id will be autogenerated.

} return dfd.promise(); } else if (method === 'update') { - var dfd = $.Deferred(); if (model.__type__ == 'Document') { _.each(self.datasets[model.dataset.id].documents, function(doc, idx) { if(doc.id === model.id) { @@ -96,7 +135,6 @@ If not defined (or id not provided) id will be autogenerated.

} return dfd.promise(); } else if (method === 'delete') { - var dfd = $.Deferred(); if (model.__type__ == 'Document') { var rawDataset = self.datasets[model.dataset.id]; var newdocs = _.reject(rawDataset.documents, function(doc) { @@ -121,7 +159,19 @@ If not defined (or id not provided) id will be autogenerated.

var fieldId = _.keys(filter.term)[0]; return (doc[fieldId] == filter.term[fieldId]); }); - });

not complete sorting!

      _.each(queryObj.sort, function(sortObj) {
+      });
+ + +
+
+
+
+ # +
+

not complete sorting!

+
+
+
      _.each(queryObj.sort, function(sortObj) {
         var fieldName = _.keys(sortObj)[0];
         results = _.sortBy(results, function(doc) {
           var _out = doc[fieldName];
@@ -145,7 +195,19 @@ If not defined (or id not provided) id will be autogenerated.

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

faceting

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

faceting

+
+
+
      _.each(documents, function(doc) {
         _.each(queryObj.facets, function(query, facetId) {
           var fieldId = query.terms.field;
           var val = doc[fieldId];
@@ -162,7 +224,19 @@ If not defined (or id not provided) id will be autogenerated.

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);
       });
@@ -173,4 +247,9 @@ If not defined (or id not provided) id will be autogenerated.

}(jQuery, this.recline.Backend)); -
\ No newline at end of file + + + +
+ + diff --git a/docs/backend/pycco.css b/docs/backend/pycco.css new file mode 100644 index 00000000..aef571a5 --- /dev/null +++ b/docs/backend/pycco.css @@ -0,0 +1,190 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 16px; + line-height: 24px; + color: #252519; + margin: 0; padding: 0; + background: #f5f5ff; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 40px 0 15px 0; +} +h2, h3, h4, h5, h6 { + margin-top: 0; + } +#container { + background: white; + } +#container, div.section { + position: relative; +} +#background { + position: absolute; + top: 0; left: 580px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: 0; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +div.docs { + float: left; + max-width: 500px; + min-width: 500px; + min-height: 5px; + padding: 10px 25px 1px 50px; + vertical-align: top; + text-align: left; +} + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .octowrap { + position: relative; + } + .octothorpe { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + div.docs:hover .octothorpe { + opacity: 1; + } +div.code { + margin-left: 580px; + padding: 14px 15px 16px 50px; + vertical-align: top; +} + .code pre, .docs p code { + font-size: 12px; + } + pre, tt, code { + line-height: 18px; + font-family: Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } +div.clearall { + clear: both; +} + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/docs/model.html b/docs/model.html index 2a21273c..92dbe7fe 100644 --- a/docs/model.html +++ b/docs/model.html @@ -1,31 +1,69 @@ - model.js

model.js

Recline Backbone Models

this.recline = this.recline || {};
+
+
+
+  
+  model.js
+  
+
+
+
+
+
+

model.js

+
+
+
+
+
+ # +
+

Recline Backbone Models

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

A Dataset model

- +(function($, my) { + + +
+
+
+
+ # +
+

A Dataset model

A model has the following (non-Backbone) attributes:

-

@property {FieldList} fields: (aka columns) is a FieldList listing all the fields on this Dataset (this can be set explicitly, or, will be set by Dataset.fetch() or Dataset.query()

-

@property {DocumentList} currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (updated by calling query method)

-

@property {number} docCount: total number of documents in this dataset

-

@property {Backend} backend: the Backend (instance) for this Dataset

-

@property {Query} queryState: Query object which stores current queryState. queryState may be edited by other components (e.g. a query editor view) changes will trigger a Dataset query.

-

@property {FacetList} facets: FacetList object containing all current -Facets.

my.Dataset = Backbone.Model.extend({
-  __type__: 'Dataset',

initialize

- -

Sets up instance properties (see above)

  initialize: function(model, backend) {
+Facets.

+
+
+
my.Dataset = Backbone.Model.extend({
+  __type__: 'Dataset',
+
+ +
+
+
+
+ # +
+

initialize

+

Sets up instance properties (see above)

+
+
+
  initialize: function(model, backend) {
     _.bindAll(this, 'query');
     this.backend = backend;
     if (backend && backend.constructor == String) {
@@ -38,15 +76,24 @@ Facets.

this.queryState = new my.Query(); this.queryState.bind('change', this.query); this.queryState.bind('facet:add', this.query); - },

query

- + }, + + +
+
+
+
+ # +
+

query

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

-

It will query based on current query state (given by this.queryState) updated by queryObj (if provided).

-

Resulting DocumentList are used to reset this.currentDocuments and are -also returned.

  query: function(queryObj) {
+also returned.

+
+
+
  query: function(queryObj) {
     var self = this;
     this.trigger('query:start');
     var actualQuery = self._prepareQuery(queryObj);
@@ -91,16 +138,38 @@ also returned.

data.fields = this.fields.toJSON(); return data; } -});

A Document (aka Row)

- -

A single entry or row in the dataset

my.Document = Backbone.Model.extend({
+});
+ + +
+
+
+
+ # +
+

A Document (aka Row)

+

A single entry or row in the dataset

+
+
+
my.Document = Backbone.Model.extend({
   __type__: 'Document',
   initialize: function() {
     _.bindAll(this, 'getFieldValue');
-  },

getFieldValue

- + }, + + +
+
+
+
+ # +
+

getFieldValue

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

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

+
+
+
  getFieldValue: function(field) {
     var val = this.get(field.id);
     if (field.deriver) {
       val = field.deriver(val, field, this);
@@ -110,80 +179,150 @@ for this document.

} return val; } -});

A Backbone collection of Documents

my.DocumentList = Backbone.Collection.extend({
+});
+ + +
+
+
+
+ # +
+

A Backbone collection of Documents

+
+
+
my.DocumentList = Backbone.Collection.extend({
   __type__: 'DocumentList',
   model: my.Document
-});

A Field (aka Column) on a Dataset

- +}); + + +
+
+
+
+ # +
+

A Field (aka Column) on a Dataset

Following (Backbone) attributes as standard:

-
  • id: a unique identifer for this field- usually this should match the key in the documents hash
  • label: (optional: defaults to id) the visible label used for this field
  • type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on http://www.elasticsearch.org/guide/reference/mapping/
  • -
  • format: (optional) used to indicate how the data should be formatted. For example: -
    • type=date, format=yyyy-mm-dd
    • +
    • format: (optional) used to indicate how the data should be formatted. For example:
    • +
    • type=date, format=yyyy-mm-dd
    • type=float, format=percentage
    • -
    • type=float, format='###,###.##'
  • +
  • type=float, format='###,###.##'
  • is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
-

Following additional instance properties:

-

@property {Function} renderer: a function to render the data for this field. Signature: function(value, field, doc) where value is the value of this cell, field is corresponding field object and document is the document object. Note that implementing functions can ignore arguments (e.g. function(value) would be a valid formatter function).

-

@property {Function} deriver: a function to derive/compute the value of data in this field as a function of this field's value (if any) and the current document, its signature and behaviour is the same as for renderer. Use of this function allows you to define an entirely new value for data in this field. This provides support for a) 'derived/computed' fields: i.e. fields whose data are functions of the data in other fields b) transforming the -value of this field prior to rendering.

my.Field = Backbone.Model.extend({

defaults - define default values

  defaults: {
+value of this field prior to rendering.

+
+
+
my.Field = Backbone.Model.extend({
+
+ +
+
+
+
+ # +
+

defaults - define default values

+
+
+
  defaults: {
     label: null,
     type: 'string',
     format: null,
     is_derived: false
-  },

initialize

- + }, + + +
+
+
+
+ # +
+

initialize

@param {Object} data: standard Backbone model attributes

- -

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
+

@param {Object} options: renderer and/or deriver functions.

+
+
+
  initialize: function(data, options) {
+
+ +
+
+
+
+ # +
+

if a hash not passed in the first argument throw error

+
+
+
    if ('0' in data) {
       throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
     }
-    if (this.attributes.label == null) {
+    if (this.attributes.label === null) {
       this.set({label: this.id});
     }
     if (options) {
       this.renderer = options.renderer;
       this.deriver = options.deriver;
     }
+    if (!this.renderer) {
+      this.renderer = this.defaultRenderers[this.get('type')];
+    }
+  },
+  defaultRenderers: {
+    object: function(val, field, doc) {
+      return JSON.stringify(val);
+    },
+    'float': function(val, field, doc) {
+      var format = field.get('format'); 
+      if (format === 'percentage') {
+        return val + '%';
+      }
+    }
   }
 });
 
 my.FieldList = Backbone.Collection.extend({
   model: my.Field
-});

Query

- +}); + + +
+
+
+
+ # +
+

Query

Query instances encapsulate a query to the backend (see query method on backend). Useful both for creating queries and for storing and manipulating query state - e.g. from a query editor).

-

Query Structure and format

-

Query structure should follow that of ElasticSearch query language.

-

NB: It is up to specific backends how to implement and support this query structure. Different backends might choose to implement things differently or not support certain features. Please check your backend for details.

-

Query object has the following key attributes:

-
  • size (=limit): number of results to return
  • from (=offset): offset into result set - http://www.elasticsearch.org/guide/reference/api/search/from-size.html
  • @@ -191,25 +330,26 @@ or not support certain features. Please check your backend for details.
  • query: Query in ES Query DSL http://www.elasticsearch.org/guide/reference/api/search/query.html
  • filter: See filters and Filtered Query
  • fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html
  • -
  • facets: TODO - see http://www.elasticsearch.org/guide/reference/api/search/facets/
  • +
  • facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/
-

Additions:

-
    -
  • q: either straight text or a hash will map directly onto a query_string -query -in backend

    - -
    • Of course this can be re-interpreted by different backends. E.g. some -may just pass this straight through e.g. for an SQL backend this could be -the full SQL query
  • -
  • filters: dict of ElasticSearch filters. These will be and-ed together for -execution.

  • +
  • +

    q: either straight text or a hash will map directly onto a query_string + query + in backend

    +
  • +
  • +

    Of course this can be re-interpreted by different backends. E.g. some + may just pass this straight through e.g. for an SQL backend this could be + the full SQL query

    +
  • +
  • +

    filters: dict of ElasticSearch filters. These will be and-ed together for + execution.

    +
-

Examples

-
 {
    q: 'quick brown fox',
@@ -217,40 +357,122 @@ execution.

{ term: { 'owner': 'jones' } } ] } -
my.Query = Backbone.Model.extend({
+
+
+
+
my.Query = Backbone.Model.extend({
   defaults: function() {
     return {
-      size: 100
-      , from: 0
-      , facets: {}

http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html -, filter: {}

      , filters: []
-    }
-  },

addTermFilter

- + size: 100, + from: 0, + facets: {}, + + +
+
+ +
+
      filters: []
+    };
+  },
+
+
+
+
+
+
+ # +
+

addTermFilter

Set (update or add) a terms filter to filters

- -

See http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html

  addTermFilter: function(fieldId, value) {
+

See http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html

+
+
+
  addTermFilter: function(fieldId, value) {
     var filters = this.get('filters');
     var filter = { term: {} };
     filter.term[fieldId] = value;
     filters.push(filter);
-    this.set({filters: filters});

change does not seem to be triggered automatically

    if (value) {
+    this.set({filters: filters});
+ + +
+
+
+
+ # +
+

change does not seem to be triggered automatically

+
+
+
    if (value) {
       this.trigger('change');
-    } else {

adding a new blank filter and do not want to trigger a new query

      this.trigger('change:filters:new-blank');
+    } else {
+ + +
+
+
+
+ # +
+

adding a new blank filter and do not want to trigger a new query

+
+
+
      this.trigger('change:filters:new-blank');
     }
-  },

removeFilter

- -

Remove a filter from filters at index filterIndex

  removeFilter: function(filterIndex) {
+  },
+ + +
+
+
+
+ # +
+

removeFilter

+

Remove a filter from filters at index filterIndex

+
+
+
  removeFilter: function(filterIndex) {
     var filters = this.get('filters');
     filters.splice(filterIndex, 1);
     this.set({filters: filters});
     this.trigger('change');
-  },

addFacet

- + }, + + +
+
+
+
+ # +
+

addFacet

Add a Facet to this query

- -

See http://www.elasticsearch.org/guide/reference/api/search/facets/

  addFacet: function(fieldId) {
-    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
+

See http://www.elasticsearch.org/guide/reference/api/search/facets/

+
+
+
  addFacet: function(fieldId) {
+    var facets = this.get('facets');
+
+ +
+
+
+
+ # +
+

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

+
+
+
    if (_.contains(_.keys(facets), fieldId)) {
       return;
     }
     facets[fieldId] = {
@@ -258,20 +480,36 @@ execution.

}; this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); + }, + addHistogramFacet: function(fieldId) { + var facets = this.get('facets'); + facets[fieldId] = { + date_histogram: { + field: fieldId, + interval: 'day' + } + }; + this.set({facets: facets}, {silent: true}); + this.trigger('facet:add', this); } -});

A Facet (Result)

- +}); + + +
+
+
+
+ # +
+

A Facet (Result)

Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the backend.

-

Structure of a facet follows that of Facet results in ElasticSearch, see: http://www.elasticsearch.org/guide/reference/api/search/facets/

-

Specifically the object structure of a facet looks like (there is one addition compared to ElasticSearch: the "id" field which corresponds to the key used to specify this facet in the facet query):

-
 {
   "id": "id-of-facet",
@@ -296,7 +534,10 @@ key used to specify this facet in the facet query):

} ] } -
my.Facet = Backbone.Model.extend({
+
+
+
+
my.Facet = Backbone.Model.extend({
   defaults: function() {
     return {
       _type: 'terms',
@@ -304,14 +545,42 @@ key used to specify this facet in the facet query):

other: 0, missing: 0, terms: [] - } + }; } -});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
+});
+ + +
+
+
+
+ # +
+

A Collection/List of Facets

+
+
+
my.FacetList = Backbone.Collection.extend({
   model: my.Facet
-});

Backend registry

- -

Backends will register themselves by id into this registry

my.backends = {};
+});
+ + +
+
+
+
+ # +
+

Backend registry

+

Backends will register themselves by id into this registry

+
+
+
my.backends = {};
 
 }(jQuery, this.recline.Model));
 
-
\ No newline at end of file + + + +
+ + diff --git a/docs/pycco.css b/docs/pycco.css new file mode 100644 index 00000000..aef571a5 --- /dev/null +++ b/docs/pycco.css @@ -0,0 +1,190 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 16px; + line-height: 24px; + color: #252519; + margin: 0; padding: 0; + background: #f5f5ff; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 40px 0 15px 0; +} +h2, h3, h4, h5, h6 { + margin-top: 0; + } +#container { + background: white; + } +#container, div.section { + position: relative; +} +#background { + position: absolute; + top: 0; left: 580px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: 0; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +div.docs { + float: left; + max-width: 500px; + min-width: 500px; + min-height: 5px; + padding: 10px 25px 1px 50px; + vertical-align: top; + text-align: left; +} + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .octowrap { + position: relative; + } + .octothorpe { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + div.docs:hover .octothorpe { + opacity: 1; + } +div.code { + margin-left: 580px; + padding: 14px 15px 16px 50px; + vertical-align: top; +} + .code pre, .docs p code { + font-size: 12px; + } + pre, tt, code { + line-height: 18px; + font-family: Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } +div.clearall { + clear: both; +} + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/docs/view-flot-graph.html b/docs/view-flot-graph.html index cc2893c3..50035571 100644 --- a/docs/view-flot-graph.html +++ b/docs/view-flot-graph.html @@ -1,25 +1,55 @@ - view-flot-graph.js
varseries=this.$series.map(function(){return$(this).val();}); - this.chartConfig.series=$.makeArray(series) + this.chartConfig.series=$.makeArray(series);this.chartConfig.group=this.el.find('.editor-group select').val(); - this.chartConfig.graphType=this.el.find('.editor-type select').val(); this.plot.resize(); this.plot.setupGrid(); this.plot.draw(); - }

view-flot-graph.js

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

view-flot-graph.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.

- +(function($, my) { + + +
+
+
+
+ # +
+

Graph view for a Dataset using Flot graphing library.

Initialization arguments:

-
  • model: recline.Model.Dataset
  • -
  • config: (optional) graph configuration hash of form:

    - +
  • +

    config: (optional) graph 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.FlotGraph = Backbone.View.extend({
+generate the element itself (you can then append view.el to the DOM.

+
+
+
my.FlotGraph = Backbone.View.extend({
 
   tagName:  "div",
   className: "data-graph-container",
@@ -79,16 +109,28 @@ generate the element itself (you can then append view.el to the DOM.

', events: { - 'change form select': 'onEditorSubmit' - , 'click .editor-add': 'addSeries' - , 'click .action-remove-series': 'removeSeries' - , 'click .action-toggle-help': 'toggleHelp' + 'change form select': 'onEditorSubmit', + 'click .editor-add': 'addSeries', + 'click .action-remove-series': 'removeSeries', + 'click .action-toggle-help': 'toggleHelp' }, initialize: function(options, config) { var self = this; this.el = $(this.el); - _.bindAll(this, 'render', 'redraw');

we need the model.fields to render properly

    this.model.bind('change', this.render);
+    _.bindAll(this, 'render', 'redraw');
+ + +
+
+
+
+ # +
+

we need the model.fields to render properly

+
+
+
    this.model.bind('change', this.render);
     this.model.fields.bind('reset', this.render);
     this.model.fields.bind('add', this.render);
     this.model.currentDocuments.bind('add', this.redraw);
@@ -110,8 +152,32 @@ generate the element itself (you can then append view.el to the DOM.

render: function() { htmls = $.mustache(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls);

now set a load of stuff up

    this.$graph = this.el.find('.panel.graph');

for use later when adding additional series -could be simpler just to have a common template!

    this.$seriesClone = this.el.find('.editor-series').clone();
+    $(this.el).html(htmls);
+ + +
+
+
+
+ # +
+

now set a load of stuff up

+
+
+
    this.$graph = this.el.find('.panel.graph');
+
+
+
+
+
+
+ # +
+

for use later when adding additional series +could be simpler just to have a common template!

+
+
+
    this.$seriesClone = this.el.find('.editor-series').clone();
     this._updateSeries();
     return this;
   },
@@ -122,27 +188,71 @@ could be simpler just to have a common template!

update navigation

    var qs = my.parseHashQueryString();
-    qs['graph'] = JSON.stringify(this.chartConfig);
+    this.chartConfig.graphType = this.el.find('.editor-type select').val();
+ + +
+
+
+
+ # +
+

update navigation

+
+
+
    var qs = my.parseHashQueryString();
+    qs.graph = JSON.stringify(this.chartConfig);
     my.setHashQueryString(qs);
     this.redraw();
   },
 
-  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.currentDocuments.length == 0)) {
-      return
+  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.currentDocuments.length === 0)) {
+      return;
     }
     var series = this.createSeries();
     var options = this.getGraphOptions(this.chartConfig.graphType);
     this.plot = $.plot(this.$graph, series, options);
-    this.setupTooltips();

create this.plot and cache it + this.setupTooltips(); + + +

+
+
+
+ # +
+

create this.plot and cache it if (!this.plot) { this.plot = $.plot(this.$graph, series, options); } else { @@ -151,39 +261,90 @@ could be simpler just to have a common template!

  },

needs to be function as can depend on state

  getGraphOptions: function(typeId) { 
-    var self = this;

special tickformatter to show labels rather than numbers

    var tickFormatter = function (val) {
+   }

+
+
+
  },
+
+ +
+
+
+
+ # +
+

needs to be function as can depend on state

+
+
+
  getGraphOptions: function(typeId) { 
+    var self = this;
+
+
+
+
+
+
+ # +
+

special tickformatter to show labels rather than numbers

+
+
+
    var tickFormatter = function (val) {
       if (self.model.currentDocuments.models[val]) {
-        var out = self.model.currentDocuments.models[val].get(self.chartConfig.group);

if the value was in fact a number we want that not the

        if (typeof(out) == 'number') {
+        var out = self.model.currentDocuments.models[val].get(self.chartConfig.group);
+ + +
+
+
+
+ # +
+

if the value was in fact a number we want that not the

+
+
+
        if (typeof(out) == 'number') {
           return val;
         } else {
           return out;
         }
       }
       return val;
-    }

TODO: we should really use tickFormatter and 1 interval ticks if (and + }; + + +

+
+
+
+ # +
+

TODO: we should really use tickFormatter and 1 interval ticks if (and only if) x-axis values are non-numeric However, that is non-trivial to work out from a dataset (datasets may -have no field type info). Thus at present we only do this for bars.

    var options = { 
+have no field type info). Thus at present we only do this for bars.

+
+
+
    var options = { 
       lines: {
          series: { 
            lines: { show: true }
          }
-      }
-      , points: {
+      },
+      points: {
         series: {
           points: { show: true }
         },
         grid: { hoverable: true, clickable: true }
-      }
-      , 'lines-and-points': {
+      },
+      'lines-and-points': {
         series: {
           points: { show: true },
           lines: { show: true }
         },
         grid: { hoverable: true, clickable: true }
-      }
-      , bars: {
+      },
+      bars: {
         series: {
           lines: {show: false},
           bars: {
@@ -203,7 +364,7 @@ have no field type info). Thus at present we only do this for bars.

max: self.model.currentDocuments.length - 0.5 } } - } + }; return options[typeId]; }, @@ -230,7 +391,19 @@ have no field type info). Thus at present we only do this for bars.

$("#flot-tooltip").remove(); var x = item.datapoint[0]; - var y = item.datapoint[1];

convert back from 'index' value on x-axis (e.g. in cases where non-number values)

          if (self.model.currentDocuments.models[x]) {
+          var y = item.datapoint[1];
+ + +
+
+
+
+ # +
+

convert back from 'index' value on x-axis (e.g. in cases where non-number values)

+
+
+
          if (self.model.currentDocuments.models[x]) {
             x = self.model.currentDocuments.models[x].get(self.chartConfig.group);
           } else {
             x = x.toFixed(2);
@@ -264,7 +437,19 @@ have no field type info). Thus at present we only do this for bars.

var y = doc.get(field); if (typeof x === 'string') { x = index; - }

horizontal bar chart

          if (self.chartConfig.graphType == 'bars') {
+          }
+ + +
+
+
+
+ # +
+

horizontal bar chart

+
+
+
          if (self.chartConfig.graphType == 'bars') {
             points.push([y, x]);
           } else {
             points.push([x, y]);
@@ -274,12 +459,22 @@ have no field type info). Thus at present we only do this for bars.

}); } return series; - },

Public: Adds a new empty series select box to the editor.

- + }, + + +
+
+
+
+ # +
+

Public: Adds a new empty series select box to the editor.

All but the first select box will have a remove button that allows them to be removed.

- -

Returns itself.

  addSeries: function (e) {
+

Returns itself.

+
+
+
  addSeries: function (e) {
     e.preventDefault();
     var element = this.$seriesClone.clone(),
         label   = element.find('label'),
@@ -290,9 +485,20 @@ to be removed.

label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]'); label.find('span').text(String.fromCharCode(this.$series.length + 64)); return this; - },

Public: Removes a series list item from the editor.

- -

Also updates the labels of the remaining series elements.

  removeSeries: function (e) {
+  },
+ + +
+
+
+
+ # +
+

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();
@@ -308,13 +514,29 @@ to be removed.

toggleHelp: function() { this.el.find('.editor-info').toggleClass('editor-hide-info'); - },

Private: Resets the series property to reference the select elements.

- -

Returns itself.

  _updateSeries: function () {
+  },
+ + +
+
+
+
+ # +
+

Private: Resets the series property to reference the select elements.

+

Returns itself.

+
+
+
  _updateSeries: function () {
     this.$series  = this.el.find('.editor-series select');
   }
 });
 
 })(jQuery, recline.View);
 
-
\ No newline at end of file + + + +
+ + diff --git a/docs/view-grid.html b/docs/view-grid.html index 9b058bdd..f354bcf0 100644 --- a/docs/view-grid.html +++ b/docs/view-grid.html @@ -1,13 +1,43 @@ - view-grid.js

view-grid.js

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

view-grid.js

+
+
+
+
+
+ # +
+

jshint multistr:true

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

DataGrid

- +(function($, my) { + + +
+
+
+
+ # +
+

DataGrid

Provides a tabular view on a Dataset.

- -

Initialize it with a recline.Model.Dataset.

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

Initialize it with a recline.Model.Dataset.

+
+
+
my.DataGrid = Backbone.View.extend({
   tagName:  "div",
   className: "recline-grid-container",
 
@@ -23,11 +53,20 @@
   },
 
   events: {
-    'click .column-header-menu': 'onColumnHeaderClick'
-    , 'click .row-header-menu': 'onRowHeaderClick'
-    , 'click .root-header-menu': 'onRootHeaderClick'
-    , 'click .data-table-menu li a': 'onMenuClick'
-  },

TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). + 'click .column-header-menu': 'onColumnHeaderClick', + 'click .row-header-menu': 'onRowHeaderClick', + 'click .root-header-menu': 'onRootHeaderClick', + 'click .data-table-menu li a': 'onMenuClick' + }, + + +

+
+
+
+ # +
+

TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). showDialog: function(template, data) { if (!data) data = {}; util.show('dialog'); @@ -36,8 +75,24 @@ showDialog: function(template, data) { util.hide('dialog'); }) $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); -},

====================================================== -Column and row menus

  onColumnHeaderClick: function(e) {
+},

+
+
+
+
+ +
+
+
+
+ # +
+
################################################
+

Column and row menus +

+
+
+
  onColumnHeaderClick: function(e) {
     this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
   },
 
@@ -58,31 +113,45 @@ Column and row menus

var self = this; e.preventDefault(); var actions = { - bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); }, facet: function() { self.model.queryState.addFacet(self.state.currentColumn); }, + facet_histogram: function() { + self.model.queryState.addHistogramFacet(self.state.currentColumn); + }, filter: function() { self.model.queryState.addTermFilter(self.state.currentColumn, ''); }, - transform: function() { self.showTransformDialog('transform') }, - sortAsc: function() { self.setColumnSort('asc') }, - sortDesc: function() { self.setColumnSort('desc') }, - hideColumn: function() { self.hideColumn() }, - showColumn: function() { self.showColumn(e) }, + transform: function() { self.showTransformDialog('transform'); }, + sortAsc: function() { self.setColumnSort('asc'); }, + sortDesc: function() { self.setColumnSort('desc'); }, + hideColumn: function() { self.hideColumn(); }, + showColumn: function() { self.showColumn(e); }, deleteRow: function() { - var doc = _.find(self.model.currentDocuments.models, function(doc) {

important this is == as the currentRow will be string (as comes -from DOM) while id may be int

          return doc.id == self.state.currentRow
+        var doc = _.find(self.model.currentDocuments.models, function(doc) {
+ + +
+
+
+
+ # +
+

important this is == as the currentRow will be string (as comes +from DOM) while id may be int

+
+
+
          return doc.id == self.state.currentRow;
         });
         doc.destroy().then(function() { 
             self.model.currentDocuments.remove(doc);
             my.notify("Row deleted successfully");
-          })
-          .fail(function(err) {
-            my.notify("Errorz! " + err)
-          })
+          }).fail(function(err) {
+            my.notify("Errorz! " + err);
+          });
       }
-    }
+    };
     actions[$(e.target).attr('data-action')]();
   },
 
@@ -98,7 +167,7 @@ from DOM) while id may be int

$el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -112,7 +181,7 @@ from DOM) while id may be int

$el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -130,9 +199,21 @@ from DOM) while id may be int

showColumn: function(e) { this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column')); this.render(); - },

======================================================

- -

Templating

  template: ' \
+  },
+ + +
+
+
+
+ # +
+
################################################
+

Templating

+

+
+
+
  template: ' \
     <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
       <thead> \
         <tr> \
@@ -151,7 +232,8 @@ from DOM) while id may be int

<div class="btn-group column-header-menu"> \ <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \ <ul class="dropdown-menu data-table-menu pull-right"> \ - <li><a data-action="facet" href="JavaScript:void(0);">Facet on this Field</a></li> \ + <li><a data-action="facet" href="JavaScript:void(0);">Term Facet</a></li> \ + <li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</a></li> \ <li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \ <li class="divider"></li> \ <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \ @@ -172,8 +254,20 @@ from DOM) while id may be int

', toTemplateJSON: function() { - var modelData = this.model.toJSON() - modelData.notEmpty = ( this.fields.length > 0 )

TODO: move this sort of thing into a toTemplateJSON method on Dataset?

    modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
+    var modelData = this.model.toJSON();
+    modelData.notEmpty = ( this.fields.length > 0 );
+ + +
+
+
+
+ # +
+

TODO: move this sort of thing into a toTemplateJSON method on Dataset?

+
+
+
    modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
     return modelData;
   },
   render: function() {
@@ -193,24 +287,32 @@ from DOM) while id may be int

}); newView.render(); }); - this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); + this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0)); return this; } -});

DataGridRow View for rendering an individual document.

- +}); + + +
+
+
+
+ # +
+

DataGridRow View for rendering an individual document.

Since we want this to update in place it is up to creator to provider the element to attach to.

-

In addition you must pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.

-

Example:

-
 var row = new DataGridRow({
   model: dataset-document,
     el: dom-element,
     fields: mydatasets.fields // a FieldList object
   });
-
my.DataGridRow = Backbone.View.extend({
+
+
+
+
my.DataGridRow = Backbone.View.extend({
   initialize: function(initData) {
     _.bindAll(this, 'render');
     this._fields = initData.fields;
@@ -249,9 +351,9 @@ var row = new DataGridRow({
       return {
         field: field.id,
         value: doc.getFieldValue(field)
-      }
-    })
-    return { id: this.id, cells: cellData }
+      };
+    });
+    return { id: this.id, cells: cellData };
   },
 
   render: function() {
@@ -259,8 +361,21 @@ var row = new DataGridRow({
     var html = $.mustache(this.template, this.toTemplateJSON());
     $(this.el).html(html);
     return this;
-  },

=================== -Cell Editor methods

  onEditClick: function(e) {
+  },
+ + +
+
+
+
+ # +
+
#############
+

Cell Editor methods +

+
+
+
  onEditClick: function(e) {
     var editing = this.el.find('.data-table-cell-editor-editor');
     if (editing.length > 0) {
       editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
@@ -299,4 +414,9 @@ Cell Editor methods

})(jQuery, recline.View); -
\ No newline at end of file + + + +
+ + diff --git a/docs/view-map.html b/docs/view-map.html new file mode 100644 index 00000000..95dbf1aa --- /dev/null +++ b/docs/view-map.html @@ -0,0 +1,604 @@ + + + + + view-map.js + + + +
+
+
+

view-map.js

+
+
+
+
+
+ # +
+

jshint multistr:true

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

Map view for a Dataset using Leaflet mapping library.

+

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

+

Initialization arguments:

+
    +
  • +

    options: initial options. They must contain a model:

    +

    { + model: {recline.Model.Dataset} + }

    +
  • +
  • +

    config: (optional) map configuration hash (not yet used)

    +
  • +
+
+
+
my.Map = Backbone.View.extend({
+
+  tagName:  'div',
+  className: 'data-map-container',
+
+  template: ' \
+  <div class="editor"> \
+    <form class="form-stacked"> \
+      <div class="clearfix"> \
+        <div class="editor-field-type"> \
+            <label class="radio"> \
+              <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
+              Latitude / Longitude fields</label> \
+            <label class="radio"> \
+              <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
+              GeoJSON field</label> \
+        </div> \
+        <div class="editor-field-type-latlon"> \
+          <label>Latitude field</label> \
+          <div class="input editor-lat-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+          <label>Longitude field</label> \
+          <div class="input editor-lon-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+        </div> \
+        <div class="editor-field-type-geom" style="display:none"> \
+          <label>Geometry field (GeoJSON)</label> \
+          <div class="input editor-geom-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+        </div> \
+      </div> \
+      <div class="editor-buttons"> \
+        <button class="btn editor-update-map">Update</button> \
+      </div> \
+      <input type="hidden" class="editor-id" value="map-1" /> \
+      </div> \
+    </form> \
+  </div> \
+<div class="panel map"> \
+</div> \
+',
+
+
+
+
+
+
+ # +
+

These are the default field names that will be used if found. +If not found, the user will need to define the fields via the editor.

+
+
+
  latitudeFieldNames: ['lat','latitude'],
+  longitudeFieldNames: ['lon','longitude'],
+  geometryFieldNames: ['geom','the_geom','geometry','spatial','location'],
+
+
+
+
+
+
+ # +
+

Define here events for UI elements

+
+
+
  events: {
+    'click .editor-update-map': 'onEditorSubmit',
+    'change .editor-field-type': 'onFieldTypeChange'
+  },
+
+
+  initialize: function(options, config) {
+    var self = this;
+
+    this.el = $(this.el);
+
+
+
+
+
+
+ # +
+

Listen to changes in the fields

+
+
+
    this.model.bind('change', function() {
+      self._setupGeometryField();
+    });
+    this.model.fields.bind('add', this.render);
+    this.model.fields.bind('reset', function(){
+      self._setupGeometryField()
+      self.render()
+    });
+
+
+
+
+
+
+ # +
+

Listen to changes in the documents

+
+
+
    this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
+    this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
+    this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
+
+
+
+
+
+
+ # +
+

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

+
+
+
    this.bind('view:show',function(){
+        self.map.invalidateSize();
+    });
+
+    this.mapReady = false;
+
+    this.render();
+  },
+
+
+
+
+
+
+ # +
+

Public: Adds the necessary elements to the page.

+

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

+
+
+
  render: function() {
+
+    var self = this;
+
+    htmls = $.mustache(this.template, this.model.toTemplateJSON());
+
+    $(this.el).html(htmls);
+    this.$map = this.el.find('.panel.map');
+
+    if (this.geomReady && this.model.fields.length){
+      if (this._geomFieldName){
+        this._selectOption('editor-geom-field',this._geomFieldName);
+        $('#editor-field-type-geom').attr('checked','checked').change();
+      } else{
+        this._selectOption('editor-lon-field',this._lonFieldName);
+        this._selectOption('editor-lat-field',this._latFieldName);
+        $('#editor-field-type-latlon').attr('checked','checked').change();
+      }
+    }
+
+    this.model.bind('query:done', function() {
+      if (!self.geomReady){
+        self._setupGeometryField();
+      }
+
+      if (!self.mapReady){
+        self._setupMap();
+      }
+      self.redraw();
+    });
+
+    return this;
+  },
+
+
+
+
+
+
+ # +
+

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

+

Actions can be:

+
    +
  • reset: Clear all features
  • +
  • add: Add one or n features (documents)
  • +
  • remove: Remove one or n features (documents)
  • +
  • refresh: Clear existing features and add all current documents
  • +
+
+
+
  redraw: function(action,doc){
+
+    var self = this;
+
+    action = action || 'refresh';
+
+    if (this.geomReady && this.mapReady){
+      if (action == 'reset'){
+        this.features.clearLayers();
+      } else if (action == 'add' && doc){
+        this._add(doc);
+      } else if (action == 'remove' && doc){
+        this._remove(doc);
+      } else if (action == 'refresh'){
+        this.features.clearLayers();
+        this._add(this.model.currentDocuments.models);
+      }
+    }
+  },
+
+
+
+
+
+
+ # +
+

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){
+    e.preventDefault();
+    if ($('#editor-field-type-geom').attr('checked')){
+        this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
+        this._latFieldName = this._lonFieldName = false;
+    } else {
+        this._geomFieldName = false;
+        this._latFieldName = $('.editor-lat-field > select > option:selected').val();
+        this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
+    }
+    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+    this.redraw();
+
+    return false;
+  },
+
+
+
+
+
+
+ # +
+

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

+
+
+
  onFieldTypeChange: function(e){
+    if (e.target.value == 'geom'){
+        $('.editor-field-type-geom').show();
+        $('.editor-field-type-latlon').hide();
+    } else {
+        $('.editor-field-type-geom').hide();
+        $('.editor-field-type-latlon').show();
+    }
+  },
+
+
+
+
+
+
+ # +
+

Private: Add one or n features to the map

+

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

+

Each feature will have a popup associated with all the document fields.

+
+
+
  _add: function(doc){
+
+    var self = this;
+
+    if (!(doc instanceof Array)) doc = [doc];
+
+    doc.forEach(function(doc){
+      var feature = self._getGeometryFromDocument(doc);
+      if (feature instanceof Object){
+
+
+
+
+
+
+ # +
+

Build popup contents +TODO: mustache?

+
+
+
        html = ''
+        for (key in doc.attributes){
+          html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
+        }
+        feature.properties = {popupContent: html};
+
+
+
+
+
+
+ # +
+

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

+
+
+
        feature.properties.cid = doc.cid;
+
+        try {
+            self.features.addGeoJSON(feature);
+        } catch (except) {
+            var msg = 'Wrong geometry value';
+            if (except.message) msg += ' (' + except.message + ')';
+            my.notify(msg,{category:'error'});
+            _.breakLoop();
+        }
+      } else {
+        my.notify('Wrong geometry value',{category:'error'});
+        _.breakLoop();
+      }
+    });
+  },
+
+
+
+
+
+
+ # +
+

Private: Remove one or n features to the map

+
+
+
  _remove: function(doc){
+
+    var self = this;
+
+    if (!(doc instanceof Array)) doc = [doc];
+
+    doc.forEach(function(doc){
+      for (key in self.features._layers){
+        if (self.features._layers[key].cid == doc.cid){
+          self.features.removeLayer(self.features._layers[key]);
+        }
+      }
+    });
+
+  },
+
+
+
+
+
+
+ # +
+

Private: Return a GeoJSON geomtry extracted from the document fields

+
+
+
  _getGeometryFromDocument: function(doc){
+    if (this.geomReady){
+      if (this._geomFieldName){
+
+
+
+
+
+
+ # +
+

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

+
+
+
        return doc.attributes[this._geomFieldName];
+      } else if (this._lonFieldName && this._latFieldName){
+
+
+
+
+
+
+ # +
+

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

+
+
+
        return {
+          type: 'Point',
+          coordinates: [
+            doc.attributes[this._lonFieldName],
+            doc.attributes[this._latFieldName]
+            ]
+        };
+      }
+      return null;
+    }
+  },
+
+
+
+
+
+
+ # +
+

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(){
+    var geomField, latField, lonField;
+
+    this._geomFieldName = this._checkField(this.geometryFieldNames);
+    this._latFieldName = this._checkField(this.latitudeFieldNames);
+    this._lonFieldName = this._checkField(this.longitudeFieldNames);
+
+    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+  },
+
+
+
+
+
+
+ # +
+

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');
+    for (var i = 0; i < fieldNames.length; i++){
+      for (var j = 0; j < modelFieldNames.length; j++){
+        if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
+          return modelFieldNames[j];
+      }
+    }
+    return null;
+  },
+
+
+
+
+
+
+ # +
+

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

+

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

+
+
+
  _setupMap: function(){
+
+    this.map = new L.Map(this.$map.get(0));
+
+    var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+    var osmAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
+    var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
+    this.map.addLayer(bg);
+
+    this.features = new L.GeoJSON();
+    this.features.on('featureparse', function (e) {
+      if (e.properties && e.properties.popupContent){
+        e.layer.bindPopup(e.properties.popupContent);
+       }
+      if (e.properties && e.properties.cid){
+        e.layer.cid = e.properties.cid;
+       }
+
+    });
+    this.map.addLayer(this.features);
+
+    this.map.setView(new L.LatLng(0, 0), 2);
+
+    this.mapReady = true;
+  },
+
+
+
+
+
+
+ # +
+

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){
+        if (this.value == value) {
+          $(this).attr('selected','selected');
+          return false;
+        }
+      });
+    }
+  }
+
+ });
+
+})(jQuery, recline.View);
+
+
+
+
+
+
+ diff --git a/docs/view.html b/docs/view.html index 0bebc221..ce57a75f 100644 --- a/docs/view.html +++ b/docs/view.html @@ -1,11 +1,39 @@ - view.js
$(this.el).html(template);var$dataViewContainer=this.el.find('.data-view-container');_.each(this.pageViews,function(view,pageName){ - $dataViewContainer.append(view.view.el) + $dataViewContainer.append(view.view.el);});varqueryEditor=newmy.QueryEditor({model:this.model.queryState @@ -182,7 +255,19 @@ note this.model and dataset returned are the same

},setupRouting:function(){ - varself=this;this.el.find('.navigation li a').removeClass('disabled');var$el=this.el.find('.navigation li a[href=#'+pageName+']');$el.parent().addClass('active'); - $el.addClass('disabled'); ',events:{ - 'submit form':'onFormSubmit' - ,'click .action-pagination-update':'onPaginationUpdate' + 'submit form':'onFormSubmit', + 'click .action-pagination-update':'onPaginationUpdate'},initialize:function(){ @@ -257,10 +356,11 @@ note this.model and dataset returned are the same

onPaginationUpdate:function(e){e.preventDefault();var$el=$(e.target); + varnewFrom=0;if($el.parent().hasClass('prev')){ - varnewFrom=this.model.get('from')-Math.max(0,this.model.get('size')); + newFrom=this.model.get('from')-Math.max(0,this.model.get('size'));}else{ - varnewFrom=this.model.get('from')+this.model.get('size'); + newFrom=this.model.get('from')+this.model.get('size');}this.model.set({from:newFrom});}, @@ -317,7 +417,19 @@ note this.model and dataset returned are the same

this.render();},render:function(){ - vartmplData=$.extend(true,{},this.model.toJSON());fieldId:fieldId,label:fieldId,value:filter.term[fieldId] - } + };});varout=$.mustache(this.template,tmplData); - this.el.html(out); {{#terms}} \ <li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \ {{/terms}} \ + {{#entries}} \ + <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \ + {{/entries}} \ </ul> \ </div> \ {{/facets}} \ @@ -404,8 +531,29 @@ note this.model and dataset returned are the same

facets:this.model.facets.toJSON(),fields:this.model.fields.toJSON()}; + tmplData.facets=_.map(tmplData.facets,function(facet){ + if(facet._type==='date_histogram'){ + facet.entries=_.map(facet.entries,function(entry){ + entry.term=newDate(entry.time).toDateString(); + returnentry; + }); + } + returnfacet; + });vartemplated=$.mustache(this.template,tmplData); - this.el.html(templated);varvalue=$target.attr('data-value');this.model.queryState.addTermFilter(fieldId,value);} -}); - -/* ========================================================== */if(q&&q.length&&q[0]==='?'){q=q.slice(1);} - while(e=r.exec(q)){});queryString+=items.join('&');returnqueryString; -} +};my.getNewHashForQueryString=function(queryParams){varqueryPart=my.composeQueryString(queryParams); - if(window.location.hash){});},1000);} -}

view.js

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

view.js

+
+
+
+
+
+ # +
+

jshint multistr:true

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

DataExplorer

- +(function($, my) { + + +
+
+
+
+ # +
+

DataExplorer

The primary view for the entire application. Usage:

-
 var myExplorer = new model.recline.DataExplorer({
   model: {{recline.Model.Dataset instance}}
@@ -13,18 +41,14 @@ var myExplorer = new model.recline.DataExplorer({
   views: {{page views}}
   config: {{config options -- see below}}
 });
-
+

Parameters

-

model: (required) Dataset instance.

-

el: (required) DOM element.

-

views: (optional) the views (Grid, Graph etc) for DataExplorer to show. This is an array of view hashes. If not provided just initialize a DataGrid with id 'grid'. Example:

-
 var views = [
   {
@@ -45,14 +69,15 @@ var views = [
 

config: Config options like:

-
  • readOnly: true/false (default: false) value indicating whether to -operate in read-only mode (hiding all editing options).
  • + operate in read-only mode (hiding all editing options).
-

NB: the element already being in the DOM is important for rendering of -FlotGraph subview.

my.DataExplorer = Backbone.View.extend({
+FlotGraph subview.

+
+
+
my.DataExplorer = Backbone.View.extend({
   template: ' \
   <div class="recline-data-explorer"> \
     <div class="alert-messages"></div> \
@@ -95,7 +120,19 @@ FlotGraph subview.

options.config); if (this.config.readOnly) { this.setReadOnly(); - }

Hash of 'page' views (i.e. those for whole page) keyed by page name

    if (options.views) {
+    }
+ + +
+
+
+
+ # +
+

Hash of 'page' views (i.e. those for whole page) keyed by page name

+
+
+
    if (options.views) {
       this.pageViews = options.views;
     } else {
       this.pageViews = [{
@@ -105,7 +142,19 @@ FlotGraph subview.

model: this.model }) }]; - }

this must be called after pageViews are created

    this.render();
+    }
+ + +
+
+
+
+ # +
+

this must be called after pageViews are created

+
+
+
    this.render();
 
     this.router = new Backbone.Router();
     this.setupRouting();
@@ -116,8 +165,20 @@ FlotGraph subview.

this.model.bind('query:done', function() { my.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'});

update navigation

        var qs = my.parseHashQueryString();
-        qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON());
+        my.notify('Data loaded', {category: 'success'});
+ + +
+
+
+
+ # +
+

update navigation

+
+
+
        var qs = my.parseHashQueryString();
+        qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
         var out = my.getNewHashForQueryString(qs);
         self.router.navigate(out);
       });
@@ -137,8 +198,20 @@ FlotGraph subview.

msg = 'There was an error querying the backend'; } my.notify(msg, {category: 'error', persist: true}); - });

retrieve basic data like fields etc -note this.model and dataset returned are the same

    this.model.fetch()
+      });
+ + +
+
+
+
+ # +
+

retrieve basic data like fields etc +note this.model and dataset returned are the same

+
+
+
    this.model.fetch()
       .done(function(dataset) {
         var queryState = my.parseHashQueryString().reclineQuery;
         if (queryState) {
@@ -163,7 +236,7 @@ note this.model and dataset returned are the same

Default route

    this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
+    var self = this;
+ + +
+
+
+
+ # +
+

Default route

+
+
+
    this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
       self.updateNav(self.pageViews[0].id, queryString);
     });
     $.each(this.pageViews, function(idx, view) {
@@ -197,11 +282,25 @@ note this.model and dataset returned are the same

show the specific page

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

show the specific page

+
+
+
    _.each(this.pageViews, function(view, idx) {
       if (view.id === pageName) {
         view.view.el.show();
+        view.view.trigger('view:show');
       } else {
         view.view.el.hide();
+        view.view.trigger('view:hide');
       }
     });
   },
@@ -237,8 +336,8 @@ note this.model and dataset returned are the same

we will use idx in list as there id ...

    tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+    var tmplData = $.extend(true, {}, this.model.toJSON());
+ + +
+
+
+
+ # +
+

we will use idx in list as there id ...

+
+
+
    tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
       filter.id = idx;
       return filter;
     });
@@ -331,10 +443,22 @@ note this.model and dataset returned are the same

are there actually any facets to show?

    if (this.model.get('filters').length > 0) {
+    this.el.html(out);
+ + +
+
+
+
+ # +
+

are there actually any facets to show?

+
+
+
    if (this.model.get('filters').length > 0) {
       this.el.show();
     } else {
       this.el.hide();
@@ -382,6 +506,9 @@ note this.model and dataset returned are the same

are there actually any facets to show?

    if (this.model.facets.length > 0) {
+    this.el.html(templated);
+ + +
+
+
+
+ # +
+

are there actually any facets to show?

+
+
+
    if (this.model.facets.length > 0) {
       this.el.show();
     } else {
       this.el.hide();
@@ -421,19 +569,53 @@ note this.model and dataset returned are the same

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) {
+  if (parsed === null) {
     return {};
   } else {
     return {
       path: parsed[1],
       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 {};
   }
@@ -446,13 +628,49 @@ note this.model and dataset returned are the same

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) {
@@ -460,35 +678,57 @@ note this.model and dataset returned are the same

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;
   }
-}
+};
 
 my.setHashQueryString = function(queryParams) {
   window.location.hash = my.getNewHashForQueryString(queryParams);
-}

notify

- +}; + + +
+
+
+
+ # +
+

notify

Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are:

-
  • category: warning (default), success, error
  • persist: if true alert is persistent, o/w hidden after 3s (default = false)
  • loader: if true show loading spinner
  • -
my.notify = function(message, options) {
-  if (!options) var options = {};
+
+    
+
+
my.notify = function(message, options) {
+  if (!options) options = {};
   var tmplData = _.extend({
     msg: message,
     category: 'warning'
     },
     options);
   var _template = ' \
-    <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
+    <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
       {{msg}} \
         {{#loader}} \
         <span class="notification-loader">&nbsp;</span> \
@@ -503,13 +743,29 @@ note this.model and dataset returned are the same

clearNotifications

- -

Clear all existing notifications

my.clearNotifications = function() {
+};
+ + +
+
+
+
+ # +
+

clearNotifications

+

Clear all existing notifications

+
+
+
my.clearNotifications = function() {
   var $notifications = $('.recline-data-explorer .alert-messages .alert');
   $notifications.remove();
-}
+};
 
 })(jQuery, recline.View);
 
-
\ No newline at end of file +
+ + +
+ + diff --git a/index.html b/index.html index e7b7d616..1db10c36 100644 --- a/index.html +++ b/index.html @@ -254,7 +254,7 @@ also easily write your own). Each view holds a pointer to a Dataset:

  • DataGrid: the data grid view.
  • FlotGraph: a simple graphing view using Flot.
  • -
  • Map: a map view using Leaflet.
  • +
  • Map: a map view using Leaflet.
  • There are additional views which do not display a whole dataset but which @@ -272,6 +272,7 @@ are useful:

  • DataExplorer View (plus common view code)
  • DataGrid View
  • Graph View (based on Flot)
  • +
  • Map View (based on Leaflet)
  • Backends

    @@ -320,6 +321,7 @@ However, it has been rewritten from the ground up using Backbone.

  • Underscore >= 1.0
  • JQuery Mustache
  • JQuery Flot >= 0.7: (Optional) for graphing
  • +
  • Leaflet >= 0.3.1: (Optional) for mapping
  • Bootstrap >= v2.0: (Optional) for CSS/JS
  • diff --git a/src/view-map.js b/src/view-map.js index d39aebed..e1705f1e 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -5,27 +5,29 @@ this.recline.View = this.recline.View || {}; (function($, my) { +// ## Map view for a Dataset using Leaflet mapping library. +// +// This view allows to plot gereferenced documents on a map. The location +// information can be provided either via a field with +// [GeoJSON](http://geojson.org) objects or two fields with latitude and +// longitude coordinates. +// +// Initialization arguments: +// +// * options: initial options. They must contain a model: +// +// { +// model: {recline.Model.Dataset} +// } +// +// * config: (optional) map configuration hash (not yet used) +// +// my.Map = Backbone.View.extend({ tagName: 'div', className: 'data-map-container', - latitudeFieldNames: ['lat','latitude'], - longitudeFieldNames: ['lon','longitude'], - geometryFieldNames: ['geom','the_geom','geometry','spatial','location'], - - //TODO: In case we want to change the default markers - /* - markerOptions: { - radius: 5, - color: 'grey', - fillColor: 'orange', - weight: 2, - opacity: 1, - fillOpacity: 1 - }, - */ - template: ' \
    \
    \ @@ -81,6 +83,13 @@ my.Map = Backbone.View.extend({
    \ ', + // These are the default field names that will be used if found. + // If not found, the user will need to define the fields via the editor. + latitudeFieldNames: ['lat','latitude'], + longitudeFieldNames: ['lon','longitude'], + geometryFieldNames: ['geom','the_geom','geometry','spatial','location'], + + // Define here events for UI elements events: { 'click .editor-update-map': 'onEditorSubmit', 'change .editor-field-type': 'onFieldTypeChange' @@ -91,6 +100,8 @@ my.Map = Backbone.View.extend({ var self = this; this.el = $(this.el); + + // Listen to changes in the fields this.model.bind('change', function() { self._setupGeometryField(); }); @@ -99,11 +110,13 @@ my.Map = Backbone.View.extend({ self._setupGeometryField() self.render() }); + + // Listen to changes in the documents this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); - // If the div is hidden, Leaflet needs to recalculate some sizes + // If the div was hidden, Leaflet needs to recalculate some sizes // to display properly this.bind('view:show',function(){ self.map.invalidateSize(); @@ -114,6 +127,9 @@ my.Map = Backbone.View.extend({ this.render(); }, + // Public: Adds the necessary elements to the page. + // + // Also sets up the editor fields and the map if necessary. render: function() { var self = this; @@ -123,7 +139,6 @@ my.Map = Backbone.View.extend({ $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); - // Setup editor fields if (this.geomReady && this.model.fields.length){ if (this._geomFieldName){ this._selectOption('editor-geom-field',this._geomFieldName); @@ -149,6 +164,15 @@ my.Map = Backbone.View.extend({ return this; }, + // Public: Redraws the features on the map according to the action provided + // + // Actions can be: + // + // * reset: Clear all features + // * add: Add one or n features (documents) + // * remove: Remove one or n features (documents) + // * refresh: Clear existing features and add all current documents + // redraw: function(action,doc){ var self = this; @@ -157,24 +181,27 @@ my.Map = Backbone.View.extend({ if (this.geomReady && this.mapReady){ if (action == 'reset'){ - // Clear all features this.features.clearLayers(); } else if (action == 'add' && doc){ - // Add one or n features this._add(doc); } else if (action == 'remove' && doc){ - // Remove one or n features this._remove(doc); } else if (action == 'refresh'){ - // Clear and rebuild all features this.features.clearLayers(); this._add(this.model.currentDocuments.models); } } }, - /* UI Event handlers */ + // + // 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){ e.preventDefault(); if ($('#editor-field-type-geom').attr('checked')){ @@ -191,6 +218,9 @@ my.Map = Backbone.View.extend({ return false; }, + // Public: Shows the relevant select lists depending on the location field + // type selected. + // onFieldTypeChange: function(e){ if (e.target.value == 'geom'){ $('.editor-field-type-geom').show(); @@ -201,6 +231,14 @@ my.Map = Backbone.View.extend({ } }, + // Private: Add one or n features to the map + // + // For each document passed, a GeoJSON geometry will be extracted and added + // to the features layer. If an exception is thrown, the process will be + // stopped and an error notification shown. + // + // Each feature will have a popup associated with all the document fields. + // _add: function(doc){ var self = this; @@ -237,6 +275,8 @@ my.Map = Backbone.View.extend({ }); }, + // Private: Remove one or n features to the map + // _remove: function(doc){ var self = this; @@ -253,6 +293,8 @@ my.Map = Backbone.View.extend({ }, + // Private: Return a GeoJSON geomtry extracted from the document fields + // _getGeometryFromDocument: function(doc){ if (this.geomReady){ if (this._geomFieldName){ @@ -272,11 +314,13 @@ my.Map = Backbone.View.extend({ } }, + // 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(){ var geomField, latField, lonField; - // Check if there is a field with GeoJSON geometries or alternatively, - // two fields with lat/lon values this._geomFieldName = this._checkField(this.geometryFieldNames); this._latFieldName = this._checkField(this.latitudeFieldNames); this._lonFieldName = this._checkField(this.longitudeFieldNames); @@ -284,6 +328,10 @@ my.Map = Backbone.View.extend({ this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); }, + // 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'); @@ -296,17 +344,20 @@ my.Map = Backbone.View.extend({ return null; }, + // Private: Sets up the Leaflet map control and the features layer. + // + // The map uses a base layer from [MapQuest](http://www.mapquest.com) based + // on [OpenStreetMap](http://openstreetmap.org). + // _setupMap: function(){ this.map = new L.Map(this.$map.get(0)); - // MapQuest OpenStreetMap base map var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of MapQuest '; var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'}); this.map.addLayer(bg); - // Layer to hold the features this.features = new L.GeoJSON(); this.features.on('featureparse', function (e) { if (e.properties && e.properties.popupContent){ @@ -324,6 +375,8 @@ my.Map = Backbone.View.extend({ this.mapReady = true; }, + // Private: Helper function to select an option from a select list + // _selectOption: function(id,value){ var options = $('.' + id + ' > select > option'); if (options){ From cb28668d8f57f357a52df3bd259ba8d36f60b7a8 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 11 Apr 2012 17:34:28 +0100 Subject: [PATCH 07/48] [make][xs] Add support for DOCCO_EXECUTABLE environ variable for non-standard docco installs --- make | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/make b/make index 4d60070a..c4553597 100755 --- a/make +++ b/make @@ -11,7 +11,9 @@ def cat(): def docs(): # build docs print("** Building docs") - cmd = 'docco src/model.js src/view.js src/view-grid.js src/view-flot-graph.js' + + docco_executable = os.environ.get('DOCCO_EXECUTABLE','docco') + cmd = '%s src/model.js src/view.js src/view-grid.js src/view-flot-graph.js src/view-map.js' % (docco_executable) os.system(cmd) if os.path.exists('/tmp/recline-docs'): shutil.rmtree('/tmp/recline-docs') @@ -19,7 +21,7 @@ def docs(): os.system('mkdir -p docs/backend') files = '%s/src/backend/*.js' % os.getcwd() dest = '%s/docs/backend' % os.getcwd() - os.system('cd /tmp/recline-docs && docco %s && mv docs/* %s' % (files, dest)) + os.system('cd /tmp/recline-docs && %s %s && mv docs/* %s' % (docco_executable,files, dest)) print("** Docs built ok") if __name__ == '__main__': From bd37d4b0451114a52aad631814739e87801192f4 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 11 Apr 2012 17:35:44 +0100 Subject: [PATCH 08/48] [build] Build recline.js with latest changes --- recline.js | 592 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 431 insertions(+), 161 deletions(-) diff --git a/recline.js b/recline.js index 11c3f779..9bfe37ec 100644 --- a/recline.js +++ b/recline.js @@ -247,13 +247,27 @@ my.Field = Backbone.Model.extend({ if ('0' in data) { throw new Error('Looks like you did not pass a proper hash with id to Field constructor'); } - if (this.attributes.label == null) { + if (this.attributes.label === null) { this.set({label: this.id}); } if (options) { this.renderer = options.renderer; this.deriver = options.deriver; } + if (!this.renderer) { + this.renderer = this.defaultRenderers[this.get('type')]; + } + }, + defaultRenderers: { + object: function(val, field, doc) { + return JSON.stringify(val); + }, + 'float': function(val, field, doc) { + var format = field.get('format'); + if (format === 'percentage') { + return val + '%'; + } + } } }); @@ -285,7 +299,7 @@ my.FieldList = Backbone.Collection.extend({ // * query: Query in ES Query DSL // * filter: See filters and Filtered Query // * fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html -// * facets: TODO - see http://www.elasticsearch.org/guide/reference/api/search/facets/ +// * facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/ // // Additions: // @@ -313,13 +327,13 @@ my.FieldList = Backbone.Collection.extend({ my.Query = Backbone.Model.extend({ defaults: function() { return { - size: 100 - , from: 0 - , facets: {} + size: 100, + from: 0, + facets: {}, // // , filter: {} - , filters: [] - } + filters: [] + }; }, // #### addTermFilter // @@ -365,6 +379,17 @@ my.Query = Backbone.Model.extend({ }; this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); + }, + addHistogramFacet: function(fieldId) { + var facets = this.get('facets'); + facets[fieldId] = { + date_histogram: { + field: fieldId, + interval: 'day' + } + }; + this.set({facets: facets}, {silent: true}); + this.trigger('facet:add', this); } }); @@ -415,7 +440,7 @@ my.Facet = Backbone.Model.extend({ other: 0, missing: 0, terms: [] - } + }; } }); @@ -435,8 +460,8 @@ my.backends = {}; var util = function() { var templates = { - transformActions: '
  • Global transform...
  • ' - , cellEditor: ' \ + transformActions: '
  • Global transform...
  • ', + cellEditor: ' \ \ \ - ' - , editPreview: ' \ + ', + editPreview: ' \
    \ \ \ @@ -496,7 +521,7 @@ var util = function() { function registerEmitter() { var Emitter = function(obj) { this.emit = function(obj, channel) { - if (!channel) var channel = 'data'; + if (!channel) channel = 'data'; this.trigger(channel, obj); }; }; @@ -513,7 +538,7 @@ var util = function() { 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" - } + }; window.addEventListener("keyup", function(e) { var pressed = shortcuts[e.keyCode]; if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); @@ -559,10 +584,11 @@ var util = function() { if ( !options ) options = {data: {}}; if ( !options.data ) options = {data: options}; var html = $.mustache( templates[template], options.data ); + var targetDom = null; if (target instanceof jQuery) { - var targetDom = target; + targetDom = target; } else { - var targetDom = $( "." + target + ":first" ); + targetDom = $( "." + target + ":first" ); } if( options.append ) { targetDom.append( html ); @@ -665,10 +691,10 @@ my.FlotGraph = Backbone.View.extend({ ', events: { - 'change form select': 'onEditorSubmit' - , 'click .editor-add': 'addSeries' - , 'click .action-remove-series': 'removeSeries' - , 'click .action-toggle-help': 'toggleHelp' + 'change form select': 'onEditorSubmit', + 'click .editor-add': 'addSeries', + 'click .action-remove-series': 'removeSeries', + 'click .action-toggle-help': 'toggleHelp' }, initialize: function(options, config) { @@ -714,12 +740,12 @@ my.FlotGraph = Backbone.View.extend({ var series = this.$series.map(function () { return $(this).val(); }); - this.chartConfig.series = $.makeArray(series) + this.chartConfig.series = $.makeArray(series); this.chartConfig.group = this.el.find('.editor-group select').val(); this.chartConfig.graphType = this.el.find('.editor-type select').val(); // update navigation var qs = my.parseHashQueryString(); - qs['graph'] = JSON.stringify(this.chartConfig); + qs.graph = JSON.stringify(this.chartConfig); my.setHashQueryString(qs); this.redraw(); }, @@ -732,8 +758,8 @@ my.FlotGraph = Backbone.View.extend({ // 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.currentDocuments.length == 0)) { - return + if ((!areWeVisible || this.model.currentDocuments.length === 0)) { + return; } var series = this.createSeries(); var options = this.getGraphOptions(this.chartConfig.graphType); @@ -766,7 +792,7 @@ my.FlotGraph = Backbone.View.extend({ } } return val; - } + }; // TODO: we should really use tickFormatter and 1 interval ticks if (and // only if) x-axis values are non-numeric // However, that is non-trivial to work out from a dataset (datasets may @@ -776,21 +802,21 @@ my.FlotGraph = Backbone.View.extend({ series: { lines: { show: true } } - } - , points: { + }, + points: { series: { points: { show: true } }, grid: { hoverable: true, clickable: true } - } - , 'lines-and-points': { + }, + 'lines-and-points': { series: { points: { show: true }, lines: { show: true } }, grid: { hoverable: true, clickable: true } - } - , bars: { + }, + bars: { series: { lines: {show: false}, bars: { @@ -810,7 +836,7 @@ my.FlotGraph = Backbone.View.extend({ max: self.model.currentDocuments.length - 0.5 } } - } + }; return options[typeId]; }, @@ -964,10 +990,10 @@ my.DataGrid = Backbone.View.extend({ }, events: { - 'click .column-header-menu': 'onColumnHeaderClick' - , 'click .row-header-menu': 'onRowHeaderClick' - , 'click .root-header-menu': 'onRootHeaderClick' - , 'click .data-table-menu li a': 'onMenuClick' + 'click .column-header-menu': 'onColumnHeaderClick', + 'click .row-header-menu': 'onRowHeaderClick', + 'click .root-header-menu': 'onRootHeaderClick', + 'click .data-table-menu li a': 'onMenuClick' }, // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). @@ -1006,33 +1032,35 @@ my.DataGrid = Backbone.View.extend({ var self = this; e.preventDefault(); var actions = { - bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); }, facet: function() { self.model.queryState.addFacet(self.state.currentColumn); }, + facet_histogram: function() { + self.model.queryState.addHistogramFacet(self.state.currentColumn); + }, filter: function() { self.model.queryState.addTermFilter(self.state.currentColumn, ''); }, - transform: function() { self.showTransformDialog('transform') }, - sortAsc: function() { self.setColumnSort('asc') }, - sortDesc: function() { self.setColumnSort('desc') }, - hideColumn: function() { self.hideColumn() }, - showColumn: function() { self.showColumn(e) }, + transform: function() { self.showTransformDialog('transform'); }, + sortAsc: function() { self.setColumnSort('asc'); }, + sortDesc: function() { self.setColumnSort('desc'); }, + hideColumn: function() { self.hideColumn(); }, + showColumn: function() { self.showColumn(e); }, deleteRow: function() { var doc = _.find(self.model.currentDocuments.models, function(doc) { // important this is == as the currentRow will be string (as comes // from DOM) while id may be int - return doc.id == self.state.currentRow + return doc.id == self.state.currentRow; }); doc.destroy().then(function() { self.model.currentDocuments.remove(doc); my.notify("Row deleted successfully"); - }) - .fail(function(err) { - my.notify("Errorz! " + err) - }) + }).fail(function(err) { + my.notify("Errorz! " + err); + }); } - } + }; actions[$(e.target).attr('data-action')](); }, @@ -1048,7 +1076,7 @@ my.DataGrid = Backbone.View.extend({ $el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -1062,7 +1090,7 @@ my.DataGrid = Backbone.View.extend({ $el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -1103,7 +1131,8 @@ my.DataGrid = Backbone.View.extend({
    \ \
    \
    \
    \ ', + // These are the default field names that will be used if found. + // If not found, the user will need to define the fields via the editor. + latitudeFieldNames: ['lat','latitude'], + longitudeFieldNames: ['lon','longitude'], + geometryFieldNames: ['geom','the_geom','geometry','spatial','location'], + + // Define here events for UI elements + events: { + 'click .editor-update-map': 'onEditorSubmit', + 'change .editor-field-type': 'onFieldTypeChange' + }, + + initialize: function(options, config) { var self = this; this.el = $(this.el); - this.render(); + + // Listen to changes in the fields this.model.bind('change', function() { self._setupGeometryField(); }); + this.model.fields.bind('add', this.render); + this.model.fields.bind('reset', function(){ + self._setupGeometryField() + self.render() + }); + + // Listen to changes in the documents + this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); + this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); + this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); + + // If the div was hidden, Leaflet needs to recalculate some sizes + // to display properly + this.bind('view:show',function(){ + self.map.invalidateSize(); + }); this.mapReady = false; + + this.render(); }, + // Public: Adds the necessary elements to the page. + // + // Also sets up the editor fields and the map if necessary. render: function() { var self = this; htmls = $.mustache(this.template, this.model.toTemplateJSON()); + $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); + if (this.geomReady && this.model.fields.length){ + if (this._geomFieldName){ + this._selectOption('editor-geom-field',this._geomFieldName); + $('#editor-field-type-geom').attr('checked','checked').change(); + } else{ + this._selectOption('editor-lon-field',this._lonFieldName); + this._selectOption('editor-lat-field',this._latFieldName); + $('#editor-field-type-latlon').attr('checked','checked').change(); + } + } + this.model.bind('query:done', function() { if (!self.geomReady){ self._setupGeometryField(); @@ -1319,41 +1447,143 @@ my.Map = Backbone.View.extend({ if (!self.mapReady){ self._setupMap(); } - self.redraw() + self.redraw(); }); return this; }, - redraw: function(){ + // Public: Redraws the features on the map according to the action provided + // + // Actions can be: + // + // * reset: Clear all features + // * add: Add one or n features (documents) + // * remove: Remove one or n features (documents) + // * refresh: Clear existing features and add all current documents + // + redraw: function(action,doc){ var self = this; - if (this.geomReady){ - if (this.model.currentDocuments.length > 0){ + action = action || 'refresh'; + + if (this.geomReady && this.mapReady){ + if (action == 'reset'){ this.features.clearLayers(); - var bounds = new L.LatLngBounds(); - - this.model.currentDocuments.forEach(function(doc){ - var feature = self._getGeometryFromDocument(doc); - if (feature){ - // Build popup contents - // TODO: mustache? - html = '' - for (key in doc.attributes){ - html += '
    ' + key + ': '+ doc.attributes[key] + '
    ' - } - feature.properties = {popupContent: html}; - - self.features.addGeoJSON(feature); - - // TODO: bounds and center map - } - }); + } else if (action == 'add' && doc){ + this._add(doc); + } else if (action == 'remove' && doc){ + this._remove(doc); + } else if (action == 'refresh'){ + this.features.clearLayers(); + this._add(this.model.currentDocuments.models); } } }, + // + // 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){ + e.preventDefault(); + if ($('#editor-field-type-geom').attr('checked')){ + this._geomFieldName = $('.editor-geom-field > select > option:selected').val(); + this._latFieldName = this._lonFieldName = false; + } else { + this._geomFieldName = false; + this._latFieldName = $('.editor-lat-field > select > option:selected').val(); + this._lonFieldName = $('.editor-lon-field > select > option:selected').val(); + } + this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); + this.redraw(); + + return false; + }, + + // Public: Shows the relevant select lists depending on the location field + // type selected. + // + onFieldTypeChange: function(e){ + if (e.target.value == 'geom'){ + $('.editor-field-type-geom').show(); + $('.editor-field-type-latlon').hide(); + } else { + $('.editor-field-type-geom').hide(); + $('.editor-field-type-latlon').show(); + } + }, + + // Private: Add one or n features to the map + // + // For each document passed, a GeoJSON geometry will be extracted and added + // to the features layer. If an exception is thrown, the process will be + // stopped and an error notification shown. + // + // Each feature will have a popup associated with all the document fields. + // + _add: function(doc){ + + var self = this; + + if (!(doc instanceof Array)) doc = [doc]; + + doc.forEach(function(doc){ + var feature = self._getGeometryFromDocument(doc); + if (feature instanceof Object){ + // Build popup contents + // TODO: mustache? + html = '' + for (key in doc.attributes){ + html += '
    ' + key + ': '+ doc.attributes[key] + '
    ' + } + feature.properties = {popupContent: html}; + + // Add a reference to the model id, which will allow us to + // link this Leaflet layer to a Recline doc + feature.properties.cid = doc.cid; + + try { + self.features.addGeoJSON(feature); + } catch (except) { + var msg = 'Wrong geometry value'; + if (except.message) msg += ' (' + except.message + ')'; + my.notify(msg,{category:'error'}); + _.breakLoop(); + } + } else { + my.notify('Wrong geometry value',{category:'error'}); + _.breakLoop(); + } + }); + }, + + // Private: Remove one or n features to the map + // + _remove: function(doc){ + + var self = this; + + if (!(doc instanceof Array)) doc = [doc]; + + doc.forEach(function(doc){ + for (key in self.features._layers){ + if (self.features._layers[key].cid == doc.cid){ + self.features.removeLayer(self.features._layers[key]); + } + } + }); + + }, + + // Private: Return a GeoJSON geomtry extracted from the document fields + // _getGeometryFromDocument: function(doc){ if (this.geomReady){ if (this._geomFieldName){ @@ -1365,59 +1595,87 @@ my.Map = Backbone.View.extend({ type: 'Point', coordinates: [ doc.attributes[this._lonFieldName], - doc.attributes[this._latFieldName], + doc.attributes[this._latFieldName] ] - } + }; } return null; } }, + // 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(){ var geomField, latField, lonField; - // Check if there is a field with GeoJSON geometries or alternatively, - // two fields with lat/lon values this._geomFieldName = this._checkField(this.geometryFieldNames); this._latFieldName = this._checkField(this.latitudeFieldNames); this._lonFieldName = this._checkField(this.longitudeFieldNames); - // TODO: Allow users to choose the fields - this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); }, + // 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'); for (var i = 0; i < fieldNames.length; i++){ - field = this.model.fields.get(fieldNames[i]); - if (field) return field.id; + for (var j = 0; j < modelFieldNames.length; j++){ + if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase()) + return modelFieldNames[j]; + } } return null; }, + // Private: Sets up the Leaflet map control and the features layer. + // + // The map uses a base layer from [MapQuest](http://www.mapquest.com) based + // on [OpenStreetMap](http://openstreetmap.org). + // _setupMap: function(){ this.map = new L.Map(this.$map.get(0)); - // MapQuest OpenStreetMap base map var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of MapQuest'; var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'}); this.map.addLayer(bg); - // Layer to hold the features this.features = new L.GeoJSON(); this.features.on('featureparse', function (e) { if (e.properties && e.properties.popupContent){ e.layer.bindPopup(e.properties.popupContent); } + if (e.properties && e.properties.cid){ + e.layer.cid = e.properties.cid; + } + }); this.map.addLayer(this.features); this.map.setView(new L.LatLng(0, 0), 2); this.mapReady = true; + }, + + // 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){ + if (this.value == value) { + $(this).attr('selected','selected'); + return false; + } + }); + } } }); @@ -1558,8 +1816,8 @@ my.ColumnTransform = Backbone.View.extend({ ', events: { - 'click .okButton': 'onSubmit' - , 'keydown .expression-preview-code': 'onEditorKeydown' + 'click .okButton': 'onSubmit', + 'keydown .expression-preview-code': 'onEditorKeydown' }, initialize: function() { @@ -1569,7 +1827,7 @@ my.ColumnTransform = Backbone.View.extend({ render: function() { var htmls = $.mustache(this.template, {name: this.state.currentColumn} - ) + ); this.el.html(htmls); // Put in the basic (identity) transform script // TODO: put this into the template? @@ -1607,7 +1865,7 @@ my.ColumnTransform = Backbone.View.extend({ _.each(toUpdate, function(editedDoc) { var realDoc = self.model.currentDocuments.get(editedDoc.id); realDoc.set(editedDoc); - realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate) + realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate); }); }, @@ -1757,7 +2015,7 @@ my.DataExplorer = Backbone.View.extend({ my.notify('Data loaded', {category: 'success'}); // update navigation var qs = my.parseHashQueryString(); - qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON()); + qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON()); var out = my.getNewHashForQueryString(qs); self.router.navigate(out); }); @@ -1806,7 +2064,7 @@ my.DataExplorer = Backbone.View.extend({ $(this.el).html(template); var $dataViewContainer = this.el.find('.data-view-container'); _.each(this.pageViews, function(view, pageName) { - $dataViewContainer.append(view.view.el) + $dataViewContainer.append(view.view.el); }); var queryEditor = new my.QueryEditor({ model: this.model.queryState @@ -1847,8 +2105,10 @@ my.DataExplorer = Backbone.View.extend({ _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { view.view.el.show(); + view.view.trigger('view:show'); } else { view.view.el.hide(); + view.view.trigger('view:hide'); } }); }, @@ -1884,8 +2144,8 @@ my.QueryEditor = Backbone.View.extend({ ', events: { - 'submit form': 'onFormSubmit' - , 'click .action-pagination-update': 'onPaginationUpdate' + 'submit form': 'onFormSubmit', + 'click .action-pagination-update': 'onPaginationUpdate' }, initialize: function() { @@ -1904,10 +2164,11 @@ my.QueryEditor = Backbone.View.extend({ onPaginationUpdate: function(e) { e.preventDefault(); var $el = $(e.target); + var newFrom = 0; if ($el.parent().hasClass('prev')) { - var newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); + newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); } else { - var newFrom = this.model.get('from') + this.model.get('size'); + newFrom = this.model.get('from') + this.model.get('size'); } this.model.set({from: newFrom}); }, @@ -1980,7 +2241,7 @@ my.FilterEditor = Backbone.View.extend({ fieldId: fieldId, label: fieldId, value: filter.term[fieldId] - } + }; }); var out = $.mustache(this.template, tmplData); this.el.html(out); @@ -2033,6 +2294,9 @@ my.FacetViewer = Backbone.View.extend({ {{#terms}} \
  • {{term}} ({{count}})
  • \ {{/terms}} \ + {{#entries}} \ +
  • {{term}} ({{count}})
  • \ + {{/entries}} \ \ \ {{/facets}} \ @@ -2055,6 +2319,15 @@ my.FacetViewer = Backbone.View.extend({ facets: this.model.facets.toJSON(), fields: this.model.fields.toJSON() }; + tmplData.facets = _.map(tmplData.facets, function(facet) { + if (facet._type === 'date_histogram') { + facet.entries = _.map(facet.entries, function(entry) { + entry.term = new Date(entry.time).toDateString(); + return entry; + }); + } + return facet; + }); var templated = $.mustache(this.template, tmplData); this.el.html(templated); // are there actually any facets to show? @@ -2084,15 +2357,15 @@ 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) { + if (parsed === null) { return {}; } else { return { path: parsed[1], query: parsed[2] || '' - } + }; } -} +}; // Parse a URL query string (?xyz=abc...) into a dictionary. my.parseQueryString = function(q) { @@ -2113,13 +2386,13 @@ my.parseQueryString = function(q) { urlParams[d(e[1])] = d(e[2]); } return urlParams; -} +}; // 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) { @@ -2130,7 +2403,7 @@ my.composeQueryString = function(queryParams) { }); queryString += items.join('&'); return queryString; -} +}; my.getNewHashForQueryString = function(queryParams) { var queryPart = my.composeQueryString(queryParams); @@ -2140,11 +2413,11 @@ my.getNewHashForQueryString = function(queryParams) { } else { return queryPart; } -} +}; my.setHashQueryString = function(queryParams) { window.location.hash = my.getNewHashForQueryString(queryParams); -} +}; // ## notify // @@ -2154,7 +2427,7 @@ my.setHashQueryString = function(queryParams) { // * persist: if true alert is persistent, o/w hidden after 3s (default = false) // * loader: if true show loading spinner my.notify = function(message, options) { - if (!options) var options = {}; + if (!options) options = {}; var tmplData = _.extend({ msg: message, category: 'warning' @@ -2176,7 +2449,7 @@ my.notify = function(message, options) { }); }, 1000); } -} +}; // ## clearNotifications // @@ -2184,7 +2457,7 @@ my.notify = function(message, options) { my.clearNotifications = function() { var $notifications = $('.recline-data-explorer .alert-messages .alert'); $notifications.remove(); -} +}; })(jQuery, recline.View); @@ -2202,7 +2475,7 @@ this.recline.Backend = this.recline.Backend || {}; // Override Backbone.sync to hand off to sync function in relevant backend Backbone.sync = function(method, model, options) { return model.backend.sync(method, model, options); - } + }; // ## recline.Backend.Base // @@ -2351,14 +2624,14 @@ this.recline.Backend = this.recline.Backend || {}; var self = this; var base = this.get('dataproxy_url'); var data = { - url: dataset.get('url') - , 'max-results': queryObj.size - , type: dataset.get('format') + url: dataset.get('url'), + 'max-results': queryObj.size, + type: dataset.get('format') }; var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' + url: base, + data: data, + dataType: 'jsonp' }); var dfd = $.Deferred(); this._wrapInTimeout(jqxhr).done(function(results) { @@ -2449,37 +2722,33 @@ this.recline.Backend = this.recline.Backend || {}; } }, _normalizeQuery: function(queryObj) { - if (queryObj.toJSON) { - var out = queryObj.toJSON(); - } else { - var out = _.extend({}, queryObj); - } - if (out.q != undefined && out.q.trim() === '') { + var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); + if (out.q !== undefined && out.q.trim() === '') { delete out.q; } if (!out.q) { out.query = { match_all: {} - } + }; } else { out.query = { query_string: { query: out.q } - } + }; delete out.q; } // now do filters (note the *plural*) if (out.filters && out.filters.length) { if (!out.filter) { - out.filter = {} + out.filter = {}; } if (!out.filter.and) { out.filter.and = []; } out.filter.and = out.filter.and.concat(out.filters); } - if (out.filters != undefined) { + if (out.filters !== undefined) { delete out.filters; } return out; @@ -2497,10 +2766,10 @@ this.recline.Backend = this.recline.Backend || {}; // TODO: fail case jqxhr.done(function(results) { _.each(results.hits.hits, function(hit) { - if (!'id' in hit._source && hit._id) { + if (!('id' in hit._source) && hit._id) { hit._source.id = hit._id; } - }) + }); if (results.facets) { results.hits.facets = results.facets; } @@ -2538,12 +2807,12 @@ this.recline.Backend = this.recline.Backend || {}; return url; } else { // https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0 - var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/ + var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/; var matches = url.match(regex); if (matches) { var key = matches[1]; var worksheet = 1; - var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json' + var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'; return out; } else { alert('Failed to extract gdocs key from ' + url); @@ -2567,8 +2836,9 @@ this.recline.Backend = this.recline.Backend || {}; // cache data onto dataset (we have loaded whole gdoc it seems!) model._dataCache = result.data; dfd.resolve(model); - }) - return dfd.promise(); } + }); + return dfd.promise(); + } }, query: function(dataset, queryObj) { @@ -2579,7 +2849,9 @@ this.recline.Backend = this.recline.Backend || {}; // TODO: factor this out as a common method with other backends var objs = _.map(dataset._dataCache, function (d) { var obj = {}; - _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; }) + _.each(_.zip(fields, d), function (x) { + obj[x[0]] = x[1]; + }); return obj; }); dfd.resolve(this._docsToQueryResult(objs)); @@ -2616,8 +2888,8 @@ this.recline.Backend = this.recline.Backend || {}; if (gdocsSpreadsheet.feed.entry.length > 0) { for (var k in gdocsSpreadsheet.feed.entry[0]) { if (k.substr(0, 3) == 'gsx') { - var col = k.substr(4) - results.field.push(col); + var col = k.substr(4); + results.field.push(col); } } } @@ -2667,7 +2939,7 @@ this.recline.Backend = this.recline.Backend || {}; }; reader.onerror = function (e) { alert('Failed to load file. Code: ' + e.target.error.code); - } + }; reader.readAsText(file); }; @@ -2685,7 +2957,7 @@ this.recline.Backend = this.recline.Backend || {}; }); var dataset = recline.Backend.createDataset(data, fields); return dataset; - } + }; // Converts a Comma Separated Values string into an array of arrays. // Each line in the CSV becomes an array. @@ -2829,7 +3101,7 @@ this.recline.Backend = this.recline.Backend || {}; // If not defined (or id not provided) id will be autogenerated. my.createDataset = function(data, fields, metadata) { if (!metadata) { - var metadata = {}; + metadata = {}; } if (!metadata.id) { metadata.id = String(Math.floor(Math.random() * 100000000) + 1); @@ -2892,8 +3164,8 @@ this.recline.Backend = this.recline.Backend || {}; }, sync: function(method, model, options) { var self = this; + var dfd = $.Deferred(); if (method === "read") { - var dfd = $.Deferred(); if (model.__type__ == 'Dataset') { var rawDataset = this.datasets[model.id]; model.set(rawDataset.metadata); @@ -2903,7 +3175,6 @@ this.recline.Backend = this.recline.Backend || {}; } return dfd.promise(); } else if (method === 'update') { - var dfd = $.Deferred(); if (model.__type__ == 'Document') { _.each(self.datasets[model.dataset.id].documents, function(doc, idx) { if(doc.id === model.id) { @@ -2914,7 +3185,6 @@ this.recline.Backend = this.recline.Backend || {}; } return dfd.promise(); } else if (method === 'delete') { - var dfd = $.Deferred(); if (model.__type__ == 'Document') { var rawDataset = self.datasets[model.dataset.id]; var newdocs = _.reject(rawDataset.documents, function(doc) { From 45c05a33342803c260e9743d2099ef9cd2b1f04c Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 11 Apr 2012 18:47:15 +0100 Subject: [PATCH 09/48] [view/map] Ignore empty fields and replace non-crossbrowser commands --- src/view-map.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/view-map.js b/src/view-map.js index e1705f1e..cf1d5ae8 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -239,15 +239,18 @@ my.Map = Backbone.View.extend({ // // Each feature will have a popup associated with all the document fields. // - _add: function(doc){ + _add: function(docs){ var self = this; - if (!(doc instanceof Array)) doc = [doc]; + if (!(docs instanceof Array)) docs = [docs]; - doc.forEach(function(doc){ + _.every(docs,function(doc){ var feature = self._getGeometryFromDocument(doc); - if (feature instanceof Object){ + if (typeof feature === 'undefined'){ + // Empty field + return true; + } else if (feature instanceof Object){ // Build popup contents // TODO: mustache? html = '' @@ -266,24 +269,25 @@ my.Map = Backbone.View.extend({ var msg = 'Wrong geometry value'; if (except.message) msg += ' (' + except.message + ')'; my.notify(msg,{category:'error'}); - _.breakLoop(); + return false; } } else { my.notify('Wrong geometry value',{category:'error'}); - _.breakLoop(); + return false; } + return true; }); }, // Private: Remove one or n features to the map // - _remove: function(doc){ + _remove: function(docs){ var self = this; - if (!(doc instanceof Array)) doc = [doc]; + if (!(docs instanceof Array)) docs = [docs]; - doc.forEach(function(doc){ + _.each(doc,function(doc){ for (key in self.features._layers){ if (self.features._layers[key].cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); From 521322955a053d74d23dff769450946ee56b4ebb Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 11 Apr 2012 19:28:43 +0100 Subject: [PATCH 10/48] [build][xs]: build recline.js with latest fixes to map view. --- recline.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/recline.js b/recline.js index 9bfe37ec..2871c6df 100644 --- a/recline.js +++ b/recline.js @@ -1528,15 +1528,18 @@ my.Map = Backbone.View.extend({ // // Each feature will have a popup associated with all the document fields. // - _add: function(doc){ + _add: function(docs){ var self = this; - if (!(doc instanceof Array)) doc = [doc]; + if (!(docs instanceof Array)) docs = [docs]; - doc.forEach(function(doc){ + _.every(docs,function(doc){ var feature = self._getGeometryFromDocument(doc); - if (feature instanceof Object){ + if (typeof feature === 'undefined'){ + // Empty field + return true; + } else if (feature instanceof Object){ // Build popup contents // TODO: mustache? html = '' @@ -1555,24 +1558,25 @@ my.Map = Backbone.View.extend({ var msg = 'Wrong geometry value'; if (except.message) msg += ' (' + except.message + ')'; my.notify(msg,{category:'error'}); - _.breakLoop(); + return false; } } else { my.notify('Wrong geometry value',{category:'error'}); - _.breakLoop(); + return false; } + return true; }); }, // Private: Remove one or n features to the map // - _remove: function(doc){ + _remove: function(docs){ var self = this; - if (!(doc instanceof Array)) doc = [doc]; + if (!(docs instanceof Array)) docs = [docs]; - doc.forEach(function(doc){ + _.each(doc,function(doc){ for (key in self.features._layers){ if (self.features._layers[key].cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); From 9bffad30ad1265081656860cf2030421925801af Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 12 Apr 2012 08:31:01 +0100 Subject: [PATCH 11/48] [view/map][xs] Fix typo --- src/view-map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view-map.js b/src/view-map.js index cf1d5ae8..93f9d039 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -287,7 +287,7 @@ my.Map = Backbone.View.extend({ if (!(docs instanceof Array)) docs = [docs]; - _.each(doc,function(doc){ + _.each(docs,function(doc){ for (key in self.features._layers){ if (self.features._layers[key].cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); From 3f509b46dc262246dca4ea5869460529c2899969 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 12 Apr 2012 22:23:14 +0100 Subject: [PATCH 12/48] [vendor][xs]: move leaflet vendor libs under version directory (0.3.1). --- app/index.html | 6 +++--- vendor/leaflet/{ => 0.3.1}/images/layers.png | Bin vendor/leaflet/{ => 0.3.1}/images/marker-shadow.png | Bin vendor/leaflet/{ => 0.3.1}/images/marker.png | Bin vendor/leaflet/{ => 0.3.1}/images/popup-close.png | Bin vendor/leaflet/{ => 0.3.1}/images/zoom-in.png | Bin vendor/leaflet/{ => 0.3.1}/images/zoom-out.png | Bin vendor/leaflet/{ => 0.3.1}/leaflet.css | 0 vendor/leaflet/{ => 0.3.1}/leaflet.ie.css | 0 vendor/leaflet/{ => 0.3.1}/leaflet.js | 0 10 files changed, 3 insertions(+), 3 deletions(-) rename vendor/leaflet/{ => 0.3.1}/images/layers.png (100%) rename vendor/leaflet/{ => 0.3.1}/images/marker-shadow.png (100%) rename vendor/leaflet/{ => 0.3.1}/images/marker.png (100%) rename vendor/leaflet/{ => 0.3.1}/images/popup-close.png (100%) rename vendor/leaflet/{ => 0.3.1}/images/zoom-in.png (100%) rename vendor/leaflet/{ => 0.3.1}/images/zoom-out.png (100%) rename vendor/leaflet/{ => 0.3.1}/leaflet.css (100%) rename vendor/leaflet/{ => 0.3.1}/leaflet.ie.css (100%) rename vendor/leaflet/{ => 0.3.1}/leaflet.js (100%) diff --git a/app/index.html b/app/index.html index 1ff4696b..e3c7ca89 100644 --- a/app/index.html +++ b/app/index.html @@ -16,9 +16,9 @@ - + @@ -29,7 +29,7 @@ - + - + diff --git a/app/index.html b/app/index.html index fb2a1c56..0da14de7 100644 --- a/app/index.html +++ b/app/index.html @@ -12,7 +12,7 @@ - + diff --git a/css/graph-flot.css b/css/graph.css similarity index 100% rename from css/graph-flot.css rename to css/graph.css From 8097bd479166c1b2ad22407e4ac6c80b29873412 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 16:08:33 +0100 Subject: [PATCH 28/48] [#81,css,view/graph][xs]: rename parent container to recline-graph-container. --- css/graph.css | 20 ++++++++++---------- src/view-graph.js | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/css/graph.css b/css/graph.css index d50f11e1..f224603f 100644 --- a/css/graph.css +++ b/css/graph.css @@ -1,14 +1,14 @@ -.data-graph-container .graph { +.recline-graph-container .graph { height: 500px; margin-right: 200px; } -.data-graph-container .legend table { +.recline-graph-container .legend table { width: auto; margin-bottom: 0; } -.data-graph-container .legend td { +.recline-graph-container .legend td { padding: 5px; line-height: 13px; } @@ -17,34 +17,34 @@ * Editor *********************************************************/ -.data-graph-container .editor { +.recline-graph-container .editor { float: right; width: 200px; padding-left: 0px; } -.data-graph-container .editor-info { +.recline-graph-container .editor-info { padding-left: 4px; } -.data-graph-container .editor-info { +.recline-graph-container .editor-info { cursor: pointer; } -.data-graph-container .editor form { +.recline-graph-container .editor form { padding-left: 4px; } -.data-graph-container .editor select { +.recline-graph-container .editor select { width: 100%; } -.data-graph-container .editor-info { +.recline-graph-container .editor-info { border-bottom: 1px solid #ddd; margin-bottom: 10px; } -.data-graph-container .editor-hide-info p { +.recline-graph-container .editor-hide-info p { display: none; } diff --git a/src/view-graph.js b/src/view-graph.js index 4b3a9d23..9e54d213 100644 --- a/src/view-graph.js +++ b/src/view-graph.js @@ -23,7 +23,7 @@ this.recline.View = this.recline.View || {}; my.Graph = Backbone.View.extend({ tagName: "div", - className: "data-graph-container", + className: "recline-graph-container", template: ' \
    \ From 39a6c1eb7461fdde8c201a8d454432aee7569705 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 16:35:33 +0100 Subject: [PATCH 29/48] [refactor][xs]: rename DataGrid to Grid for simplicity and consistency with file name. --- app/js/app.js | 2 +- index.html | 4 ++-- src/view-grid.js | 14 +++++++------- src/view.js | 6 +++--- test/view-grid.test.js | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/js/app.js b/app/js/app.js index bdf612f9..807b3c63 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -51,7 +51,7 @@ function standardViews(dataset) { { id: 'grid', label: 'Grid', - view: new recline.View.DataGrid({ + view: new recline.View.Grid({ model: dataset }) }, diff --git a/index.html b/index.html index 7eb729cb..30c80bf8 100644 --- a/index.html +++ b/index.html @@ -251,7 +251,7 @@ also easily write your own). Each view holds a pointer to a Dataset:

    • DataExplorer: the parent view which manages the overall app and sets up sub views.
    • -
    • DataGrid: the data grid view.
    • +
    • Grid: the data grid view.
    • Graph: a simple graphing view using Flot.
    • Map: a map view using Leaflet.
    • @@ -270,7 +270,7 @@ are useful:

      diff --git a/src/view-grid.js b/src/view-grid.js index 02082455..cb959f18 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -4,12 +4,12 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { -// ## DataGrid +// ## (Data) Grid Dataset View // // Provides a tabular view on a Dataset. // // Initialize it with a `recline.Model.Dataset`. -my.DataGrid = Backbone.View.extend({ +my.Grid = Backbone.View.extend({ tagName: "div", className: "recline-grid-container", @@ -211,7 +211,7 @@ my.DataGrid = Backbone.View.extend({ this.model.currentDocuments.forEach(function(doc) { var tr = $('
    '); self.el.find('tbody').append(tr); - var newView = new my.DataGridRow({ + var newView = new my.GridRow({ model: doc, el: tr, fields: self.fields @@ -223,22 +223,22 @@ my.DataGrid = Backbone.View.extend({ } }); -// ## DataGridRow View for rendering an individual document. +// ## GridRow View for rendering an individual document. // // Since we want this to update in place it is up to creator to provider the element to attach to. // -// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid. +// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid. // // Example: // //
    -// var row = new DataGridRow({
    +// var row = new GridRow({
     //   model: dataset-document,
     //     el: dom-element,
     //     fields: mydatasets.fields // a FieldList object
     //   });
     // 
    -my.DataGridRow = Backbone.View.extend({ +my.GridRow = Backbone.View.extend({ initialize: function(initData) { _.bindAll(this, 'render'); this._fields = initData.fields; diff --git a/src/view.js b/src/view.js index 368bc95b..b78e1901 100644 --- a/src/view.js +++ b/src/view.js @@ -38,14 +38,14 @@ this.recline.View = this.recline.View || {}; // // **views**: (optional) the dataset views (Grid, Graph etc) for // DataExplorer to show. This is an array of view hashes. If not provided -// just initialize a DataGrid with id 'grid'. Example: +// just initialize a Grid with id 'grid'. Example: // //
     // var views = [
     //   {
     //     id: 'grid', // used for routing
     //     label: 'Grid', // used for view switcher
    -//     view: new recline.View.DataGrid({
    +//     view: new recline.View.Grid({
     //       model: dataset
     //     })
     //   },
    @@ -107,7 +107,7 @@ my.DataExplorer = Backbone.View.extend({
           this.pageViews = [{
             id: 'grid',
             label: 'Grid',
    -        view: new my.DataGrid({
    +        view: new my.Grid({
                 model: this.model
               })
           }];
    diff --git a/test/view-grid.test.js b/test/view-grid.test.js
    index 750cca2a..b415acde 100644
    --- a/test/view-grid.test.js
    +++ b/test/view-grid.test.js
    @@ -1,10 +1,10 @@
     (function ($) {
     
    -module("View - DataGrid");
    +module("View - Grid");
     
     test('menu - hideColumn', function () {
       var dataset = Fixture.getDataset();
    -  var view = new recline.View.DataGrid({
    +  var view = new recline.View.Grid({
         model: dataset
       });
       $('.fixtures .test-datatable').append(view.el);
    @@ -22,7 +22,7 @@ test('menu - hideColumn', function () {
     
     test('state', function () {
       var dataset = Fixture.getDataset();
    -  var view = new recline.View.DataGrid({
    +  var view = new recline.View.Grid({
         model: dataset,
         state: {
           hiddenFields: ['z']
    @@ -35,7 +35,7 @@ test('state', function () {
       view.remove();
     });
     
    -test('new DataGridRow View', function () {
    +test('new GridRow View', function () {
       var $el = $('
    '); $('.fixtures .test-datatable').append($el); var doc = new recline.Model.Document({ @@ -43,7 +43,7 @@ test('new DataGridRow View', function () { 'b': '2', 'a': '1' }); - var view = new recline.View.DataGridRow({ + var view = new recline.View.GridRow({ model: doc , el: $el , fields: new recline.Model.FieldList([{id: 'a'}, {id: 'b'}]) From 93e8b14a251c5b9d27d2a968ba3f55e7c6277a41 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 16:46:59 +0100 Subject: [PATCH 30/48] [#81,css][xs]: split out grid css into its own css file. --- app/index.html | 19 ++- css/data-explorer.css | 317 ------------------------------------------ css/grid.css | 315 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+), 323 deletions(-) create mode 100644 css/grid.css diff --git a/app/index.html b/app/index.html index 0da14de7..db5b8917 100644 --- a/app/index.html +++ b/app/index.html @@ -11,17 +11,24 @@ - - - - - - + + + + + + + + + + + + + diff --git a/css/data-explorer.css b/css/data-explorer.css index 6cc5b0a8..766a91ec 100644 --- a/css/data-explorer.css +++ b/css/data-explorer.css @@ -78,320 +78,3 @@ display: inline-block; } - -/********************************************************** - * Data Table - *********************************************************/ - -.recline-grid .btn-group .dropdown-toggle { - padding: 1px 3px; - line-height: auto; -} - -.recline-grid-container { - overflow: auto; - height: 550px; -} - -.recline-grid { - border: 1px solid #ccc; - width: 100%; -} - -.recline-grid td, .recline-grid th { - border-left: 1px solid #ccc; - padding: 3px 4px; - text-align: left; -} - -.recline-grid tr td:first-child, .recline-grid tr th:first-child { - width: 20px; -} - -/* direct borrowing from twitter buttons */ -.recline-grid th, -.transform-column-view .expression-preview-table-wrapper th -{ - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - border: 1px solid #ccc; - border-bottom-color: #bbb; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -webkit-transition: 0.1s linear all; - -moz-transition: 0.1s linear all; - -ms-transition: 0.1s linear all; - -o-transition: 0.1s linear all; - transition: 0.1s linear all; -} - - -/********************************************************** - * Data Table Menus - *********************************************************/ - -.column-header-menu, a.root-header-menu { - float: right; -} - -.read-only a.row-header-menu { - display: none; -} - -div.data-table-cell-content { - line-height: 1.2; - color: #222; - position: relative; -} - -div.data-table-cell-content-numeric { - text-align: right; -} - -a.data-table-cell-edit { - position: absolute; - top: 0; - right: 0; - display: block; - width: 25px; - height: 16px; - text-decoration: none; - background-image: url(images/edit-map.png); - background-repeat: no-repeat; - visibility: hidden; -} - -a.data-table-cell-edit:hover { - background-position: -25px 0px; -} - -.recline-grid td:hover .data-table-cell-edit { - visibility: visible; -} - -div.data-table-cell-content-numeric > a.data-table-cell-edit { - left: 0px; - right: auto; -} - -.data-table-value-nonstring { - color: #282; -} - -.data-table-error { - color: red; -} - -.data-table-cell-editor-editor { - overflow: hidden; - display: block; - width: 98%; - height: 3em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-copypaste-editor { - overflow: hidden; - display: block; - width: 98%; - height: 10em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-editor-action { - float: left; - vertical-align: bottom; - text-align: center; -} - -.data-table-cell-editor-key { - font-size: 0.8em; - color: #999; -} - - -/********************************************************** - * Dialogs - *********************************************************/ - -.dialog-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #666; - opacity: 0.5; -} - -.dialog { - position: fixed; - left: 0; - width: 100%; - text-align: center; -} - -.dialog-frame { - margin: 0 auto; - text-align: left; - background: white; - border: 1px solid #3a5774; -} - -.dialog-border { - border: 4px solid #c1d9ff; -} - -.dialog-header { - background: #e0edfe; - padding: 10px; - font-weight: bold; - font-size: 1.6em; - color: #000; - cursor: move; -} - -.dialog-body { - overflow: auto; - font-size: 1.3em; - padding: 15px; -} - -.dialog-instruction { - padding: 0 0 7px; -} - -.dialog-footer { - font-size: 1.3em; - background: #eee; - padding: 10px; -} - -.dialog-busy { - width: 400px; - border: none; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -/********************************************************** - * Transform Dialog - *********************************************************/ - -#expression-preview-tabs .ui-tabs-nav li a { - padding: 0.15em 1em; -} - -textarea.expression-preview-code { - font-family: monospace; - height: 5em; - vertical-align: top; -} - -.expression-preview-parsing-status { - color: #999; -} - -.expression-preview-parsing-status.error { - color: red; -} - -#expression-preview-tabs-preview, -#expression-preview-tabs-help, -#expression-preview-tabs-history, -#expression-preview-tabs-starred { - padding: 5px; - overflow: hidden; -} - -#expression-preview-tabs-preview > div, -#expression-preview-tabs-help > div, -#expression-preview-tabs-history > div, -#expression-preview-tabs-starred { - height: 200px; - overflow: auto; -} - -#expression-preview-tabs-preview td, #expression-preview-tabs-preview th, -#expression-preview-tabs-help td, #expression-preview-tabs-help th, -#expression-preview-tabs-history td, #expression-preview-tabs-history th, -#expression-preview-tabs-starred td, #expression-preview-tabs-starred th { - padding: 5px; -} - -.expression-preview-table-wrapper { - padding: 7px; -} - -.expression-preview-container td { - padding: 2px 5px; - border-top: 1px solid #ccc; -} - -td.expression-preview-heading { - border-top: none; - background: #ddd; - font-weight: bold; -} - -td.expression-preview-value { - max-width: 250px !important; - overflow-x: hidden; -} - -.expression-preview-special-value { - color: #aaa; -} - -.expression-preview-help-container h3 { - margin-top: 15px; - margin-bottom: 7px; - border-bottom: 1px solid #999; -} - -.expression-preview-doc-item-title { - font-weight: bold; - text-align: right; -} - -.expression-preview-doc-item-params { -} - -.expression-preview-doc-item-returns { -} - -.expression-preview-doc-item-desc { - color: #666; -} - - -/********************************************************** - * Read-only mode - *********************************************************/ - -.read-only .no-hidden .recline-grid tr td:first-child, -.read-only .no-hidden .recline-grid tr th:first-child -{ - display: none; -} - - -.read-only .write-op, -.read-only a.data-table-cell-edit -{ - display: none; -} - diff --git a/css/grid.css b/css/grid.css new file mode 100644 index 00000000..fd525be6 --- /dev/null +++ b/css/grid.css @@ -0,0 +1,315 @@ +/********************************************************** + * (Data) Grid + *********************************************************/ + +.recline-grid .btn-group .dropdown-toggle { + padding: 1px 3px; + line-height: auto; +} + +.recline-grid-container { + overflow: auto; + height: 550px; +} + +.recline-grid { + border: 1px solid #ccc; + width: 100%; +} + +.recline-grid td, .recline-grid th { + border-left: 1px solid #ccc; + padding: 3px 4px; + text-align: left; +} + +.recline-grid tr td:first-child, .recline-grid tr th:first-child { + width: 20px; +} + +/* direct borrowing from twitter buttons */ +.recline-grid th, +.transform-column-view .expression-preview-table-wrapper th +{ + background-color: #e6e6e6; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); + background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + color: #333; + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + -ms-transition: 0.1s linear all; + -o-transition: 0.1s linear all; + transition: 0.1s linear all; +} + + +/********************************************************** + * Data Table Menus + *********************************************************/ + +.column-header-menu, a.root-header-menu { + float: right; +} + +.read-only a.row-header-menu { + display: none; +} + +div.data-table-cell-content { + line-height: 1.2; + color: #222; + position: relative; +} + +div.data-table-cell-content-numeric { + text-align: right; +} + +a.data-table-cell-edit { + position: absolute; + top: 0; + right: 0; + display: block; + width: 25px; + height: 16px; + text-decoration: none; + background-image: url(images/edit-map.png); + background-repeat: no-repeat; + visibility: hidden; +} + +a.data-table-cell-edit:hover { + background-position: -25px 0px; +} + +.recline-grid td:hover .data-table-cell-edit { + visibility: visible; +} + +div.data-table-cell-content-numeric > a.data-table-cell-edit { + left: 0px; + right: auto; +} + +.data-table-value-nonstring { + color: #282; +} + +.data-table-error { + color: red; +} + +.data-table-cell-editor-editor { + overflow: hidden; + display: block; + width: 98%; + height: 3em; + font-family: monospace; + margin: 3px 0; +} + +.data-table-cell-copypaste-editor { + overflow: hidden; + display: block; + width: 98%; + height: 10em; + font-family: monospace; + margin: 3px 0; +} + +.data-table-cell-editor-action { + float: left; + vertical-align: bottom; + text-align: center; +} + +.data-table-cell-editor-key { + font-size: 0.8em; + color: #999; +} + + +/********************************************************** + * Dialogs + *********************************************************/ + +.dialog-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #666; + opacity: 0.5; +} + +.dialog { + position: fixed; + left: 0; + width: 100%; + text-align: center; +} + +.dialog-frame { + margin: 0 auto; + text-align: left; + background: white; + border: 1px solid #3a5774; +} + +.dialog-border { + border: 4px solid #c1d9ff; +} + +.dialog-header { + background: #e0edfe; + padding: 10px; + font-weight: bold; + font-size: 1.6em; + color: #000; + cursor: move; +} + +.dialog-body { + overflow: auto; + font-size: 1.3em; + padding: 15px; +} + +.dialog-instruction { + padding: 0 0 7px; +} + +.dialog-footer { + font-size: 1.3em; + background: #eee; + padding: 10px; +} + +.dialog-busy { + width: 400px; + border: none; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +/********************************************************** + * Transform Dialog + *********************************************************/ + +#expression-preview-tabs .ui-tabs-nav li a { + padding: 0.15em 1em; +} + +textarea.expression-preview-code { + font-family: monospace; + height: 5em; + vertical-align: top; +} + +.expression-preview-parsing-status { + color: #999; +} + +.expression-preview-parsing-status.error { + color: red; +} + +#expression-preview-tabs-preview, +#expression-preview-tabs-help, +#expression-preview-tabs-history, +#expression-preview-tabs-starred { + padding: 5px; + overflow: hidden; +} + +#expression-preview-tabs-preview > div, +#expression-preview-tabs-help > div, +#expression-preview-tabs-history > div, +#expression-preview-tabs-starred { + height: 200px; + overflow: auto; +} + +#expression-preview-tabs-preview td, #expression-preview-tabs-preview th, +#expression-preview-tabs-help td, #expression-preview-tabs-help th, +#expression-preview-tabs-history td, #expression-preview-tabs-history th, +#expression-preview-tabs-starred td, #expression-preview-tabs-starred th { + padding: 5px; +} + +.expression-preview-table-wrapper { + padding: 7px; +} + +.expression-preview-container td { + padding: 2px 5px; + border-top: 1px solid #ccc; +} + +td.expression-preview-heading { + border-top: none; + background: #ddd; + font-weight: bold; +} + +td.expression-preview-value { + max-width: 250px !important; + overflow-x: hidden; +} + +.expression-preview-special-value { + color: #aaa; +} + +.expression-preview-help-container h3 { + margin-top: 15px; + margin-bottom: 7px; + border-bottom: 1px solid #999; +} + +.expression-preview-doc-item-title { + font-weight: bold; + text-align: right; +} + +.expression-preview-doc-item-params { +} + +.expression-preview-doc-item-returns { +} + +.expression-preview-doc-item-desc { + color: #666; +} + + +/********************************************************** + * Read-only mode + *********************************************************/ + +.read-only .no-hidden .recline-grid tr td:first-child, +.read-only .no-hidden .recline-grid tr th:first-child +{ + display: none; +} + +.read-only .recline-grid .write-op, +.read-only .recline-grid a.data-table-cell-edit +{ + display: none; +} + From cb51f485c01ce8a70f01a5473de41eef3d7671f3 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 16:53:46 +0100 Subject: [PATCH 31/48] [#81,css][s]: map view uses recline- naming for top level container plus minor simplifying rename for graph css (fixes #81). --- css/graph.css | 20 ++++++++++---------- css/map.css | 8 ++++---- src/view-graph.js | 2 +- src/view-map.js | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/css/graph.css b/css/graph.css index f224603f..88acf5f8 100644 --- a/css/graph.css +++ b/css/graph.css @@ -1,14 +1,14 @@ -.recline-graph-container .graph { +.recline-graph .graph { height: 500px; margin-right: 200px; } -.recline-graph-container .legend table { +.recline-graph .legend table { width: auto; margin-bottom: 0; } -.recline-graph-container .legend td { +.recline-graph .legend td { padding: 5px; line-height: 13px; } @@ -17,34 +17,34 @@ * Editor *********************************************************/ -.recline-graph-container .editor { +.recline-graph .editor { float: right; width: 200px; padding-left: 0px; } -.recline-graph-container .editor-info { +.recline-graph .editor-info { padding-left: 4px; } -.recline-graph-container .editor-info { +.recline-graph .editor-info { cursor: pointer; } -.recline-graph-container .editor form { +.recline-graph .editor form { padding-left: 4px; } -.recline-graph-container .editor select { +.recline-graph .editor select { width: 100%; } -.recline-graph-container .editor-info { +.recline-graph .editor-info { border-bottom: 1px solid #ddd; margin-bottom: 10px; } -.recline-graph-container .editor-hide-info p { +.recline-graph .editor-hide-info p { display: none; } diff --git a/css/map.css b/css/map.css index c8adde77..829d0c82 100644 --- a/css/map.css +++ b/css/map.css @@ -1,4 +1,4 @@ -.data-map-container .map { +.recline-map .map { height: 500px; } @@ -6,18 +6,18 @@ * Editor *********************************************************/ -.data-map-container .editor { +.recline-map .editor { float: right; width: 200px; padding-left: 0px; margin-left: 10px; } -.data-map-container .editor form { +.recline-map .editor form { padding-left: 4px; } -.data-map-container .editor select { +.recline-map .editor select { width: 100%; } diff --git a/src/view-graph.js b/src/view-graph.js index 9e54d213..0bfbd007 100644 --- a/src/view-graph.js +++ b/src/view-graph.js @@ -23,7 +23,7 @@ this.recline.View = this.recline.View || {}; my.Graph = Backbone.View.extend({ tagName: "div", - className: "recline-graph-container", + className: "recline-graph", template: ' \
    \ diff --git a/src/view-map.js b/src/view-map.js index 93f9d039..8c9123fd 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -26,7 +26,7 @@ this.recline.View = this.recline.View || {}; my.Map = Backbone.View.extend({ tagName: 'div', - className: 'data-map-container', + className: 'recline-map', template: ' \
    \ From 2515658a0b9ed1093b74a1d3cb87d98f4ee712b0 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 17:00:42 +0100 Subject: [PATCH 32/48] [#80,refactor][xs]: switch to _.each in view-graph.js. --- src/view-graph.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view-graph.js b/src/view-graph.js index 0bfbd007..08695a92 100644 --- a/src/view-graph.js +++ b/src/view-graph.js @@ -275,9 +275,9 @@ my.Graph = Backbone.View.extend({ createSeries: function () { var self = this; var series = []; - $.each(this.state.attributes.series, function (seriesIndex, field) { + _.each(this.state.attributes.series, function(field) { var points = []; - $.each(self.model.currentDocuments.models, function (index, doc) { + _.each(self.model.currentDocuments.models, function(doc, index) { var x = doc.get(self.state.attributes.group); var y = doc.get(field); if (typeof x === 'string') { From 270f68784c83247a7481fa28ea9de8d4c167382c Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 17:07:43 +0100 Subject: [PATCH 33/48] [#81,css][xs]: css class read-only -> recline-read-only. --- css/grid.css | 16 ++++++++-------- src/view.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/css/grid.css b/css/grid.css index fd525be6..4e4bd806 100644 --- a/css/grid.css +++ b/css/grid.css @@ -63,10 +63,6 @@ float: right; } -.read-only a.row-header-menu { - display: none; -} - div.data-table-cell-content { line-height: 1.2; color: #222; @@ -301,15 +297,19 @@ td.expression-preview-value { * Read-only mode *********************************************************/ -.read-only .no-hidden .recline-grid tr td:first-child, -.read-only .no-hidden .recline-grid tr th:first-child +.recline-read-only .no-hidden .recline-grid tr td:first-child, +.recline-read-only .no-hidden .recline-grid tr th:first-child { display: none; } -.read-only .recline-grid .write-op, -.read-only .recline-grid a.data-table-cell-edit +.recline-read-only .recline-grid .write-op, +.recline-read-only .recline-grid a.data-table-cell-edit { display: none; } +.recline-read-only a.row-header-menu { + display: none; +} + diff --git a/src/view.js b/src/view.js index b78e1901..815af453 100644 --- a/src/view.js +++ b/src/view.js @@ -163,7 +163,7 @@ my.DataExplorer = Backbone.View.extend({ }, setReadOnly: function() { - this.el.addClass('read-only'); + this.el.addClass('recline-read-only'); }, render: function() { From d71203e69a5d8db3273a6e861c842e73303069f4 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 17:29:29 +0100 Subject: [PATCH 34/48] [#88,refactor/state][s]: map view now supports state (used for specifying geometry/lat/lon fields). --- src/view-map.js | 85 +++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/view-map.js b/src/view-map.js index 8c9123fd..6759c55e 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -12,17 +12,17 @@ this.recline.View = this.recline.View || {}; // [GeoJSON](http://geojson.org) objects or two fields with latitude and // longitude coordinates. // -// Initialization arguments: -// -// * options: initial options. They must contain a model: -// -// { -// model: {recline.Model.Dataset} -// } -// -// * config: (optional) map configuration hash (not yet used) -// +// Initialization arguments are as standard for Dataset Views. State object may +// have the following (optional) configuration options: // +//
    +//   {
    +//     // geomField if specified will be used in preference to lat/lon 
    +//     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}
    +//   }
    +// 
    my.Map = Backbone.View.extend({ tagName: 'div', @@ -95,14 +95,12 @@ my.Map = Backbone.View.extend({ 'change .editor-field-type': 'onFieldTypeChange' }, - - initialize: function(options, config) { + initialize: function(options) { var self = this; - this.el = $(this.el); // Listen to changes in the fields - this.model.bind('change', function() { + this.model.fields.bind('change', function() { self._setupGeometryField(); }); this.model.fields.bind('add', this.render); @@ -122,8 +120,16 @@ my.Map = Backbone.View.extend({ self.map.invalidateSize(); }); - this.mapReady = false; + var stateData = _.extend({ + geomField: null, + lonField: null, + latField: null + }, + options.state + ); + this.state = new recline.Model.ObjectState(stateData); + this.mapReady = false; this.render(); }, @@ -140,12 +146,12 @@ my.Map = Backbone.View.extend({ this.$map = this.el.find('.panel.map'); if (this.geomReady && this.model.fields.length){ - if (this._geomFieldName){ - this._selectOption('editor-geom-field',this._geomFieldName); + if (this.state.get('geomField')){ + this._selectOption('editor-geom-field',this.state.get('geomField')); $('#editor-field-type-geom').attr('checked','checked').change(); } else{ - this._selectOption('editor-lon-field',this._lonFieldName); - this._selectOption('editor-lat-field',this._latFieldName); + this._selectOption('editor-lon-field',this.state.get('lonField')); + this._selectOption('editor-lat-field',this.state.get('latField')); $('#editor-field-type-latlon').attr('checked','checked').change(); } } @@ -174,9 +180,7 @@ my.Map = Backbone.View.extend({ // * refresh: Clear existing features and add all current documents // redraw: function(action,doc){ - var self = this; - action = action || 'refresh'; if (this.geomReady && this.mapReady){ @@ -205,14 +209,19 @@ my.Map = Backbone.View.extend({ onEditorSubmit: function(e){ e.preventDefault(); if ($('#editor-field-type-geom').attr('checked')){ - this._geomFieldName = $('.editor-geom-field > select > option:selected').val(); - this._latFieldName = this._lonFieldName = false; + this.state.set({ + geomField: $('.editor-geom-field > select > option:selected').val(), + lonField: null, + latField: null + }); } else { - this._geomFieldName = false; - this._latFieldName = $('.editor-lat-field > select > option:selected').val(); - this._lonFieldName = $('.editor-lon-field > select > option:selected').val(); + this.state.set({ + geomField: null, + lonField: $('.editor-lon-field > select > option:selected').val(), + latField: $('.editor-lat-field > select > option:selected').val() + }); } - this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); + this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); this.redraw(); return false; @@ -301,16 +310,16 @@ my.Map = Backbone.View.extend({ // _getGeometryFromDocument: function(doc){ if (this.geomReady){ - if (this._geomFieldName){ + if (this.state.get('geomField')){ // We assume that the contents of the field are a valid GeoJSON object - return doc.attributes[this._geomFieldName]; - } else if (this._lonFieldName && this._latFieldName){ + return doc.attributes[this.state.get('geomField')]; + } else if (this.state.get('lonField') && this.state.get('latField')){ // We'll create a GeoJSON like point object from the two lat/lon fields return { type: 'Point', coordinates: [ - doc.attributes[this._lonFieldName], - doc.attributes[this._latFieldName] + doc.attributes[this.state.get('lonField')], + doc.attributes[this.state.get('latField')] ] }; } @@ -324,12 +333,12 @@ my.Map = Backbone.View.extend({ // If not found, the user can define them via the UI form. _setupGeometryField: function(){ var geomField, latField, lonField; - - this._geomFieldName = this._checkField(this.geometryFieldNames); - this._latFieldName = this._checkField(this.latitudeFieldNames); - this._lonFieldName = this._checkField(this.longitudeFieldNames); - - this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName)); + this.state.set({ + geomField: this._checkField(this.geometryFieldNames), + latField: this._checkField(this.latitudeFieldNames), + lonField: this._checkField(this.longitudeFieldNames) + }); + this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); }, // Private: Check if a field in the current model exists in the provided From 002308f78f455907867448f306c32bdab5b3b744 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 18:49:07 +0100 Subject: [PATCH 35/48] [#88,doc/view][m]: document state concept and usage plus provide general overview of recline views and how Dataset Views should be structured. --- src/view.js | 97 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/src/view.js b/src/view.js index 815af453..0b46df21 100644 --- a/src/view.js +++ b/src/view.js @@ -1,16 +1,83 @@ /*jshint multistr:true */ -// # Core View Functionality plus Data Explorer +// # Recline Views // -// ## Common view concepts +// Recline Views are Backbone Views and in keeping with normal Backbone views +// are Widgets / Components displaying something in the DOM. Like all Backbone +// views they have a pointer to a model or a collection and is bound to an +// element. +// +// Views provided by core Recline are crudely divided into two types: +// +// * Dataset Views: a View intended for displaying a recline.Model.Dataset +// in some fashion. Examples are the Grid, Graph and Map views. +// * Widget Views: a widget used for displaying some specific (and +// smaller) aspect of a dataset or the application. Examples are +// QueryEditor and FilterEditor which both provide a way for editing (a +// part of) a `recline.Model.Query` associated to a Dataset. +// +// ## Dataset View +// +// These views are just Backbone views with a few additional conventions: +// +// 1. The model passed to the View should always be a recline.Model.Dataset instance +// 2. Views should generate their own root element rather than having it passed +// in. +// 3. Views should apply a css class named 'recline-{view-name-lower-cased} to +// the root element (and for all CSS for this view to be qualified using this +// CSS class) +// 4. Read-only mode: CSS for this view should respect/utilize +// recline-read-only class to trigger read-only behaviour (this class will +// usually be set on some parent element of the view's root element. +// 5. State: state (configuration) information for the view should be stored on +// an attribute named state that is an instance of a Backbone Model (or, more +// speficially, be an instance of `recline.Model.ObjectState`). In addition, +// a state attribute may be specified in the Hash passed to a View on +// iniitialization and this information should be used to set the initial +// state of the view. +// +// Example of state would be the set of fields being plotted in a graph +// view. +// +// More information about State can be found below. +// +// To summarize some of this, the initialize function for a Dataset View should +// look like: +// +//
    +//    initialize: {
    +//        model: {a recline.Model.Dataset instance}
    +//        // el: {do not specify - instead view should create}
    +//        state: {(optional) Object / Hash specifying initial state}
    +//        ...
    +//    }
    +// 
    +// +// Note: Dataset Views in core Recline have a common layout on disk as +// follows, where ViewName is the named of View class: +// +//
    +// src/view-{lower-case-ViewName}.js
    +// css/{lower-case-ViewName}.css
    +// test/view-{lower-case-ViewName}.js
    +// 
    // // ### State // -// TODO +// State information exists in order to support state serialization into the +// url or elsewhere and reloading of application from a stored state. // -// ### Read-only +// State is available not only for individual views (as described above) but +// for the dataset (e.g. the current query). For an example of pulling together +// state from across multiple components see `recline.View.DataExplorer`. +// +// ### Writing your own Views // -// TODO +// See the existing Views. +// +// ---- + +// Standard JS module setup this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; @@ -59,10 +126,20 @@ this.recline.View = this.recline.View || {}; // ]; // // -// **state**: state config for this view. Options are: +// **state**: standard state config for this view. This state is slightly +// special as it includes config of many of the subviews. // -// * readOnly: true/false (default: false) value indicating whether to -// operate in read-only mode (hiding all editing options). +//
    +// state = {
    +//     query: {dataset query state - see dataset.queryState object}
    +//     view-{id1}: {view-state for this view}
    +//     view-{id2}: {view-state for }
    +//     ...
    +//     // Explorer
    +//     currentView: id of current view (defaults to first view if not specified)
    +//     readOnly: (default: false) run in read-only mode
    +// }
    +// 
    my.DataExplorer = Backbone.View.extend({ template: ' \
    \ @@ -278,7 +355,9 @@ my.DataExplorer = Backbone.View.extend({ }); }, - // Get the current state of dataset and views + // ### getState + // + // Get the current state of dataset and views (see discussion of state above) getState: function() { return this.state; } From 7743534eac6673c9dc942f0912d194e6cdc64215 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 22:19:43 +0100 Subject: [PATCH 36/48] [#88,backend][s]: add __type__ attribute to all backends to identify them and provide a more robust and generic way to load backends from a string identifier such as that __type__ field. * Also remove recline.Model.backends registry as can be replaced with this more generic solution. * This refactoring is necessitated by our need to serialize backend info for save/reload of a dataset and explorer state in #88. --- index.html | 10 ++----- src/backend/base.js | 12 +++++++- src/backend/dataproxy.js | 3 +- src/backend/elasticsearch.js | 2 +- src/backend/gdocs.js | 2 +- src/backend/memory.js | 6 ++-- src/model.js | 45 ++++++++++++++++++++++++++++-- test/backend.elasticsearch.test.js | 3 +- test/backend.test.js | 8 ++++-- test/index.html | 2 +- test/model.test.js | 10 +++++++ 11 files changed, 80 insertions(+), 23 deletions(-) diff --git a/index.html b/index.html index 30c80bf8..db4c4b93 100644 --- a/index.html +++ b/index.html @@ -182,13 +182,9 @@ Backbone.history.start();

    Creating a Dataset Explicitly with a Backend

    -// Backend can be an instance or string id for a backend in the
    -// recline.Model.backends registry
    -var backend = 'elasticsearch'
    -// alternatively you can create explicitly
    -// var backend = new recline.Backend.ElasticSearch();
    -// or even from your own backend ...
    -// var backend = new myModule.Backend();
    +// Connect to ElasticSearch index/type as our data source
    +// There are many other backends you can use (and you can write your own)
    +var backend = new recline.Backend.ElasticSearch();
     
     // Dataset is a Backbone model so the first hash become model attributes
     var dataset = recline.Model.Dataset({
    diff --git a/src/backend/base.js b/src/backend/base.js
    index ec4d8412..e9c4eea7 100644
    --- a/src/backend/base.js
    +++ b/src/backend/base.js
    @@ -17,10 +17,20 @@ this.recline.Backend = this.recline.Backend || {};
       // ## recline.Backend.Base
       //
       // Base class for backends providing a template and convenience functions.
    -  // You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
    +  // You do not have to inherit from this class but even when not it does
    +  // provide guidance on the functions you must implement.
       //
       // Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
       my.Base = Backbone.Model.extend({
    +    // ### __type__
    +    //
    +    // 'type' of this backend. This should be either the class path for this
    +    // object as a string (e.g. recline.Backend.Memory) or for Backends within
    +    // recline.Backend module it may be their class name.
    +    //
    +    // This value is used as an identifier for this backend when initializing
    +    // backends (see recline.Model.Dataset.initialize).
    +    __type__: 'base',
     
         // ### sync
         //
    diff --git a/src/backend/dataproxy.js b/src/backend/dataproxy.js
    index 794b8e79..8f2b7496 100644
    --- a/src/backend/dataproxy.js
    +++ b/src/backend/dataproxy.js
    @@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {};
       //
       // Note that this is a **read-only** backend.
       my.DataProxy = my.Base.extend({
    +    __type__: 'dataproxy',
         defaults: {
           dataproxy_url: 'http://jsonpdataproxy.appspot.com'
         },
    @@ -71,7 +72,5 @@ this.recline.Backend = this.recline.Backend || {};
           return dfd.promise();
         }
       });
    -  recline.Model.backends['dataproxy'] = new my.DataProxy();
    -
     
     }(jQuery, this.recline.Backend));
    diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js
    index 6944a2f4..164393a8 100644
    --- a/src/backend/elasticsearch.js
    +++ b/src/backend/elasticsearch.js
    @@ -20,6 +20,7 @@ this.recline.Backend = this.recline.Backend || {};
       //
       // 
    http://localhost:9200/twitter/tweet
    my.ElasticSearch = my.Base.extend({ + __type__: 'elasticsearch', _getESUrl: function(dataset) { var out = dataset.get('elasticsearch_url'); if (out) return out; @@ -115,7 +116,6 @@ this.recline.Backend = this.recline.Backend || {}; return dfd.promise(); } }); - recline.Model.backends['elasticsearch'] = new my.ElasticSearch(); }(jQuery, this.recline.Backend)); diff --git a/src/backend/gdocs.js b/src/backend/gdocs.js index 8cf0407c..e6f29b55 100644 --- a/src/backend/gdocs.js +++ b/src/backend/gdocs.js @@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {}; // ); //
    my.GDoc = my.Base.extend({ + __type__: 'gdoc', getUrl: function(dataset) { var url = dataset.get('url'); if (url.indexOf('feeds/list') != -1) { @@ -134,7 +135,6 @@ this.recline.Backend = this.recline.Backend || {}; return results; } }); - recline.Model.backends['gdocs'] = new my.GDoc(); }(jQuery, this.recline.Backend)); diff --git a/src/backend/memory.js b/src/backend/memory.js index 49f03087..f79991e8 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -20,7 +20,7 @@ this.recline.Backend = this.recline.Backend || {}; if (!metadata.id) { metadata.id = String(Math.floor(Math.random() * 100000000) + 1); } - var backend = recline.Model.backends['memory']; + var backend = new recline.Backend.Memory(); var datasetInfo = { documents: data, metadata: metadata @@ -35,7 +35,7 @@ this.recline.Backend = this.recline.Backend || {}; } } backend.addDataset(datasetInfo); - var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory'); + var dataset = new recline.Model.Dataset({id: metadata.id}, backend); dataset.fetch(); return dataset; }; @@ -70,6 +70,7 @@ this.recline.Backend = this.recline.Backend || {}; // etc ... // my.Memory = my.Base.extend({ + __type__: 'memory', initialize: function() { this.datasets = {}; }, @@ -209,6 +210,5 @@ this.recline.Backend = this.recline.Backend || {}; return facetResults; } }); - recline.Model.backends['memory'] = new my.Memory(); }(jQuery, this.recline.Backend)); diff --git a/src/model.js b/src/model.js index 5f834cad..137244d7 100644 --- a/src/model.js +++ b/src/model.js @@ -18,7 +18,7 @@ this.recline.Model = this.recline.Model || {}; // // @property {number} docCount: total number of documents in this dataset // -// @property {Backend} backend: the Backend (instance) for this Dataset +// @property {Backend} backend: the Backend (instance) for this Dataset. // // @property {Query} queryState: `Query` object which stores current // queryState. queryState may be edited by other components (e.g. a query @@ -28,14 +28,24 @@ this.recline.Model = this.recline.Model || {}; // Facets. my.Dataset = Backbone.Model.extend({ __type__: 'Dataset', + // ### initialize // // Sets up instance properties (see above) + // + // @param {Object} model: standard set of model attributes passed to Backbone models + // + // @param {Object or String} backend: Backend instance (see + // `recline.Backend.Base`) or a string specifying that instance. The + // string specifying may be a full class path e.g. + // 'recline.Backend.ElasticSearch' or a simple name e.g. + // 'elasticsearch' or 'ElasticSearch' (in this case must be a Backend in + // recline.Backend module) initialize: function(model, backend) { _.bindAll(this, 'query'); this.backend = backend; - if (backend && backend.constructor == String) { - this.backend = my.backends[backend]; + if (typeof(backend) === 'string') { + this.backend = this._backendFromString(backend); } this.fields = new my.FieldList(); this.currentDocuments = new my.DocumentList(); @@ -99,6 +109,35 @@ my.Dataset = Backbone.Model.extend({ data.docCount = this.docCount; data.fields = this.fields.toJSON(); return data; + }, + + // ### _backendFromString(backendString) + // + // See backend argument to initialize for details + _backendFromString: function(backendString) { + var parts = backendString.split('.'); + // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class + var current = window; + for(ii=0;ii - + diff --git a/test/model.test.js b/test/model.test.js index 2625a7a4..2f0c4e6a 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -103,6 +103,16 @@ test('Dataset _prepareQuery', function () { deepEqual(out, exp); }); +test('Dataset _backendFromString', function () { + var dataset = new recline.Model.Dataset(); + + var out = dataset._backendFromString('recline.Backend.Memory'); + equal(out.__type__, 'memory'); + + var out = dataset._backendFromString('dataproxy'); + equal(out.__type__, 'dataproxy'); +}); + // ================================= // Query From 2a93aeb2c134485de72675ecab388f112867c873 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 22:47:16 +0100 Subject: [PATCH 37/48] [#88,state][s]: (and finally) introduce a recline.View.DataExplorer.restore function that restores from a serialized state. * remove getState in favour of just using direct access to state object. --- src/view.js | 38 +++++++++++++++++++++++++++++--------- test/view.test.js | 20 ++++++++++++++++---- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/view.js b/src/view.js index 0b46df21..6dedc111 100644 --- a/src/view.js +++ b/src/view.js @@ -318,10 +318,12 @@ my.DataExplorer = Backbone.View.extend({ var graphState = qs['view-graph'] || qs.graph; graphState = graphState ? JSON.parse(graphState) : {}; var stateData = _.extend({ - readOnly: false, query: query, 'view-graph': graphState, - currentView: null + backend: this.model.backend.__type__, + dataset: this.model.toJSON(), + currentView: null, + readOnly: false }, initialState); this.state.set(stateData); @@ -353,16 +355,34 @@ my.DataExplorer = Backbone.View.extend({ }); } }); - }, - - // ### getState - // - // Get the current state of dataset and views (see discussion of state above) - getState: function() { - return this.state; } }); +// ## restore +// +// Restore a DataExplorer instance from a serialized state including the associated dataset +my.DataExplorer.restore = function(state) { + // hack-y - restoring a memory dataset does not mean much ... + var dataset = null; + if (state.backend === 'memory') { + dataset = recline.Backend.createDataset( + [{stub: 'this is a stub dataset because we do not restore memory datasets'}], + [], + state.dataset + ); + } else { + dataset = new recline.Model.Dataset( + state.dataset, + state.backend + ); + } + var explorer = new my.DataExplorer({ + model: dataset, + state: state + }); + return explorer; +} + my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', template: ' \ diff --git a/test/view.test.js b/test/view.test.js index 6a898989..456da7be 100644 --- a/test/view.test.js +++ b/test/view.test.js @@ -15,7 +15,7 @@ test('basic explorer functionality', function () { $el.remove(); }); -test('getState', function () { +test('get State', function () { var $el = $('
    '); $('.fixtures .data-explorer-here').append($el); var dataset = Fixture.getDataset(); @@ -23,11 +23,13 @@ test('getState', function () { model: dataset, el: $el }); - var state = explorer.getState(); + var state = explorer.state; ok(state.get('query')); equal(state.get('readOnly'), false); equal(state.get('query').size, 100); deepEqual(state.get('view-grid').hiddenFields, []); + equal(state.get('backend'), 'memory'); + ok(state.get('dataset').id !== null); $el.remove(); }); @@ -42,8 +44,18 @@ test('initialize state', function () { } } }); - var state = explorer.getState(); - ok(state.get('readOnly')); + ok(explorer.state.get('readOnly')); +}); + +test('restore (from serialized state)', function() { + var dataset = Fixture.getDataset(); + var explorer = new recline.View.DataExplorer({ + model: dataset, + }); + var state = explorer.state.toJSON(); + var explorerNew = recline.View.DataExplorer.restore(state); + var out = explorerNew.state.toJSON(); + equal(out.backend, state.backend); }); })(this.jQuery); From 3092ef6a8bd9c6578040a2bcf3382c51952268ca Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 23:03:31 +0100 Subject: [PATCH 38/48] [test][xs]: micro tidy-up (calling remove on view at end of test in Grid View test). --- test/view-grid.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/view-grid.test.js b/test/view-grid.test.js index b415acde..4d2d0a96 100644 --- a/test/view-grid.test.js +++ b/test/view-grid.test.js @@ -53,6 +53,7 @@ test('new GridRow View', function () { var tds = $el.find('td'); equal(tds.length, 3); equal($(tds[1]).attr('data-field'), 'a'); + view.remove(); }); })(this.jQuery); From bcffe673369276be36dd051df6a04ef791d59471 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 23:08:42 +0100 Subject: [PATCH 39/48] [#88,refactor][s]: create recline.Model.Dataset.restore function and move dataset restore stuff there from recline.View.DataExplorer.restore. --- src/model.js | 30 ++++++++++++++++++++++++++++++ src/view.js | 19 +++++-------------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/model.js b/src/model.js index 137244d7..1464ec46 100644 --- a/src/model.js +++ b/src/model.js @@ -141,6 +141,36 @@ my.Dataset = Backbone.Model.extend({ } }); + +// ### Dataset.restore +// +// Restore a Dataset instance from a serialized state. Serialized state for a +// Dataset is an Object like: +// +//
    +// {
    +//   backend: {backend type - i.e. value of dataset.backend.__type__}
    +//   dataset: {result of dataset.toJSON()}
    +//   ...
    +// }
    +my.Dataset.restore = function(state) {
    +  // hack-y - restoring a memory dataset does not mean much ...
    +  var dataset = null;
    +  if (state.backend === 'memory') {
    +    dataset = recline.Backend.createDataset(
    +      [{stub: 'this is a stub dataset because we do not restore memory datasets'}],
    +      [],
    +      state.dataset
    +    );
    +  } else {
    +    dataset = new recline.Model.Dataset(
    +      state.dataset,
    +      state.backend
    +    );
    +  }
    +  return dataset;
    +};
    +
     // ## A Document (aka Row)
     // 
     // A single entry or row in the dataset
    diff --git a/src/view.js b/src/view.js
    index 6dedc111..5e3a4c02 100644
    --- a/src/view.js
    +++ b/src/view.js
    @@ -140,6 +140,10 @@ this.recline.View = this.recline.View || {};
     //     readOnly: (default: false) run in read-only mode
     // }
     // 
    +// +// Note that at present we do *not* serialize information about the actual set +// of views in use -- e.g. those specified by the views argument. Instead we +// expect the client to have initialized the DataExplorer with the relevant views. my.DataExplorer = Backbone.View.extend({ template: ' \
    \ @@ -362,20 +366,7 @@ my.DataExplorer = Backbone.View.extend({ // // Restore a DataExplorer instance from a serialized state including the associated dataset my.DataExplorer.restore = function(state) { - // hack-y - restoring a memory dataset does not mean much ... - var dataset = null; - if (state.backend === 'memory') { - dataset = recline.Backend.createDataset( - [{stub: 'this is a stub dataset because we do not restore memory datasets'}], - [], - state.dataset - ); - } else { - dataset = new recline.Model.Dataset( - state.dataset, - state.backend - ); - } + var dataset = recline.Model.Dataset.restore(state); var explorer = new my.DataExplorer({ model: dataset, state: state From 6a90189b468f15f6a5909d44b084e9558cd371ed Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 23:23:37 +0100 Subject: [PATCH 40/48] [test][xs]: remove console.log. --- test/backend.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/backend.test.js b/test/backend.test.js index 62dea142..f7d96f60 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -103,7 +103,6 @@ test('Memory Backend: filters', function () { }); test('Memory Backend: facet', function () { - console.log('here'); var dataset = makeBackendDataset(); dataset.queryState.addFacet('country'); dataset.query().then(function() { From 927bc3264775c8f201243d4935f45d5e3861d217 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 23:26:57 +0100 Subject: [PATCH 41/48] [#88,view][s]: DataExplorer boots Grid, Graph and Map view by default instead of just Grid. * This makes DataExplorer.restore substantially more useful. --- src/view.js | 24 +++++++++++++++++++----- test/index.html | 6 ++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/view.js b/src/view.js index 5e3a4c02..0d26d24f 100644 --- a/src/view.js +++ b/src/view.js @@ -105,7 +105,8 @@ this.recline.View = this.recline.View || {}; // // **views**: (optional) the dataset views (Grid, Graph etc) for // DataExplorer to show. This is an array of view hashes. If not provided -// just initialize a Grid with id 'grid'. Example: +// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id +// and labels!). // //
     // var views = [
    @@ -142,8 +143,9 @@ this.recline.View = this.recline.View || {};
     // 
    // // Note that at present we do *not* serialize information about the actual set -// of views in use -- e.g. those specified by the views argument. Instead we -// expect the client to have initialized the DataExplorer with the relevant views. +// of views in use -- e.g. those specified by the views argument -- but instead +// expect either that the default views are fine or that the client to have +// initialized the DataExplorer with the relevant views themselves. my.DataExplorer = Backbone.View.extend({ template: ' \
    \ @@ -189,8 +191,20 @@ my.DataExplorer = Backbone.View.extend({ id: 'grid', label: 'Grid', view: new my.Grid({ - model: this.model - }) + model: this.model + }) + }, { + id: 'graph', + label: 'Graph', + view: new my.Graph({ + model: this.model + }) + }, { + id: 'map', + label: 'Map', + view: new my.Map({ + model: this.model + }) }]; } this.state = new recline.Model.ObjectState(); diff --git a/test/index.html b/test/index.html index 6d0838a6..da0e90b8 100644 --- a/test/index.html +++ b/test/index.html @@ -4,12 +4,15 @@ Qunit Tests + + + @@ -29,10 +32,13 @@ + + + From 0ead86d6d715e1425322680f43c7d1d3cf6f8cb9 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 00:13:14 +0100 Subject: [PATCH 42/48] [#88,view,state][s]: support for setting and getting currentView. * Also refactor so that dataset view switcher in DataExplorer runs via direct JS rather than routing (as have meant to do for a while). this is important because a) routing stuff is partly going away b) it's cleaner this way. --- src/view.js | 41 ++++++++++++++++++++++++++++++----------- test/view.test.js | 13 +++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/view.js b/src/view.js index 0d26d24f..27df520e 100644 --- a/src/view.js +++ b/src/view.js @@ -154,7 +154,7 @@ my.DataExplorer = Backbone.View.extend({
    \ \
    \ @@ -177,7 +177,8 @@ my.DataExplorer = Backbone.View.extend({
    \ ', events: { - 'click .menu-right a': 'onMenuClick' + 'click .menu-right a': '_onMenuClick', + 'click .navigation a': '_onSwitchView' }, initialize: function(options) { @@ -207,10 +208,10 @@ my.DataExplorer = Backbone.View.extend({ }) }]; } - this.state = new recline.Model.ObjectState(); // these must be called after pageViews are created - this._setupState(options.state); this.render(); + // should come after render as may need to interact with elements in the view + this._setupState(options.state); this.router = new Backbone.Router(); this.setupRouting(); @@ -299,10 +300,10 @@ my.DataExplorer = Backbone.View.extend({ }); }, - updateNav: function(pageName, queryString) { + updateNav: function(pageName) { this.el.find('.navigation li').removeClass('active'); this.el.find('.navigation li a').removeClass('disabled'); - var $el = this.el.find('.navigation li a[href=#' + pageName + ']'); + var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]'); $el.parent().addClass('active'); $el.addClass('disabled'); // show the specific page @@ -317,7 +318,7 @@ my.DataExplorer = Backbone.View.extend({ }); }, - onMenuClick: function(e) { + _onMenuClick: function(e) { e.preventDefault(); var action = $(e.target).attr('data-action'); if (action === 'filters') { @@ -327,29 +328,47 @@ my.DataExplorer = Backbone.View.extend({ } }, + _onSwitchView: function(e) { + e.preventDefault(); + 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 + // + // 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 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, 'view-graph': graphState, backend: this.model.backend.__type__, dataset: this.model.toJSON(), - currentView: null, + currentView: this.pageViews[0].id, readOnly: false }, initialState); - this.state.set(stateData); + this.state = new recline.Model.ObjectState(stateData); // now do updates based on state if (this.state.get('readOnly')) { this.setReadOnly(); } + if (this.state.get('currentView')) { + this.updateNav(this.state.get('currentView')); + } _.each(this.pageViews, function(pageView) { var viewId = 'view-' + pageView.id; if (viewId in self.state.attributes) { @@ -357,7 +376,7 @@ my.DataExplorer = Backbone.View.extend({ } }); - // bind for changes state in associated objects + // 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({queryState: self.model.queryState.toJSON()}); }); @@ -376,7 +395,7 @@ my.DataExplorer = Backbone.View.extend({ } }); -// ## restore +// ### DataExplorer.restore // // Restore a DataExplorer instance from a serialized state including the associated dataset my.DataExplorer.restore = function(state) { diff --git a/test/view.test.js b/test/view.test.js index 456da7be..cb0d4a0c 100644 --- a/test/view.test.js +++ b/test/view.test.js @@ -26,6 +26,7 @@ test('get State', function () { var state = explorer.state; ok(state.get('query')); equal(state.get('readOnly'), false); + equal(state.get('currentView'), 'grid'); equal(state.get('query').size, 100); deepEqual(state.get('view-grid').hiddenFields, []); equal(state.get('backend'), 'memory'); @@ -34,17 +35,29 @@ test('get State', function () { }); test('initialize state', function () { + var $el = $('
    '); + $('.fixtures .data-explorer-here').append($el); var dataset = Fixture.getDataset(); var explorer = new recline.View.DataExplorer({ model: dataset, + el: $el, state: { readOnly: true, + currentView: 'graph', 'view-grid': { hiddenFields: ['x'] } } }); ok(explorer.state.get('readOnly')); + ok(explorer.state.get('currentView'), 'graph'); + // check the correct view is visible + var css = explorer.el.find('.navigation a[data-view="graph"]').attr('class').split(' '); + ok(_.contains(css, 'disabled'), css); + + var css = explorer.el.find('.navigation a[data-view="grid"]').attr('class').split(' '); + ok(!(_.contains(css, 'disabled')), css); + $el.remove(); }); test('restore (from serialized state)', function() { From 2d636b0eb8bbcde418e52f97c5525a2c415df4cc Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 03:02:42 +0100 Subject: [PATCH 43/48] [css/grid.css][xs]: vertical-align top for grid cells. --- css/grid.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/css/grid.css b/css/grid.css index 4e4bd806..aeb9984e 100644 --- a/css/grid.css +++ b/css/grid.css @@ -23,6 +23,10 @@ text-align: left; } +.recline-grid td { + vertical-align: top; +} + .recline-grid tr td:first-child, .recline-grid tr th:first-child { width: 20px; } From 40a771c38bec81c6ed7060da83042b7fb462581f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 03:06:31 +0100 Subject: [PATCH 44/48] [app][s]: refactor app to be a backbone view. --- app/index.html | 2 + app/js/app.js | 177 +++++++++++++++++++++++-------------------------- 2 files changed, 84 insertions(+), 95 deletions(-) diff --git a/app/index.html b/app/index.html index db5b8917..29ff9781 100644 --- a/app/index.html +++ b/app/index.html @@ -60,6 +60,7 @@ +
    diff --git a/app/js/app.js b/app/js/app.js index 807b3c63..956e6f53 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -1,7 +1,8 @@ -$(function() { +jQuery(function($) { var qs = recline.View.parseQueryString(window.location.search); + var dataest = null; if (qs.url) { - var dataset = new recline.Model.Dataset({ + dataset = new recline.Model.Dataset({ id: 'my-dataset', url: qs.url, webstore_url: qs.url @@ -12,67 +13,90 @@ $(function() { dataset = localDataset(); } - createExplorer(dataset); + var app = new ExplorerApp({ + model: dataset, + el: $('.recline-app') + }) Backbone.history.start(); - - // setup the loader menu in top bar - setupLoader(createExplorer); }); -// make Explorer creation / initialization in a function so we can call it -// again and again -function createExplorer(dataset) { - // remove existing data explorer view - var reload = false; - if (window.dataExplorer) { - window.dataExplorer.remove(); - reload = true; - } - window.dataExplorer = null; - var $el = $('
    '); - $el.appendTo($('.data-explorer-here')); - var views = standardViews(dataset); - window.dataExplorer = new recline.View.DataExplorer({ - el: $el - , model: dataset - , views: views - }); - // HACK (a bit). Issue is that Backbone will not trigger the route - // if you are already at that location so we have to make sure we genuinely switch - if (reload) { - window.dataExplorer.router.navigate('graph'); - window.dataExplorer.router.navigate('', true); - } -} +var ExplorerApp = Backbone.View.extend({ + events: { + 'submit form.js-import-url': '_onImportURL', + 'submit .js-import-dialog-file form': '_onImportFile' + }, -// convenience function -function standardViews(dataset) { - var views = [ - { - id: 'grid', - label: 'Grid', - view: new recline.View.Grid({ - model: dataset - }) - }, - { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset - }) - }, - { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset - }) + initialize: function() { + this.explorer = null; + this.explorerDiv = $('.data-explorer-here'); + this.createExplorer(this.model); + }, + + // make Explorer creation / initialization in a function so we can call it + // again and again + createExplorer: function(dataset) { + // remove existing data explorer view + var reload = false; + if (this.dataExplorer) { + this.dataExplorer.remove(); + reload = true; } + this.dataExplorer = null; + var $el = $('
    '); + $el.appendTo(this.explorerDiv); + this.dataExplorer = new recline.View.DataExplorer({ + el: $el + , model: dataset + }); + // HACK (a bit). Issue is that Backbone will not trigger the route + // if you are already at that location so we have to make sure we genuinely switch + if (reload) { + this.dataExplorer.router.navigate('graph'); + this.dataExplorer.router.navigate('', true); + } + }, - ]; - return views; -} + // setup the loader menu in top bar + setupLoader: function(callback) { + // pre-populate webstore load form with an example url + var demoUrl = 'http://thedatahub.org/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013'; + $('form.js-import-url input[name="source"]').val(demoUrl); + }, + + _onImportURL: function(e) { + e.preventDefault(); + $('.modal.js-import-dialog-url').modal('hide'); + var $form = $(e.target); + var source = $form.find('input[name="source"]').val(); + var type = $form.find('select[name="backend_type"]').val(); + var dataset = new recline.Model.Dataset({ + id: 'my-dataset', + url: source, + webstore_url: source + }, + type + ); + this.createExplorer(dataset); + }, + + _onImportFile: function(e) { + var self = this; + e.preventDefault(); + var $form = $(e.target); + $('.modal.js-import-dialog-file').modal('hide'); + var $file = $form.find('input[type="file"]')[0]; + var file = $file.files[0]; + var options = { + separator : $form.find('input[name="separator"]').val(), + encoding : $form.find('input[name="encoding"]').val() + }; + recline.Backend.loadFromCSVFile(file, function(dataset) { + self.createExplorer(dataset) + }, + options + ); + } +}); // provide a demonstration in memory dataset function localDataset() { @@ -83,7 +107,7 @@ function localDataset() { , name: '1-my-test-dataset' , id: datasetId }, -fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}], + fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}], documents: [ {id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40} , {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60} @@ -100,40 +124,3 @@ fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'l return dataset; } -// setup the loader menu in top bar -function setupLoader(callback) { - // pre-populate webstore load form with an example url - var demoUrl = 'http://thedatahub.org/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013'; - $('form.js-import-url input[name="source"]').val(demoUrl); - $('form.js-import-url').submit(function(e) { - e.preventDefault(); - $('.modal.js-import-dialog-url').modal('hide'); - var $form = $(e.target); - var source = $form.find('input[name="source"]').val(); - var type = $form.find('select[name="backend_type"]').val(); - var dataset = new recline.Model.Dataset({ - id: 'my-dataset', - url: source, - webstore_url: source - }, - type - ); - callback(dataset); - }); - - $('.js-import-dialog-file form').submit(function(e) { - e.preventDefault(); - var $form = $(e.target); - $('.modal.js-import-dialog-file').modal('hide'); - var $file = $form.find('input[type="file"]')[0]; - var file = $file.files[0]; - var options = { - separator : $form.find('input[name="separator"]').val(), - encoding : $form.find('input[name="encoding"]').val() - }; - recline.Backend.loadFromCSVFile(file, function(dataset) { - callback(dataset) - }, options); - }); -} - From 53e099beda3d41ea21dc906f3cd5a3beff0dc582 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 15:15:59 +0100 Subject: [PATCH 45/48] [test][xs]: make fixtures dataset same as for demo. --- test/base.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/test/base.js b/test/base.js index bffbba1c..729e1178 100644 --- a/test/base.js +++ b/test/base.js @@ -1,13 +1,21 @@ var Fixture = { getDataset: function() { - var fields = [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}]; + var fields = [ + {id: 'x'}, + {id: 'y'}, + {id: 'z'}, + {id: 'country'}, + {id: 'label'}, + {id: 'lat'}, + {id: 'lon'} + ]; var documents = [ - {id: 0, x: 1, y: 2, z: 3, country: 'DE', lat:52.56, lon:13.40} - , {id: 1, x: 2, y: 4, z: 6, country: 'UK', lat:54.97, lon:-1.60} - , {id: 2, x: 3, y: 6, z: 9, country: 'US', lat:40.00, lon:-75.5} - , {id: 3, x: 4, y: 8, z: 12, country: 'UK', lat:57.27, lon:-6.20} - , {id: 4, x: 5, y: 10, z: 15, country: 'UK', lat:51.58, lon:0} - , {id: 5, x: 6, y: 12, z: 18, country: 'DE', lat:51.04, lon:7.9} + {id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}, + {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60}, + {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5}, + {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20}, + {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0}, + {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth', lat:51.04, lon:7.9} ]; var dataset = recline.Backend.createDataset(documents, fields); return dataset; From a42840cdf3231a050a784d7104d5786818fdfa76 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 15:16:28 +0100 Subject: [PATCH 46/48] [#88,model/state][xs]: can just specify a url instead of full dataset object when restoring from state (easier and good for backwards compatability). --- src/model.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/model.js b/src/model.js index 1464ec46..4145507b 100644 --- a/src/model.js +++ b/src/model.js @@ -150,17 +150,22 @@ my.Dataset = Backbone.Model.extend({ //
     // {
     //   backend: {backend type - i.e. value of dataset.backend.__type__}
    -//   dataset: {result of dataset.toJSON()}
    +//   dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
    +//   // convenience - if url provided and dataste not this be used as dataset url
    +//   url: {dataset url}
     //   ...
     // }
     my.Dataset.restore = function(state) {
       // hack-y - restoring a memory dataset does not mean much ...
       var dataset = null;
    +  if (state.url && !state.dataset) {
    +    state.dataset = {url: state.url};
    +  }
       if (state.backend === 'memory') {
         dataset = recline.Backend.createDataset(
           [{stub: 'this is a stub dataset because we do not restore memory datasets'}],
           [],
    -      state.dataset
    +      state.dataset // metadata
         );
       } else {
         dataset = new recline.Model.Dataset(
    
    From 0f0fa633a1d8f31e1bf1662d7b6d310f6c32805f Mon Sep 17 00:00:00 2001
    From: Rufus Pollock 
    Date: Mon, 16 Apr 2012 15:17:19 +0100
    Subject: [PATCH 47/48] [#67,app][s]: first pass at sharable link support in
     app (though seems rather buggy).
    
    ---
     app/index.html | 17 +++++++++++++++
     app/js/app.js  | 56 +++++++++++++++++++++++++++++++++-----------------
     src/view.js    |  3 +++
     3 files changed, 57 insertions(+), 19 deletions(-)
    
    diff --git a/app/index.html b/app/index.html
    index 29ff9781..cfd9492e 100644
    --- a/app/index.html
    +++ b/app/index.html
    @@ -81,6 +81,12 @@
                   
                 
               
    +          
  • + + Share and Embed + + +
  • @@ -148,6 +154,17 @@
    + +
    diff --git a/app/js/app.js b/app/js/app.js index 956e6f53..ab6f4dc4 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -1,20 +1,5 @@ jQuery(function($) { - var qs = recline.View.parseQueryString(window.location.search); - var dataest = null; - if (qs.url) { - dataset = new recline.Model.Dataset({ - id: 'my-dataset', - url: qs.url, - webstore_url: qs.url - }, - qs.backend || 'elasticsearch' - ); - } else { - dataset = localDataset(); - } - var app = new ExplorerApp({ - model: dataset, el: $('.recline-app') }) Backbone.history.start(); @@ -27,14 +12,32 @@ var ExplorerApp = Backbone.View.extend({ }, initialize: function() { + this.el = $(this.el); this.explorer = null; this.explorerDiv = $('.data-explorer-here'); - this.createExplorer(this.model); + + var state = recline.View.parseQueryString(window.location.search); + if (state) { + _.each(state, function(value, key) { + try { + value = JSON.parse(value); + } catch(e) {} + state[key] = value; + }); + } + var dataset = null; + if (state.dataset || state.url) { + dataset = recline.Model.Dataset.restore(state); + } else { + dataset = localDataset(); + } + this.createExplorer(dataset, state); }, // make Explorer creation / initialization in a function so we can call it // again and again - createExplorer: function(dataset) { + createExplorer: function(dataset, state) { + var self = this; // remove existing data explorer view var reload = false; if (this.dataExplorer) { @@ -45,9 +48,12 @@ var ExplorerApp = Backbone.View.extend({ var $el = $('
    '); $el.appendTo(this.explorerDiv); this.dataExplorer = new recline.View.DataExplorer({ - el: $el - , model: dataset + model: dataset, + el: $el, + state: state }); + this._setupPermaLink(this.dataExplorer); + // HACK (a bit). Issue is that Backbone will not trigger the route // if you are already at that location so we have to make sure we genuinely switch if (reload) { @@ -56,6 +62,18 @@ var ExplorerApp = Backbone.View.extend({ } }, + _setupPermaLink: function(explorer) { + var $viewLink = this.el.find('.js-share-and-embed-dialog .view-link'); + function makePermaLink(state) { + var qs = recline.View.composeQueryString(state.toJSON()); + return window.location.origin + window.location.pathname + qs; + } + explorer.state.bind('change', function() { + $viewLink.val(makePermaLink(explorer.state)); + }); + $viewLink.val(makePermaLink(explorer.state)); + }, + // setup the loader menu in top bar setupLoader: function(callback) { // pre-populate webstore load form with an example url diff --git a/src/view.js b/src/view.js index 27df520e..159ed32e 100644 --- a/src/view.js +++ b/src/view.js @@ -682,6 +682,9 @@ my.composeQueryString = function(queryParams) { var queryString = '?'; var items = []; $.each(queryParams, function(key, value) { + if (typeof(value) === 'object') { + value = JSON.stringify(value); + } items.push(key + '=' + value); }); queryString += items.join('&'); From 3ef664fe7713152ddeda45acb2ebe53fa0ab5d1c Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 16 Apr 2012 20:47:47 +0100 Subject: [PATCH 48/48] [#88,#67,state,view,app][s]: restore was not in fact working correcty for views as we need to pass the state on initialization (we were setting the state later). * Also turn off url updating since I think that was messing with things and we were planning to do that. (For later: reintroduce this as it supports well-behaved navigation) --- src/view.js | 65 ++++++++++++++++++++++++----------------------- test/view.test.js | 3 ++- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/view.js b/src/view.js index 159ed32e..69ba9e34 100644 --- a/src/view.js +++ b/src/view.js @@ -185,6 +185,7 @@ my.DataExplorer = Backbone.View.extend({ var self = this; this.el = $(this.el); // Hash of 'page' views (i.e. those for whole page) keyed by page name + this._setupState(options.state); if (options.views) { this.pageViews = options.views; } else { @@ -192,26 +193,35 @@ my.DataExplorer = Backbone.View.extend({ id: 'grid', label: 'Grid', view: new my.Grid({ - model: this.model - }) + model: this.model, + state: this.state.get('view-grid') + }), }, { id: 'graph', label: 'Graph', view: new my.Graph({ - model: this.model - }) + model: this.model, + state: this.state.get('view-graph') + }), }, { id: 'map', label: 'Map', view: new my.Map({ - model: this.model - }) + model: this.model, + state: this.state.get('view-map') + }), }]; } // these must be called after pageViews are created this.render(); - // should come after render as may need to interact with elements in the view - this._setupState(options.state); + this._bindStateChanges(); + // now do updates based on state (need to come after render) + if (this.state.get('readOnly')) { + this.setReadOnly(); + } + if (this.state.get('currentView')) { + this.updateNav(this.state.get('currentView')); + } this.router = new Backbone.Router(); this.setupRouting(); @@ -227,7 +237,7 @@ my.DataExplorer = Backbone.View.extend({ var qs = my.parseHashQueryString(); qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON()); var out = my.getNewHashForQueryString(qs); - self.router.navigate(out); + // self.router.navigate(out); }); this.model.bind('query:fail', function(error) { my.clearNotifications(); @@ -290,13 +300,15 @@ my.DataExplorer = Backbone.View.extend({ setupRouting: function() { var self = this; // Default route - this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) { - self.updateNav(self.pageViews[0].id, queryString); - }); - $.each(this.pageViews, function(idx, view) { - self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { - self.updateNav(viewId, queryString); - }); +// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) { +// self.updateNav(self.pageViews[0].id, queryString); +// }); +// $.each(this.pageViews, function(idx, view) { +// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { +// self.updateNav(viewId, queryString); +// }); +// }); + this.router.route(/.*/, 'view', function() { }); }, @@ -356,29 +368,18 @@ my.DataExplorer = Backbone.View.extend({ 'view-graph': graphState, backend: this.model.backend.__type__, dataset: this.model.toJSON(), - currentView: this.pageViews[0].id, + currentView: null, readOnly: false }, initialState); this.state = new recline.Model.ObjectState(stateData); + }, - // now do updates based on state - if (this.state.get('readOnly')) { - this.setReadOnly(); - } - if (this.state.get('currentView')) { - this.updateNav(this.state.get('currentView')); - } - _.each(this.pageViews, function(pageView) { - var viewId = 'view-' + pageView.id; - if (viewId in self.state.attributes) { - pageView.view.state.set(self.state.get(viewId)); - } - }); - + _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() { - self.state.set({queryState: self.model.queryState.toJSON()}); + self.state.set({query: self.model.queryState.toJSON()}); }); _.each(this.pageViews, function(pageView) { if (pageView.view.state && pageView.view.state.bind) { diff --git a/test/view.test.js b/test/view.test.js index cb0d4a0c..19fcc177 100644 --- a/test/view.test.js +++ b/test/view.test.js @@ -26,9 +26,10 @@ test('get State', function () { var state = explorer.state; ok(state.get('query')); equal(state.get('readOnly'), false); - equal(state.get('currentView'), 'grid'); + equal(state.get('currentView'), null); equal(state.get('query').size, 100); deepEqual(state.get('view-grid').hiddenFields, []); + deepEqual(state.get('view-graph').group, null); equal(state.get('backend'), 'memory'); ok(state.get('dataset').id !== null); $el.remove();