diff --git a/dist/recline.js b/dist/recline.js index 238ab5a4..f3d4605a 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -159,8 +159,14 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // // @param {String} s The string to convert // @param {Object} options Options for loading CSV including - // @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported - // @param {String} [separator=','] Separator for CSV file + // @param {Boolean} [trim=false] If set to True leading and trailing + // whitespace is stripped off of each non-quoted field as it is imported + // @param {String} [delimiter=','] A one-character string used to separate + // fields. It defaults to ',' + // @param {String} [quotechar='"'] A one-character string used to quote + // fields containing special characters, such as the delimiter or + // quotechar, or which contain new-line characters. It defaults to '"' + // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parseCSV= function(s, options) { @@ -169,8 +175,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var options = options || {}; var trm = (options.trim === false) ? false : true; - var separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. inQuote = false, @@ -205,7 +211,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; cur = s.charAt(i); // If we are at a EOF or EOR - if (inQuote === false && (cur === separator || cur === "\n")) { + if (inQuote === false && (cur === delimiter || cur === "\n")) { field = processField(field); // Add the current field to the current row row.push(field); @@ -218,8 +224,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; fieldQuoted = false; } else { - // If it's not a delimiter, add it to the field buffer - if (cur !== delimiter) { + // If it's not a quotechar, add it to the field buffer + if (cur !== quotechar) { field += cur; } else { if (!inQuote) { @@ -227,9 +233,9 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; inQuote = true; fieldQuoted = true; } else { - // Next char is delimiter, this is an escaped delimiter - if (s.charAt(i + 1) === delimiter) { - field += delimiter; + // Next char is quotechar, this is an escaped quotechar + if (s.charAt(i + 1) === quotechar) { + field += quotechar; // Skip the next char i += 1; } else { @@ -249,23 +255,48 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; return out; }; - // Converts an array of arrays into a Comma Separated Values string. - // Each array becomes a line in the CSV. + // ### serializeCSV + // + // Convert an Object or a simple array of arrays into a Comma + // Separated Values string. // // Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers. // // @return The array serialized as a CSV // @type String // - // @param {Array} a The array of arrays to convert - // @param {Object} options Options for loading CSV including - // @param {String} [separator=','] Separator for CSV file - // Heavily based on uselesscode's JS CSV parser (MIT Licensed): + // @param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows: + // + // { + // fields: [ {id: .., ...}, {id: ..., + // records: [ { record }, { record }, ... ] + // ... // more attributes we do not care about + // } + // + // @param {object} options Options for serializing the CSV file including + // delimiter and quotechar (see parseCSV options parameter above for + // details on these). + // + // Heavily based on uselesscode's JS CSV serializer (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ - my.serializeCSV= function(a, options) { + my.serializeCSV= function(dataToSerialize, options) { + var a = null; + if (dataToSerialize instanceof Array) { + a = dataToSerialize; + } else { + a = []; + var fieldNames = _.pluck(dataToSerialize.fields, 'id'); + a.push(fieldNames); + _.each(dataToSerialize.records, function(record, index) { + var tmp = _.map(fieldNames, function(fn) { + return record[fn]; + }); + a.push(tmp); + }); + } var options = options || {}; - var separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. field = '', // Buffer for building up the current field @@ -281,7 +312,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { // Convert string to delimited string - field = delimiter + field + delimiter; + field = quotechar + field + quotechar; } else if (typeof field === "number") { // Convert number to string field = field.toString(10); @@ -302,7 +333,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; row = ''; } else { // Add the current field to the current row - row += field + separator; + row += field + delimiter; } // Flush the field buffer field = ''; @@ -1848,26 +1879,27 @@ my.Graph = Backbone.View.extend({ return getFormattedX(x); }; + // infoboxes on mouse hover on points/bars etc var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y; - // it's horizontal so we have to flip - if (self.state.attributes.graphType === 'bars') { - var _tmp = x; - x = y; - y = _tmp; - } - - x = getFormattedX(x); + var x = obj.x; + var y = obj.y; + // it's horizontal so we have to flip + if (self.state.attributes.graphType === 'bars') { + var _tmp = x; + x = y; + y = _tmp; + } + + x = getFormattedX(x); - var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { - group: self.state.attributes.group, - x: x, - series: obj.series.label, - y: y - }); - - return content; + var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { + group: self.state.attributes.group, + x: x, + series: obj.series.label, + y: y + }); + + return content; }; var getFormattedX = function (x) { @@ -1938,18 +1970,18 @@ my.Graph = Backbone.View.extend({ xaxis: yaxis, yaxis: xaxis, mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' + track: true, + relative: true, + trackFormatter: trackFormatter, + fillColor: '#FFFFFF', + fillOpacity: 0.3, + position: 'e' }, bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 + show: true, + horizontal: true, + shadowSize: 0, + barWidth: 0.8 }, }, columns: { @@ -2470,6 +2502,11 @@ this.recline.View = this.recline.View || {}; // latField: {id of field containing latitude in the dataset} // } // +// +// Useful attributes to know about (if e.g. customizing) +// +// * map: the Leaflet map (L.Map) +// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON) my.Map = Backbone.View.extend({ template: ' \
\ @@ -2488,6 +2525,8 @@ my.Map = Backbone.View.extend({ this.el = $(this.el); this.visible = true; this.mapReady = false; + // this will be the Leaflet L.Map object (setup below) + this.map = null; var stateData = _.extend({ geomField: null, @@ -2501,18 +2540,18 @@ my.Map = Backbone.View.extend({ // Listen to changes in the fields this.model.fields.bind('change', function() { - self._setupGeometryField() - self.render() + self._setupGeometryField(); + self.render(); }); // Listen to changes in the records - this.model.records.bind('add', function(doc){self.redraw('add',doc)}); + this.model.records.bind('add', function(doc){self.redraw('add',doc);}); this.model.records.bind('change', function(doc){ self.redraw('remove',doc); self.redraw('add',doc); }); - this.model.records.bind('remove', function(doc){self.redraw('remove',doc)}); - this.model.records.bind('reset', function(){self.redraw('reset')}); + this.model.records.bind('remove', function(doc){self.redraw('remove',doc);}); + this.model.records.bind('reset', function(){self.redraw('reset');}); this.menu = new my.MapMenu({ model: this.model, @@ -2525,6 +2564,34 @@ my.Map = Backbone.View.extend({ this.elSidebar = this.menu.el; }, + // ## Customization Functions + // + // The following methods are designed for overriding in order to customize + // behaviour + + // ### infobox + // + // Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes. + // + // Users should override this function to customize behaviour i.e. + // + // view = new View({...}); + // view.infobox = function(record) { + // ... + // } + infobox: function(record) { + var html = ''; + for (key in record.attributes){ + if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ + html += '
' + key + ': '+ record.attributes[key] + '
'; + } + } + return html; + }, + + // END: Customization section + // ---- + // ### Public: Adds the necessary elements to the page. // // Also sets up the editor fields and the map if necessary. @@ -2612,29 +2679,26 @@ my.Map = Backbone.View.extend({ var count = 0; var wrongSoFar = 0; - _.every(docs,function(doc){ + _.every(docs, function(doc){ count += 1; var feature = self._getGeometryFromRecord(doc); if (typeof feature === 'undefined' || feature === null){ // Empty field return true; } else if (feature instanceof Object){ - // Build popup contents - // TODO: mustache? - html = '' - for (key in doc.attributes){ - if (!(self.state.get('geomField') && key == self.state.get('geomField'))){ - 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; + feature.properties = { + popupContent: self.infobox(doc), + // Add a reference to the model id, which will allow us to + // link this Leaflet layer to a Recline doc + cid: doc.cid + }; try { - self.features.addGeoJSON(feature); + self.features.addData(feature); + + if (feature.properties && feature.properties.popupContent) { + self.features.bindPopup(feature.properties.popupContent); + } } catch (except) { wrongSoFar += 1; var msg = 'Wrong geometry value'; @@ -2644,7 +2708,7 @@ my.Map = Backbone.View.extend({ } } } else { - wrongSoFar += 1 + wrongSoFar += 1; if (wrongSoFar <= 10) { self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } @@ -2663,7 +2727,7 @@ my.Map = Backbone.View.extend({ _.each(docs,function(doc){ for (key in self.features._layers){ - if (self.features._layers[key].cid == doc.cid){ + if (self.features._layers[key].feature.properties.cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); } } @@ -2762,10 +2826,10 @@ my.Map = Backbone.View.extend({ // _zoomToFeatures: function(){ var bounds = this.features.getBounds(); - if (bounds){ + if (bounds.getNorthEast()){ this.map.fitBounds(bounds); } else { - this.map.setView(new L.LatLng(0, 0), 2); + this.map.setView([0, 0], 2); } }, @@ -2779,39 +2843,13 @@ my.Map = Backbone.View.extend({ 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'}); + 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 will be available in the next Leaflet stable release. - // In the meantime we add it manually to our layer. - this.features.getBounds = function(){ - var bounds = new L.LatLngBounds(); - this._iterateLayers(function (layer) { - if (layer instanceof L.Marker){ - bounds.extend(layer.getLatLng()); - } else { - if (layer.getBounds){ - bounds.extend(layer.getBounds().getNorthEast()); - bounds.extend(layer.getBounds().getSouthWest()); - } - } - }, this); - return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null; - } - this.map.addLayer(this.features); - this.map.setView(new L.LatLng(0, 0), 2); + this.map.setView([0, 0], 2); this.mapReady = true; }, @@ -3111,8 +3149,9 @@ my.MultiView = Backbone.View.extend({
\ \
\ @@ -3142,28 +3181,28 @@ my.MultiView = Backbone.View.extend({ view: new my.SlickGrid({ model: this.model, state: this.state.get('view-grid') - }), + }) }, { id: 'graph', label: 'Graph', view: new my.Graph({ model: this.model, state: this.state.get('view-graph') - }), + }) }, { id: 'map', label: 'Map', view: new my.Map({ model: this.model, state: this.state.get('view-map') - }), + }) }, { id: 'timeline', label: 'Timeline', view: new my.Timeline({ model: this.model, state: this.state.get('view-timeline') - }), + }) }, { id: 'transform', label: 'Transform', @@ -3246,6 +3285,7 @@ my.MultiView = Backbone.View.extend({ render: function() { var tmplData = this.model.toTemplateJSON(); tmplData.views = this.pageViews; + tmplData.sidebarViews = this.sidebarViews; var template = Mustache.render(this.template, tmplData); $(this.el).html(template); @@ -3265,7 +3305,7 @@ my.MultiView = Backbone.View.extend({ _.each(this.sidebarViews, function(view) { this['$'+view.id] = view.view.el; $dataSidebar.append(view.view.el); - }); + }, this); var pager = new recline.View.Pager({ model: this.model.queryState @@ -3308,13 +3348,7 @@ my.MultiView = Backbone.View.extend({ _onMenuClick: function(e) { e.preventDefault(); var action = $(e.target).attr('data-action'); - if (action === 'filters') { - this.$filterEditor.toggle(); - } else if (action === 'fields') { - this.$fieldsView.toggle(); - } else if (action === 'transform') { - this.transformView.el.toggle(); - } + this['$'+action].toggle(); }, _onSwitchView: function(e) { @@ -3379,7 +3413,7 @@ my.MultiView = Backbone.View.extend({ var self = this; _.each(this.pageViews, function(pageView) { pageView.view.bind('recline:flash', function(flash) { - self.notify(flash); + self.notify(flash); }); }); }, @@ -3401,14 +3435,15 @@ my.MultiView = Backbone.View.extend({ }, flash ); + var _template; if (tmplData.loader) { - var _template = ' \ + _template = ' \
\ {{message}} \   \
'; } else { - var _template = ' \ + _template = ' \
× \ {{message}} \
'; diff --git a/docs/src/backend.ckan.html b/docs/src/backend.ckan.html new file mode 100644 index 00000000..0727be5b --- /dev/null +++ b/docs/src/backend.ckan.html @@ -0,0 +1,87 @@ + backend.ckan.js
Jump To …

backend.ckan.js

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

CKAN Backend

+ +

This provides connection to the CKAN DataStore (v2)

+ +

General notes

+ +
    +
  • Every dataset must have an id equal to its resource id on the CKAN instance
  • +
  • You should set the CKAN API endpoint for requests by setting APIENDPOINT value on this module (recline.Backend.Ckan.APIENDPOINT)
  • +
  my.__type__ = 'ckan';

Default CKAN API endpoint used for requests (you can change this but it will affect every request!)

  my.API_ENDPOINT = 'http://datahub.io/api';

fetch

  my.fetch = function(dataset) {
+    var wrapper = my.DataStore();
+    var dfd = $.Deferred();
+    var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
+    jqxhr.done(function(results) {

map ckan types to our usual types ...

      var fields = _.map(results.result.fields, function(field) {
+        field.type = field.type in CKAN_TYPES_MAP ? CKAN_TYPES_MAP[field.type] : field.type;
+        return field;
+      });
+      var out = {
+        fields: fields,
+        useMemoryStore: false
+      };
+      dfd.resolve(out);  
+    });
+    return dfd.promise();
+  };

only put in the module namespace so we can access for tests!

  my._normalizeQuery = function(queryObj, dataset) {
+    var actualQuery = {
+      resource_id: dataset.id,
+      q: queryObj.q,
+      limit: queryObj.size || 10,
+      offset: queryObj.from || 0
+    };
+    if (queryObj.sort && queryObj.sort.length > 0) {
+      var _tmp = _.map(queryObj.sort, function(sortObj) {
+        return sortObj.field + ' ' + (sortObj.order || '');
+      });
+      actualQuery.sort = _tmp.join(',');
+    }
+    return actualQuery;
+  }
+
+  my.query = function(queryObj, dataset) {
+    var actualQuery = my._normalizeQuery(queryObj, dataset);
+    var wrapper = my.DataStore();
+    var dfd = $.Deferred();
+    var jqxhr = wrapper.search(actualQuery);
+    jqxhr.done(function(results) {
+      var out = {
+        total: results.result.total,
+        hits: results.result.records,
+      };
+      dfd.resolve(out);  
+    });
+    return dfd.promise();
+  };

DataStore

+ +

Simple wrapper around the CKAN DataStore API

+ +

@param endpoint: CKAN api endpoint (e.g. http://datahub.io/api)

  my.DataStore = function(endpoint) { 
+    var that = {
+      endpoint: endpoint || my.API_ENDPOINT
+    };
+    that.search = function(data) {
+      var searchUrl = that.endpoint + '/3/action/datastore_search';
+      var jqxhr = $.ajax({
+        url: searchUrl,
+        data: data,
+        dataType: 'json'
+      });
+      return jqxhr;
+    }
+
+    return that;
+  }
+
+  var CKAN_TYPES_MAP = {
+    'int4': 'integer',
+    'float8': 'float',
+    'text': 'string'
+  };
+
+}(jQuery, this.recline.Backend.Ckan));
+
+
\ No newline at end of file diff --git a/docs/src/backend.couchdb.html b/docs/src/backend.couchdb.html index c2602fdf..f126f2d1 100644 --- a/docs/src/backend.couchdb.html +++ b/docs/src/backend.couchdb.html @@ -1,17 +1,17 @@ - backend.couchdb.js
Jump To …

backend.couchdb.js

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

Backbone connector for a CouchDB backend.

-

Usage:

- -

var backend = new recline.Backend.CouchDB(); -var dataset = new recline.Model.Dataset({ - dburl: '/couchdb/mydb', - viewurl: '/couchdb/mydb/design/design1/views/view1', - queryoptions: { - 'key': 'somedocument_key' - } +

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

-

Alternatively: -var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); +backend.query(query, dataset.toJSON()).done(function () { ... }); +

+ +

Alternatively:

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

+var results = dataset.query(query_obj); +
-

Additionally, the Dataset instance may define three methods: - function recordupdate (record, document) { ... } +

Additionally, the Dataset instance may define three methods:

+ +

function recordupdate (record, document) { ... } function recorddelete (record, document) { ... } - function recordcreate (record, document) { ... } -Where record is the JSON representation of the Record/Document instance + function record_create (record, document) { ... }

+ +

Where record is the JSON representation of the Record/Document instance and document is the JSON document stored in couchdb. -When _alldocs view is used (default), a record is the same as a document +When alldocs view is used (default), a record is the same as a document so these methods need not be defined. They are most useful when using a custom view that performs a map-reduce operation on each document to yield a record. Hence, when the record is @@ -422,8 +425,8 @@ we should remove it before sending it to the server.

dfd.reject(args); }); return dfd.promise(); + } }; - }(jQuery, this.recline.Backend.CouchDB));

backend.couchdb.js

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

CouchDB Wrapper

+(function($, my) { +my.__type__ = 'couchdb';

CouchDB Wrapper

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

-
http://localhost:5984/ckan-std
+

TODO Add user/password arguments for couchdb authentication support.

-

TODO Add user/password arguments for couchdb authentication support.

  my.CouchDBWrapper = function(db_url, view_url, options) { 
+

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

  my.CouchDBWrapper = function(db_url, view_url, options) { 
     var self = this;
     self.endpoint = db_url;
     self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs';
@@ -124,31 +124,34 @@ See: http://wiki.apache.org/couchdb/HTTPviewAPI

\ No newline at end of file diff --git a/docs/src/backend.csv.html b/docs/src/backend.csv.html index c0092c84..33c222f2 100644 --- a/docs/src/backend.csv.html +++ b/docs/src/backend.csv.html @@ -1,4 +1,4 @@ - backend.csv.js

backend.csv.js

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

backend.csv.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.CSV = this.recline.Backend.CSV || {};
 
@@ -57,15 +57,21 @@ Each line in the CSV becomes an array.

@param {String} s The string to convert @param {Object} options Options for loading CSV including - @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported -@param {String} [separator=','] Separator for CSV file -Heavily based on uselesscode's JS CSV parser (MIT Licensed): + @param {Boolean} [trim=false] If set to True leading and trailing + whitespace is stripped off of each non-quoted field as it is imported + @param {String} [delimiter=','] A one-character string used to separate + fields. It defaults to ',' + @param {String} [quotechar='"'] A one-character string used to quote + fields containing special characters, such as the delimiter or + quotechar, or which contain new-line characters. It defaults to '"'

+ +

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

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
 
     var options = options || {};
     var trm = (options.trim === false) ? false : true;
-    var separator = options.separator || ',';
-    var delimiter = options.delimiter || '"';
+    var delimiter = options.delimiter || ',';
+    var quotechar = options.quotechar || '"';
 
     var cur = '', // The character we are currently processing.
       inQuote = false,
@@ -90,19 +96,19 @@ http://www.uselesscode.org/javascript/csv/

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

If we are at a EOF or EOR

      if (inQuote === false && (cur === separator || cur === "\n")) {
+      cur = s.charAt(i);

If we are at a EOF or EOR

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

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

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

Flush the field buffer

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

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

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

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

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

We are not in a quote, start a quote

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

Next char is delimiter, this is an escaped delimiter

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

Skip the next char

              i += 1;
+          } else {

Next char is quotechar, this is an escaped quotechar

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

Skip the next char

              i += 1;
             } else {

It's not escaping, so end quote

              inQuote = false;
             }
           }
@@ -113,22 +119,48 @@ http://www.uselesscode.org/javascript/csv/

out.push(row); return out; - };

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

+ };

serializeCSV

+ +

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

Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers.

@return The array serialized as a CSV @type String

-

@param {Array} a The array of arrays to convert -@param {Object} options Options for loading CSV including -@param {String} [separator=','] Separator for CSV file -Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

  my.serializeCSV= function(a, options) {
+

@param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows:

+ +
{
+  fields: [ {id: .., ...}, {id: ..., 
+  records: [ { record }, { record }, ... ]
+  ... // more attributes we do not care about
+}
+
+ +

@param {object} options Options for serializing the CSV file including + delimiter and quotechar (see parseCSV options parameter above for + details on these).

+ +

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

  my.serializeCSV= function(dataToSerialize, options) {
+    var a = null;
+    if (dataToSerialize instanceof Array) {
+      a = dataToSerialize;
+    } else {
+      a = [];
+      var fieldNames = _.pluck(dataToSerialize.fields, 'id');
+      a.push(fieldNames);
+      _.each(dataToSerialize.records, function(record, index) {
+        var tmp = _.map(fieldNames, function(fn) {
+          return record[fn];
+        });
+        a.push(tmp);
+      });
+    }
     var options = options || {};
-    var separator = options.separator || ',';
-    var delimiter = options.delimiter || '"';
+    var delimiter = options.delimiter || ',';
+    var quotechar = options.quotechar || '"';
 
     var cur = '', // The character we are currently processing.
       field = '', // Buffer for building up the current field
@@ -140,7 +172,7 @@ http://www.uselesscode.org/javascript/csv/

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

If field is null set to empty string

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

Convert string to delimited string

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

Convert string to delimited string

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

Convert number to string

        field = field.toString(10);
       }
 
@@ -155,7 +187,7 @@ http://www.uselesscode.org/javascript/csv/

row += field; out += row + "\n"; row = ''; - } else {

Add the current field to the current row

          row += field + separator;
+        } else {

Add the current field to the current row

          row += field + delimiter;
         }

Flush the field buffer

        field = '';
       }
     }
diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html
index f0232bf4..80c1bc89 100644
--- a/docs/src/backend.dataproxy.html
+++ b/docs/src/backend.dataproxy.html
@@ -1,4 +1,4 @@
-      backend.dataproxy.js           

backend.dataproxy.js

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

backend.dataproxy.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
 
diff --git a/docs/src/backend.elasticsearch.html b/docs/src/backend.elasticsearch.html
index ea0e1344..e8f2cedf 100644
--- a/docs/src/backend.elasticsearch.html
+++ b/docs/src/backend.elasticsearch.html
@@ -1,4 +1,4 @@
-      backend.elasticsearch.js           

backend.elasticsearch.js

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

backend.elasticsearch.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
 
@@ -104,6 +104,16 @@ on http://localhost:9200 with index twitter and type tweet it would be:

}); } return out; + },

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

    this._normalizeSort = function(sort) {
+      var out = _.map(sort, function(sortObj) {
+        var _tmp = {};
+        var _tmp2 = _.clone(sortObj);
+        delete _tmp2['field'];
+        _tmp[sortObj.field] = _tmp2;
+        return _tmp;
+      });
+      return out;
     },
 
     this._convertFilter = function(filter) {
@@ -117,14 +127,16 @@ on http://localhost:9200 with index twitter and type tweet it would be:

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

query

+ },

query

@return deferred supporting promise API

    this.query = function(queryObj) {
       var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
-      var queryNormalized = this._normalizeQuery(queryObj);
+      esQuery.query = this._normalizeQuery(queryObj);
       delete esQuery.q;
       delete esQuery.filters;
-      esQuery.query = queryNormalized;
+      if (esQuery.sort && esQuery.sort.length > 0) {
+        esQuery.sort = this._normalizeSort(esQuery.sort);
+      }
       var data = {source: JSON.stringify(esQuery)};
       var url = this.endpoint + '/_search';
       var jqxhr = makeRequest({
@@ -134,10 +146,10 @@ on http://localhost:9200 with index twitter and type tweet it would be:

}); return jqxhr; } - };

Recline Connectors

+ };

