From cb51f485c01ce8a70f01a5473de41eef3d7671f3 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 15 Apr 2012 16:53:46 +0100 Subject: [PATCH 01/14] [#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 02/14] [#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 03/14] [#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 04/14] [#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 05/14] [#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 06/14] [#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 07/14] [#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 08/14] [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 09/14] [#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 10/14] [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 11/14] [#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 12/14] [#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 13/14] [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 14/14] [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); - }); -} -