Recline Connectors

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

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

  my.esOptions = {};

fetch

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

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

  my.esOptions = {};

fetch

  my.fetch = function(dataset) {
     var es = new my.Wrapper(dataset.url, my.esOptions);
     var dfd = $.Deferred();
     es.mapping().done(function(schema) {
@@ -145,7 +157,7 @@ via the url attribute.

if (!schema){ dfd.reject({'message':'Elastic Search did not return a mapping'}); return; - }

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

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

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

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

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

save

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

save

  my.save = function(changes, dataset) {
     var es = new my.Wrapper(dataset.url, my.esOptions);
     if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
       var dfd = $.Deferred();
@@ -175,7 +187,7 @@ via the url attribute.

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

query

  my.query = function(queryObj, dataset) {
+  };

query

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

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

makeRequest

+ };

makeRequest

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

diff --git a/docs/src/backend.gdocs.html b/docs/src/backend.gdocs.html index 9425ca1d..e05f0b2f 100644 --- a/docs/src/backend.gdocs.html +++ b/docs/src/backend.gdocs.html @@ -1,4 +1,4 @@ - backend.gdocs.js

backend.gdocs.js

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

backend.gdocs.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
 
diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html
index 2ea8beb5..248eca99 100644
--- a/docs/src/backend.memory.html
+++ b/docs/src/backend.memory.html
@@ -1,4 +1,4 @@
-      backend.memory.js           

backend.memory.js

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

backend.memory.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.Memory = this.recline.Backend.Memory || {};
 
@@ -60,13 +60,14 @@ from the data.

var results = this.data; results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj);

not complete sorting!

      _.each(queryObj.sort, function(sortObj) {
-        var fieldName = _.keys(sortObj)[0];
+      results = this._applyFreeTextQuery(results, queryObj);

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

      _.each(queryObj.sort, function(sortObj) {
+        var fieldName = sortObj.field;
         results = _.sortBy(results, function(doc) {
           var _out = doc[fieldName];
           return _out;
         });
-        if (sortObj[fieldName].order == 'desc') {
+        if (sortObj.order == 'desc') {
           results.reverse();
         }
       });
diff --git a/docs/src/data.transform.html b/docs/src/data.transform.html
index ef709101..62a7fa8e 100644
--- a/docs/src/data.transform.html
+++ b/docs/src/data.transform.html
@@ -1,4 +1,4 @@
-      data.transform.js           

data.transform.js

this.recline = this.recline || {};
+      data.transform.js           

data.transform.js

this.recline = this.recline || {};
 this.recline.Data = this.recline.Data || {};
 
 (function(my) {

adapted from https://github.com/harthur/costco. heather rules

my.Transform = {};
diff --git a/docs/src/model.html b/docs/src/model.html
index 26572c1a..47cd7aab 100644
--- a/docs/src/model.html
+++ b/docs/src/model.html
@@ -1,4 +1,4 @@
-      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) {

Dataset

my.Dataset = Backbone.Model.extend({
@@ -233,37 +233,12 @@ also returned.

} return backend; } -});

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: {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) {
-  var dataset = null;

hack-y - restoring a memory dataset does not mean much ...

  if (state.backend === 'memory') {
-    var datasetInfo = {
-      records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
-    };
-  } else {
-    var datasetInfo = {
-      url: state.url,
-      backend: state.backend
-    };
-  }
-  dataset = new recline.Model.Dataset(datasetInfo);
-  return dataset;
-};

A Record

+});

A Record

A single record (or row) in the dataset

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

initialize

+ },

initialize

Create a Record

@@ -272,7 +247,7 @@ Dataset e.g. in query method

Certain methods require presence of a fields attribute (identical to that on Dataset)

  initialize: function() {
     _.bindAll(this, 'getFieldValue');
-  },

getFieldValue

+ },

getFieldValue

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

  getFieldValue: function(field) {
@@ -281,7 +256,7 @@ for this record.

val = field.renderer(val, field, this.toJSON()); } return val; - },

getFieldValueUnrendered

+ },

getFieldValueUnrendered

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

  getFieldValueUnrendered: function(field) {
@@ -290,7 +265,7 @@ for this record.

val = field.deriver(val, field, this); } return val; - },

summary

+ },

summary

Get a simple html summary of this record in form of key/value list

  summary: function(record) {
     var self = this;
@@ -302,30 +277,30 @@ for this record.

}); html += '</div>'; return html; - },

Override Backbone save, fetch and destroy so they do nothing + },

Override Backbone save, fetch and destroy so they do nothing Instead, Dataset object that created this Record should take care of handling these changes (discovery will occur via event notifications) WARNING: these will not persist unless you call save on Dataset

  fetch: function() {},
   save: function() {},
   destroy: function() { this.trigger('destroy', this); }
-});

A Backbone collection of Records

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

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
   constructor: function RecordList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Record
-});

A Field (aka Column) on a Dataset

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

A Field (aka Column) on a Dataset

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

defaults - define default values

  defaults: {
+  },

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) {
@@ -366,7 +341,7 @@ WARNING: these will not persist unless you call save on Dataset

} } else if (format == 'plain') { return val; - } else {

as this is the default and default type is string may get things + } else {

as this is the default and default type is string may get things here that are not actually strings

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

Backbone.Collection.prototype.constructor.apply(this, arguments); }, model: my.Field -});

Query

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

Query

my.Query = Backbone.Model.extend({
   constructor: function Query() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -396,7 +371,7 @@ here that are not actually strings

}, _filterTemplates: { term: { - type: 'term',

TODO do we need this attribute here?

      field: '',
+      type: 'term',

TODO do we need this attribute here?

      field: '',
       term: ''
     },
     range: {
@@ -413,11 +388,11 @@ here that are not actually strings

lat: 0 } } - },

addFilter

+ },

addFilter

Add a new filter (appended to the list of filters)

-

@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

  addFilter: function(filter) {

crude deep copy

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

not full specified so use template and over-write +

@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

  addFilter: function(filter) {

crude deep copy

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

not full specified so use template and over-write 3 as for 'type', 'field' and 'fieldType'

    if (_.keys(filter).length <= 3) {
       ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
     }
@@ -426,19 +401,19 @@ here that are not actually strings

this.trigger('change:filters:new-blank'); }, updateFilter: function(index, value) { - },

removeFilter

+ },

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)) {
+    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] = {
@@ -458,7 +433,7 @@ here that are not actually strings

this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); } -});

A Facet (Result)

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

A Facet (Result)

my.Facet = Backbone.Model.extend({
   constructor: function Facet() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -471,15 +446,15 @@ here that are not actually strings

terms: [] }; } -});

A Collection/List of Facets

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

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
   constructor: function FacetList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Facet
-});

Object State

+});

Object State

Convenience Backbone model for storing (configuration) state of objects like Views.

my.ObjectState = Backbone.Model.extend({
-});

Backbone.sync

+});

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);
diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html
index 62c65a23..8fd0d659 100644
--- a/docs/src/view.graph.html
+++ b/docs/src/view.graph.html
@@ -1,4 +1,4 @@
-      view.graph.js           

view.graph.js

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

view.graph.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
@@ -102,29 +102,28 @@ generate the element itself (you can then append view.el to the DOM.

var tickFormatter = function (x) { return getFormattedX(x); }; - - var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y;

it's horizontal so we have to flip

          if (self.state.attributes.graphType === 'bars') {
-            var _tmp = x;
-            x = y;
-            y = _tmp;
-          }
-          
-          x = getFormattedX(x);
+    

infoboxes on mouse hover on points/bars etc

    var trackFormatter = function (obj) {
+      var x = obj.x;
+      var y = obj.y;

it's horizontal so we have to flip

      if (self.state.attributes.graphType === 'bars') {
+        var _tmp = x;
+        x = y;
+        y = _tmp;
+      }
+      
+      x = getFormattedX(x);
 
-          var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
-            group: self.state.attributes.group,
-            x: x,
-            series: obj.series.label,
-            y: y
-          });
-        
-        return content;
+      var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
+        group: self.state.attributes.group,
+        x: x,
+        series: obj.series.label,
+        y: y
+      });
+      
+      return content;
     };
     
     var getFormattedX = function (x) {
-      var xfield = self.model.fields.get(self.state.attributes.group);

time series

      var isDateTime = xfield.get('type') === 'date';
+      var xfield = self.model.fields.get(self.state.attributes.group);

time series

      var isDateTime = xfield.get('type') === 'date';
 
       if (self.model.records.models[parseInt(x)]) {
         x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
@@ -151,7 +150,7 @@ generate the element itself (you can then append view.el to the DOM.

var legend = {}; legend.position = 'ne'; -

mouse.lineColor is set in createSeries

    var optionsPerGraphType = { 
+    

mouse.lineColor is set in createSeries

    var optionsPerGraphType = { 
       lines: {
         legend: legend,
         colors: this.graphColors,
@@ -186,18 +185,18 @@ generate the element itself (you can then append view.el to the DOM.

xaxis: yaxis, yaxis: xaxis, mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' + track: true, + relative: true, + trackFormatter: trackFormatter, + fillColor: '#FFFFFF', + fillOpacity: 0.3, + position: 'e' }, bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 + show: true, + horizontal: true, + shadowSize: 0, + barWidth: 0.8 }, }, columns: { @@ -233,12 +232,12 @@ generate the element itself (you can then append view.el to the DOM.

var points = []; _.each(self.model.records.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); - var x = doc.getFieldValue(xfield);

time series

        var isDateTime = xfield.get('type') === 'date';
+        var x = doc.getFieldValue(xfield);

time series

        var isDateTime = xfield.get('type') === 'date';
         
-        if (isDateTime) {

datetime

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

not bar or column

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

bar or column

            x = index;
+        if (isDateTime) {

datetime

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

not bar or column

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

bar or column

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

string

          x = parseFloat(x);
+        } else if (typeof x === 'string') {

string

          x = parseFloat(x);
           if (isNaN(x)) {
             x = index;
           }
@@ -246,7 +245,7 @@ generate the element itself (you can then append view.el to the DOM.

var yfield = self.model.fields.get(field); var y = doc.getFieldValue(yfield); -

horizontal bar chart

        if (self.state.attributes.graphType == 'bars') {
+        

horizontal bar chart

        if (self.state.attributes.graphType == 'bars') {
           points.push([y, x]);
         } else {
           points.push([x, y]);
@@ -330,12 +329,12 @@ generate the element itself (you can then append view.el to the DOM.

var self = this; var tmplData = this.model.toTemplateJSON(); var htmls = Mustache.render(this.template, tmplData); - this.el.html(htmls);

set up editor from state

    if (this.state.get('graphType')) {
+    this.el.html(htmls);

set up editor from state

    if (this.state.get('graphType')) {
       this._selectOption('.editor-type', this.state.get('graphType'));
     }
     if (this.state.get('group')) {
       this._selectOption('.editor-group', this.state.get('group'));
-    }

ensure at least one series box shows up

    var tmpSeries = [""];
+    }

ensure at least one series box shows up

    var tmpSeries = [""];
     if (this.state.get('series').length > 0) {
       tmpSeries = this.state.get('series');
     }
@@ -344,7 +343,7 @@ generate the element itself (you can then append view.el to the DOM.

self._selectOption('.editor-series.js-series-' + idx, series); }); return this; - },

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

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

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

  _selectOption: function(id,value){
     var options = this.el.find(id + ' select > option');
     if (options) {
       options.each(function(opt){
@@ -369,7 +368,7 @@ generate the element itself (you can then append view.el to the DOM.

graphType: this.el.find('.editor-type select').val() }; this.state.set(updatedState); - },

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

+ },

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

@param [int] idx index of this series in the list of series

@@ -387,7 +386,7 @@ generate the element itself (you can then append view.el to the DOM.

_onAddSeries: function(e) { e.preventDefault(); this.addSeries(this.state.get('series').length); - },

Public: Removes a series list item from the editor.

+ },

Public: Removes a series list item from the editor.

Also updates the labels of the remaining series elements.

  removeSeries: function (e) {
     e.preventDefault();
diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html
index 7e613802..02c7230a 100644
--- a/docs/src/view.grid.html
+++ b/docs/src/view.grid.html
@@ -1,4 +1,4 @@
-      view.grid.js           

view.grid.js

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

view.grid.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
diff --git a/docs/src/view.map.html b/docs/src/view.map.html
index b30e2692..9dc80dde 100644
--- a/docs/src/view.map.html
+++ b/docs/src/view.map.html
@@ -1,4 +1,4 @@
-      view.map.js           

view.map.js

/*jshint multistr:true */
+      view.map.js           
this.map.addLayer(bg);this.features=newL.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; - } - - }); <input type="hidden" class="editor-id" value="map-1" /> \ </div> \ </form> \ -',this.state=newrecline.Model.ObjectState(options.state);this.state.bind('change',this.render);this.render(); - },_geomReady:function(){returnBoolean(this.state.get('geomField')||(this.state.get('latField')&&this.state.get('lonField'))); - },

view.map.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
@@ -20,7 +20,14 @@ have the following (optional) configuration options:

lonField: {id of field containing longitude in the dataset} latField: {id of field containing latitude in the dataset} } -
my.Map = Backbone.View.extend({
+
+ +

Useful attributes to know about (if e.g. customizing)

+ +
    +
  • map: the Leaflet map (L.Map)
  • +
  • features: Leaflet GeoJSON layer containing all the features (L.GeoJSON)
  • +
my.Map = Backbone.View.extend({
   template: ' \
     <div class="recline-map"> \
       <div class="panel map"></div> \
@@ -34,7 +41,7 @@ If not found, the user will need to define the fields via the editor.

var self = this; this.el = $(this.el); this.visible = true; - this.mapReady = false; + this.mapReady = false;

this will be the Leaflet L.Map object (setup below)

    this.map = null;
 
     var stateData = _.extend({
         geomField: null,
@@ -44,16 +51,16 @@ If not found, the user will need to define the fields via the editor.

}, options.state ); - this.state = new recline.Model.ObjectState(stateData);

Listen to changes in the fields

    this.model.fields.bind('change', function() {
-      self._setupGeometryField()
-      self.render()
-    });

Listen to changes in the records

    this.model.records.bind('add', function(doc){self.redraw('add',doc)});
+    this.state = new recline.Model.ObjectState(stateData);

Listen to changes in the fields

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

Listen to changes in the records

    this.model.records.bind('add', function(doc){self.redraw('add',doc);});
     this.model.records.bind('change', function(doc){
         self.redraw('remove',doc);
         self.redraw('add',doc);
     });
-    this.model.records.bind('remove', function(doc){self.redraw('remove',doc)});
-    this.model.records.bind('reset', function(){self.redraw('reset')});
+    this.model.records.bind('remove', function(doc){self.redraw('remove',doc);});
+    this.model.records.bind('reset', function(){self.redraw('reset');});
 
     this.menu = new my.MapMenu({
       model: this.model,
@@ -64,7 +71,28 @@ If not found, the user will need to define the fields via the editor.

self.redraw(); }); this.elSidebar = this.menu.el; - },

Public: Adds the necessary elements to the page.

+ },

Customization Functions

+ +

The following methods are designed for overriding in order to customize +behaviour

infobox

+ +

Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.

+ +

Users should override this function to customize behaviour i.e.

+ +
view = new View({...});
+view.infobox = function(record) {
+  ...
+}
+
  infobox: function(record) {
+    var html = '';
+    for (key in record.attributes){
+      if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
+        html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
+      }
+    }
+    return html;
+  },

END: Customization section

Public: Adds the necessary elements to the page.

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

  render: function() {
     var self = this;
@@ -74,7 +102,7 @@ If not found, the user will need to define the fields via the editor.

this.$map = this.el.find('.panel.map'); this.redraw(); return this; - },

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

+ },

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

Actions can be:

@@ -85,7 +113,7 @@ If not found, the user will need to define the fields via the editor.

  • refresh: Clear existing features and add all current records
  •   redraw: function(action, doc){
         var self = this;
    -    action = action || 'refresh';

    try to set things up if not already

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

    try to set things up if not already

        if (!self._geomReady()){
           self._setupGeometryField();
         }
         if (!self.mapReady){
    @@ -111,7 +139,7 @@ If not found, the user will need to define the fields via the editor.

    } }, - show: function() {

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

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

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

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

    Private: Add one or n features to the map

    + },

    Private: Add one or n features to the map

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

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

    Empty field

            return true;
    -      } else if (feature instanceof Object){

    Build popup contents -TODO: mustache?

            html = ''
    -        for (key in doc.attributes){
    -          if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
    -            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;
    +      if (typeof feature === 'undefined' || feature === null){

    Empty field

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

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

              cid: doc.cid
    +        };
     
             try {
    -          self.features.addGeoJSON(feature);
    +          self.features.addData(feature);
    +
    +          if (feature.properties && feature.properties.popupContent) {
    +            self.features.bindPopup(feature.properties.popupContent);
    +          }
             } catch (except) {
               wrongSoFar += 1;
               var msg = 'Wrong geometry value';
    @@ -166,14 +194,14 @@ link this Leaflet layer to a Recline doc

    } } } else { - wrongSoFar += 1 + wrongSoFar += 1; if (wrongSoFar <= 10) { self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } } return true; }); - },

    Private: Remove one or n features to the map

      _remove: function(docs){
    +  },

    Private: Remove one or n features to the map

      _remove: function(docs){
     
         var self = this;
     
    @@ -181,16 +209,16 @@ link this Leaflet layer to a Recline doc

    _.each(docs,function(doc){ for (key in self.features._layers){ - if (self.features._layers[key].cid == doc.cid){ + if (self.features._layers[key].feature.properties.cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); } } }); - },

    Private: Return a GeoJSON geomtry extracted from the record fields

      _getGeometryFromRecord: function(doc){
    +  },

    Private: Return a GeoJSON geomtry extracted from the record fields

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

    We may have a GeoJSON string representation

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

    We may have a GeoJSON string representation

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

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

    [ lon, lat ]

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

    [ lon, lat ]

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

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

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

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

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

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

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

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

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

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

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

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

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

    } } return null; - },

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

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

    -

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

      _setupGeometryField: function(){

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

        if (!this._geomReady()) {
    +

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

      _setupGeometryField: function(){

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

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

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

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

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

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

    } } return null; - },

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

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

      _zoomToFeatures: function(){
         var bounds = this.features.getBounds();
    -    if (bounds){
    +    if (bounds.getNorthEast()){
           this.map.fitBounds(bounds);
         } else {
    -      this.map.setView(new L.LatLng(0, 0), 2);
    +      this.map.setView([0, 0], 2);
         }
    -  },

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

    + },

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

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

      _setupMap: function(){
    @@ -269,36 +297,12 @@ on OpenStreetMap.

    This will be available in the next Leaflet stable release. -In the meantime we add it manually to our layer.

        this.features.getBounds = function(){
    -      var bounds = new L.LatLngBounds();
    -      this._iterateLayers(function (layer) {
    -        if (layer instanceof L.Marker){
    -          bounds.extend(layer.getLatLng());
    -        } else {
    -          if (layer.getBounds){
    -            bounds.extend(layer.getBounds().getNorthEast());
    -            bounds.extend(layer.getBounds().getSouthWest());
    -          }
    -        }
    -      }, this);
    -      return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
    -    }
    -
         this.map.addLayer(this.features);
     
    -    this.map.setView(new L.LatLng(0, 0), 2);
    +    this.map.setView([0, 0], 2);
     
         this.mapReady = true;
    -  },

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

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

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

      _selectOption: function(id,value){
         var options = $('.' + id + ' > select > option');
         if (options){
           options.each(function(opt){
    @@ -368,7 +372,7 @@ In the meantime we add it manually to our layer.

    Define here events for UI elements

      events: {
    +',

    Define here events for UI elements

      events: {
         'click .editor-update-map': 'onEditorSubmit',
         'change .editor-field-type': 'onFieldTypeChange',
         'click #editor-auto-zoom': 'onAutoZoomChange'
    @@ -382,7 +386,7 @@ In the meantime we add it manually to our layer.

    Public: Adds the necessary elements to the page.

    + },

    Public: Adds the necessary elements to the page.

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

      render: function() {
         var self = this;
    @@ -410,7 +414,7 @@ In the meantime we add it manually to our layer.

    UI Event handlers

    Public: Update map with user options

    + },

    UI Event handlers

    Public: Update map with user options

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

      onEditorSubmit: function(e){
    @@ -429,7 +433,7 @@ location information.

    }); } return false; - },

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

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

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

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

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

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

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

      _selectOption: function(id,value){
         var options = this.el.find('.' + id + ' > select > option');
         if (options){
           options.each(function(opt){
    diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
    index 902aff81..20ce53b2 100644
    --- a/docs/src/view.multiview.html
    +++ b/docs/src/view.multiview.html
    @@ -1,4 +1,4 @@
    -      view.multiview.js           

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

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

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

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

    MultiView

    @@ -106,8 +106,9 @@ initialized the MultiView with the relevant views themselves.

    </div> \ <div class="menu-right"> \ <div class="btn-group" data-toggle="buttons-checkbox"> \ - <a href="#" class="btn active" data-action="filters">Filters</a> \ - <a href="#" class="btn active" data-action="fields">Fields</a> \ + {{#sidebarViews}} \ + <a href="#" data-action="{{id}}" class="btn active">{{label}}</a> \ + {{/sidebarViews}} \ </div> \ </div> \ <div class="query-editor-here" style="display:inline;"></div> \ @@ -134,28 +135,28 @@ initialized the MultiView with the relevant views themselves.

    view: new my.SlickGrid({ model: this.model, state: this.state.get('view-grid') - }), + }) }, { id: 'graph', label: 'Graph', view: new my.Graph({ model: this.model, state: this.state.get('view-graph') - }), + }) }, { id: 'map', label: 'Map', view: new my.Map({ model: this.model, state: this.state.get('view-map') - }), + }) }, { id: 'timeline', label: 'Timeline', view: new my.Timeline({ model: this.model, state: this.state.get('view-timeline') - }), + }) }, { id: 'transform', label: 'Transform', @@ -229,6 +230,7 @@ TODO: set query state ...?

    render: function() { var tmplData = this.model.toTemplateJSON(); tmplData.views = this.pageViews; + tmplData.sidebarViews = this.sidebarViews; var template = Mustache.render(this.template, tmplData); $(this.el).html(template);

    now create and append other views

        var $dataViewContainer = this.el.find('.data-view-container');
         var $dataSidebar = this.el.find('.data-view-sidebar');

    the main views

        _.each(this.pageViews, function(view, pageName) {
    @@ -242,7 +244,7 @@ TODO: set query state ...?

    _.each(this.sidebarViews, function(view) { this['$'+view.id] = view.view.el; $dataSidebar.append(view.view.el); - }); + }, this); var pager = new recline.View.Pager({ model: this.model.queryState @@ -283,13 +285,7 @@ TODO: set query state ...?

    _onMenuClick: function(e) { e.preventDefault(); var action = $(e.target).attr('data-action'); - if (action === 'filters') { - this.$filterEditor.toggle(); - } else if (action === 'fields') { - this.$fieldsView.toggle(); - } else if (action === 'transform') { - this.transformView.el.toggle(); - } + this['$'+action].toggle(); }, _onSwitchView: function(e) { @@ -310,6 +306,7 @@ TODO: set query state ...?

    'view-graph': graphState, backend: this.model.backend.__type__, url: this.model.get('url'), + dataset: this.model.toJSON(), currentView: null, readOnly: false }, @@ -339,7 +336,7 @@ TODO: set query state ...?

    var self = this; _.each(this.pageViews, function(pageView) { pageView.view.bind('recline:flash', function(flash) { - self.notify(flash); + self.notify(flash); }); }); },

    notify

    @@ -360,14 +357,15 @@ flash object. Flash attributes (all are optional):

    }, flash ); + var _template; if (tmplData.loader) { - var _template = ' \ + _template = ' \ <div class="alert alert-info alert-loader"> \ {{message}} \ <span class="notification-loader">&nbsp;</span> \ </div>'; } else { - var _template = ' \ + _template = ' \ <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \ {{message}} \ </div>'; @@ -391,14 +389,28 @@ flash object. Flash attributes (all are optional):

    } });

    MultiView.restore

    -

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

    my.MultiView.restore = function(state) {
    -  var dataset = recline.Model.Dataset.restore(state);
    +

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

    + +

    This inverts the state serialization process in Multiview

    my.MultiView.restore = function(state) {

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

      if (state.backend === 'memory') {
    +    var datasetInfo = {
    +      backend: 'memory',
    +      records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
    +    };
    +  } else {
    +    var datasetInfo = _.extend({
    +        url: state.url,
    +        backend: state.backend
    +      },
    +      state.dataset
    +    );
    +  }
    +  var dataset = new recline.Model.Dataset(datasetInfo);
       var explorer = new my.MultiView({
         model: dataset,
         state: state
       });
       return explorer;
    -}

    Miscellaneous Utilities

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

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

    my.parseHashUrl = function(hashUrl) {
    +}

    Miscellaneous Utilities

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

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

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

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

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

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

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

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

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

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

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

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

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

    Parse the query string out of the URL hash

    my.parseHashQueryString = function() {
    +};

    Parse the query string out of the URL hash

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

    Compse a Query String

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

    Compse a Query String

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

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

    slice(1) to remove # at start

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

    slice(1) to remove # at start

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

    view.slickgrid.js

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

    view.slickgrid.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -100,15 +100,17 @@ column picker

    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

    Column sorting

        var sortInfo = this.model.queryState.get('sort');
         if (sortInfo){
    -      var column = _.keys(sortInfo[0])[0];
    -      var sortAsc = !(sortInfo[0][column].order == 'desc');
    +      var column = sortInfo[0].field;
    +      var sortAsc = !(sortInfo[0].order == 'desc');
           this.grid.setSortColumn(column, sortAsc);
         }
     
         this.grid.onSort.subscribe(function(e, args){
           var order = (args.sortAsc) ? 'asc':'desc';
    -      var sort = [{}];
    -      sort[0][args.sortCol.field] = {order: order};
    +      var sort = [{
    +        field: args.sortCol.field,
    +        order: order
    +      }];
           self.model.query({sort: sort});
         });
     
    diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html
    index 295900ce..04956d6d 100644
    --- a/docs/src/view.timeline.html
    +++ b/docs/src/view.timeline.html
    @@ -1,4 +1,4 @@
    -      view.timeline.js           

    view.timeline.js

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

    view.timeline.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/view.transform.html b/docs/src/view.transform.html
    index fdca17c9..e311eced 100644
    --- a/docs/src/view.transform.html
    +++ b/docs/src/view.transform.html
    @@ -1,4 +1,4 @@
    -      view.transform.js           

    view.transform.js

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

    view.transform.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};

    Views module following classic module pattern

    (function($, my) {

    ColumnTransform

    diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html index 62af60b6..e3144459 100644 --- a/docs/src/widget.facetviewer.html +++ b/docs/src/widget.facetviewer.html @@ -1,4 +1,4 @@ - widget.facetviewer.js

    widget.facetviewer.js

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

    widget.facetviewer.js

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

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    + widget.fields.js

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    For each field

    diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html index 30085297..2b02a8c7 100644 --- a/docs/src/widget.filtereditor.html +++ b/docs/src/widget.filtereditor.html @@ -1,4 +1,4 @@ - widget.filtereditor.js

    widget.filtereditor.js

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

    widget.filtereditor.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
    index 9ecfadc9..101da7a1 100644
    --- a/docs/src/widget.pager.html
    +++ b/docs/src/widget.pager.html
    @@ -1,4 +1,4 @@
    -      widget.pager.js           

    widget.pager.js

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

    widget.pager.js