From 8762b9a862aa0a3be450c20a0cbb344e079dca9f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 15 Mar 2012 00:43:06 +0000 Subject: [PATCH] [build,doc][s]: build all the docs and tweak main doc file. --- docs/backend/base.html | 2 +- docs/backend/dataproxy.html | 39 ++-- docs/backend/elasticsearch.html | 2 +- docs/backend/gdocs.html | 36 +++- docs/backend/memory.html | 2 +- docs/backend/webstore.html | 61 ------ docs/model.html | 2 +- docs/view-flot-graph.html | 129 ++++++++++--- docs/view-grid.html | 48 +++-- docs/view.html | 105 ++++++++--- index.html | 59 +++--- make | 3 + recline.js | 323 +++++++++++++++++++------------- 13 files changed, 486 insertions(+), 325 deletions(-) delete mode 100644 docs/backend/webstore.html diff --git a/docs/backend/base.html b/docs/backend/base.html index 92c0ca53..b4da1482 100644 --- a/docs/backend/base.html +++ b/docs/backend/base.html @@ -1,4 +1,4 @@ - base.js

base.js

Recline Backends

+ base.js

base.js

Recline Backends

Backends are connectors to backend data sources and stores

diff --git a/docs/backend/dataproxy.html b/docs/backend/dataproxy.html index 83405602..d232ef91 100644 --- a/docs/backend/dataproxy.html +++ b/docs/backend/dataproxy.html @@ -1,4 +1,4 @@ - dataproxy.js

dataproxy.js

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

dataproxy.js

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

DataProxy Backend

@@ -25,28 +25,9 @@ sync: function(method, model, options) { var self = this; if (method === "read") { - if (model.__type__ == 'Dataset') { - var base = self.get('dataproxy_url');

TODO: should we cache for extra efficiency

          var data = {
-            url: model.get('url')
-            , 'max-results':  1
-            , type: model.get('format') || 'csv'
-          };
-          var jqxhr = $.ajax({
-            url: base
-            , data: data
-            , dataType: 'jsonp'
-          });
-          var dfd = $.Deferred();
-          my.wrapInTimeout(jqxhr).done(function(results) {
-            model.fields.reset(_.map(results.fields, function(fieldId) {
-              return {id: fieldId};
-              })
-            );
-            dfd.resolve(model, jqxhr);
-          })
-          .fail(function(arguments) {
-            dfd.reject(arguments);
-          });
+        if (model.__type__ == 'Dataset') {

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

          var dfd = $.Deferred();
+          dfd.resolve(model);
           return dfd.promise();
         }
       } else {
@@ -66,7 +47,14 @@
         , dataType: 'jsonp'
       });
       var dfd = $.Deferred();
-      jqxhr.done(function(results) {
+      my.wrapInTimeout(jqxhr).done(function(results) {
+        if (results.error) {
+          dfd.reject(results.error);
+        }
+        dataset.fields.reset(_.map(results.fields, function(fieldId) {
+          return {id: fieldId};
+          })
+        );
         var _out = _.map(results.data, function(doc) {
           var tmp = {};
           _.each(results.fields, function(key, idx) {
@@ -75,6 +63,9 @@
           return tmp;
         });
         dfd.resolve(_out);
+      })
+      .fail(function(arguments) {
+        dfd.reject(arguments);
       });
       return dfd.promise();
     }
diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html
index d576b1cf..0b91fa81 100644
--- a/docs/backend/elasticsearch.html
+++ b/docs/backend/elasticsearch.html
@@ -1,4 +1,4 @@
-      elasticsearch.js           

elasticsearch.js

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

elasticsearch.js

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

ElasticSearch Backend

diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html index d503f679..c058c6f5 100644 --- a/docs/backend/gdocs.html +++ b/docs/backend/gdocs.html @@ -1,4 +1,4 @@ - gdocs.js

gdocs.js

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

gdocs.js

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

Google spreadsheet backend

@@ -15,18 +15,36 @@ var dataset = new recline.Model.Dataset({ 'gdocs' );
  my.GDoc = Backbone.Model.extend({
+    getUrl: function(dataset) {
+      var url = dataset.get('url');
+      if (url.indexOf('feeds/list') != -1) {
+        return url;
+      } else {

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

        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/
+        var matches = url.match(regex);
+        if (matches) {
+          var key = matches[1];
+          var worksheet = 1;
+          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
+          return out;
+        } else {
+          alert('Failed to extract gdocs key from ' + url);
+        }
+      }
+    },
     sync: function(method, model, options) {
       var self = this;
       if (method === "read") { 
         var dfd = $.Deferred(); 
         var dataset = model;
 
-        $.getJSON(model.get('url'), function(d) {
+        var url = this.getUrl(model);
+
+        $.getJSON(url, function(d) {
           result = self.gdocsToJavascript(d);
           model.fields.reset(_.map(result.field, function(fieldId) {
               return {id: fieldId};
             })
-          );

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

          model._dataCache = result.data;
+          );

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

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

zip the fields with the data rows to produce js objs + var fields = _.pluck(dataset.fields.toJSON(), 'id');

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

      var objs = _.map(dataset._dataCache, function (d) { 
         var obj = {};
         _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
@@ -59,11 +77,11 @@ TODO: factor this out as a common method with other backends

var results = { 'field': [], 'data': [] - };

default is no special info on type of columns

      var colTypes = {};
+      };

default is no special info on type of columns

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

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

      if (options.columnsToUse) {

columns set to subset supplied

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

set columns to use to be all available

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

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

      if (options.columnsToUse) {

columns set to subset supplied

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

set columns to use to be all available

        if (gdocsSpreadsheet.feed.entry.length > 0) {
           for (var k in gdocsSpreadsheet.feed.entry[0]) {
             if (k.substr(0, 3) == 'gsx') {
               var col = k.substr(4)
@@ -71,13 +89,13 @@ TODO: factor this out as a common method with other backends

} } } - }

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

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

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

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

if labelled as % and value contains %, convert

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

if labelled as % and value contains %, convert

          if (colTypes[col] == 'percent') {
             if (rep.test(value)) {
               var value2 = rep.exec(value);
               var value3 = parseFloat(value2);
diff --git a/docs/backend/memory.html b/docs/backend/memory.html
index 982be977..8b9fe834 100644
--- a/docs/backend/memory.html
+++ b/docs/backend/memory.html
@@ -1,4 +1,4 @@
-      memory.js           

memory.js

this.recline = this.recline || {};
+      memory.js           
onEditorSubmit:function(e){varselect=this.el.find('.editor-group select'); - this._getEditorData();},setupRouting:function(){ - varself=this;updateNav:function(pageName,queryString){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+']'); - $el.parent().addClass('active');className:'recline-query-editor',template:' \ <form action="" method="GET" class="form-inline"> \ - <input type="text" name="q" value="{{q}}" class="text-query" /> \ + <div class="input-prepend text-query"> \ + <span class="add-on"><i class="icon-search"></i></span> \ + <input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \ + <div class="btn-group menu"> \ + <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \ + <ul class="dropdown-menu"> \ + <li><a data-action="size" href="">Number of items to show ({{size}})</a></li> \ + <li><a data-action="from" href="">Show from ({{from}})</a></li> \ + </ul> \ + </div> \ + </div> \ <div class="pagination"> \ <ul> \ - <li class="prev action-pagination-update"><a>&laquo; back</a></li> \ - <li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \ - <li class="next action-pagination-update"><a>next &raquo;</a></li> \ + <li class="prev action-pagination-update"><a href="">&laquo;</a></li> \ + <li class="active"><a>{{from}} &ndash; {{to}}</a></li> \ + <li class="next action-pagination-update"><a href="">&raquo;</a></li> \ </ul> \ </div> \ - <button type="submit" class="btn" style="">Update &raquo;</button> \ </form> \ ',events:{ - 'submit form':'onFormSubmit', - 'click .action-pagination-update':'onPaginationUpdate' + 'submit form':'onFormSubmit' + ,'click .action-pagination-update':'onPaginationUpdate' + ,'click .menu li a':'onMenuItemClick'},initialize:function(){ @@ -196,10 +226,8 @@ note this.model and dataset returned are the same

},onFormSubmit:function(e){e.preventDefault(); - varnewFrom=parseInt(this.el.find('input[name="from"]').val()); - varnewSize=parseInt(this.el.find('input[name="to"]').val())-newFrom; - varquery=this.el.find('.text-query').val(); - this.model.set({size:newSize,from:newFrom,q:query}); + varquery=this.el.find('.text-query input').val(); + this.model.set({q:query});},onPaginationUpdate:function(e){e.preventDefault(); @@ -211,6 +239,20 @@ note this.model and dataset returned are the same

}this.model.set({from:newFrom});}, + onMenuItemClick:function(e){ + e.preventDefault(); + varattrName=$(e.target).attr('data-action'); + varmsg=_.template('New value (<%= value %>)', + {value:this.model.get(attrName)} + ); + varnewValue=prompt(msg); + if(newValue){ + newValue=parseInt(newValue); + varupdate={}; + update[attrName]=newValue; + this.model.set(update); + } + },render:function(){vartmplData=this.model.toJSON();tmplData.to=this.model.get('from')+this.model.get('size'); @@ -220,7 +262,7 @@ note this.model and dataset returned are the same

}); -/* ========================================================== */query:parsed[2]||''}} -}if(q&&q.length&&q[0]==='?'){q=q.slice(1);} - while(e=r.exec(q)){my.setHashQueryString=function(queryParams){window.location.hash=window.location.hash.split('?')[0]+my.composeQueryString(queryParams); -} <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \ {{msg}} \ {{#loader}} \ - <img src="images/small-spinner.gif" class="notification-loader"> \ + <span class="notification-loader">&nbsp;</span> \ {{/loader}} \ </div>';var_templated=$.mustache(_template,tmplData); @@ -289,7 +334,7 @@ note this.model and dataset returned are the same

});},1000);} -}

memory.js

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

Memory Backend - uses in-memory data

diff --git a/docs/backend/webstore.html b/docs/backend/webstore.html deleted file mode 100644 index aae1bbc5..00000000 --- a/docs/backend/webstore.html +++ /dev/null @@ -1,61 +0,0 @@ - webstore.js

webstore.js

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

Webstore Backend

- -

Connecting to Webstores

- -

To use this backend ensure your Dataset has a webstore_url in its attributes.

  my.Webstore = Backbone.Model.extend({
-    sync: function(method, model, options) {
-      if (method === "read") {
-        if (model.__type__ == 'Dataset') {
-          var base = model.get('webstore_url');
-          var schemaUrl = base + '/schema.json';
-          var jqxhr = $.ajax({
-            url: schemaUrl,
-              dataType: 'jsonp',
-              jsonp: '_callback'
-          });
-          var dfd = $.Deferred();
-          my.wrapInTimeout(jqxhr).done(function(schema) {
-            var fieldData = _.map(schema.data, function(item) {
-              item.id = item.name;
-              delete item.name;
-              return item;
-            });
-            model.fields.reset(fieldData);
-            model.docCount = schema.count;
-            dfd.resolve(model, jqxhr);
-          })
-          .fail(function(arguments) {
-            dfd.reject(arguments);
-          });
-          return dfd.promise();
-        }
-      }
-    },
-    query: function(model, queryObj) {
-      var base = model.get('webstore_url');
-      var data = {
-        _limit:  queryObj.size
-        , _offset: queryObj.from
-      };
-      var jqxhr = $.ajax({
-        url: base + '.json',
-        data: data,
-        dataType: 'jsonp',
-        jsonp: '_callback',
-        cache: true
-      });
-      var dfd = $.Deferred();
-      jqxhr.done(function(results) {
-        dfd.resolve(results.data);
-      });
-      return dfd.promise();
-    }
-  });
-  recline.Model.backends['webstore'] = new my.Webstore();
-
-}(jQuery, this.recline.Backend));
-
-
\ No newline at end of file diff --git a/docs/model.html b/docs/model.html index 053d7577..ae7bcb5c 100644 --- a/docs/model.html +++ b/docs/model.html @@ -36,7 +36,7 @@ updated by queryObj (if provided).

also returned.

  query: function(queryObj) {
     this.trigger('query:start');
     var self = this;
-    this.queryState.set(queryObj, {silent: true});
+    this.queryState.set(queryObj);
     var dfd = $.Deferred();
     this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
       var docs = _.map(rows, function(row) {
diff --git a/docs/view-flot-graph.html b/docs/view-flot-graph.html
index dac85e8a..cd3d6080 100644
--- a/docs/view-flot-graph.html
+++ b/docs/view-flot-graph.html
@@ -36,7 +36,10 @@ generate the element itself (you can then append view.el to the DOM.

<label>Graph Type</label> \ <div class="input editor-type"> \ <select> \ - <option value="line">Line</option> \ + <option value="lines-and-points">Lines and Points</option> \ + <option value="lines">Lines</option> \ + <option value="points">Points</option> \ + <option value="bars">Bars</option> \ </select> \ </div> \ <label>Group Column (x-axis)</label> \ @@ -95,7 +98,7 @@ generate the element itself (you can then append view.el to the DOM.

this.chartConfig = _.extend({ group: null, series: [], - graphType: 'line' + graphType: 'lines-and-points' }, configFromHash, config @@ -113,9 +116,14 @@ could be simpler just to have a common template!

update navigation -TODO: make this less invasive (e.g. preserve other keys in query string)

    var qs = my.parseHashQueryString();
-    qs['graph'] = this.chartConfig;
+    $editor = this;
+    var series = this.$series.map(function () {
+      return $(this).val();
+    });
+    this.chartConfig.series = $.makeArray(series)
+    this.chartConfig.group = this.el.find('.editor-group select').val();
+    this.chartConfig.graphType = this.el.find('.editor-type select').val();

update navigation

    var qs = my.parseHashQueryString();
+    qs['graph'] = JSON.stringify(this.chartConfig);
     my.setHashQueryString(qs);
     this.redraw();
   },
@@ -128,25 +136,98 @@ TODO: make this less invasive (e.g. preserve other keys in query string)

    var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
     if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
       return
-    }

create this.plot and cache it

    if (!this.plot) {

only lines for the present

      options = {
-        id: 'line',
-        name: 'Line Chart'
-      };
-      this.plot = $.plot(this.$graph, this.createSeries(), options);
-    } 
-    this.plot.setData(this.createSeries());
-    this.plot.resize();
-    this.plot.setupGrid();
-    this.plot.draw();
+    }
+    var series = this.createSeries();
+    var options = this.graphOptions[this.chartConfig.graphType];
+    this.plot = $.plot(this.$graph, series, options);
+    if (this.chartConfig.graphType in { 'points': '', 'lines-and-points': '' }) {
+      this.setupTooltips();
+    }

create this.plot and cache it + if (!this.plot) { + this.plot = $.plot(this.$graph, series, options); + } else { + this.plot.parseOptions(options); + this.plot.setData(this.createSeries()); + this.plot.resize(); + this.plot.setupGrid(); + this.plot.draw(); + }

  },
+
+  graphOptions: { 
+    lines: {
+       series: { 
+         lines: { show: true }
+       }
+    }
+    , points: {
+      series: {
+        points: { show: true }
+      },
+      grid: { hoverable: true, clickable: true }
+    }
+    , 'lines-and-points': {
+      series: {
+        points: { show: true },
+        lines: { show: true }
+      },
+      grid: { hoverable: true, clickable: true }
+    }
+    , bars: {
+      series: {
+        lines: {show: false},
+        bars: {
+          show: true,
+          barWidth: 1,
+          align: "left",
+          fill: true
+        }
+      },
+      xaxis: {
+        tickSize: 1,
+        tickLength: 1,
+      }
+    }
   },
 
-  _getEditorData: function() {
-    $editor = this
-    var series = this.$series.map(function () {
-      return $(this).val();
+  setupTooltips: function() {
+    var self = this;
+    function showTooltip(x, y, contents) {
+      $('<div id="flot-tooltip">' + contents + '</div>').css( {
+        position: 'absolute',
+        display: 'none',
+        top: y + 5,
+        left: x + 5,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'background-color': '#fee',
+        opacity: 0.80
+      }).appendTo("body").fadeIn(200);
+    }
+
+    var previousPoint = null;
+    this.$graph.bind("plothover", function (event, pos, item) {
+      if (item) {
+        if (previousPoint != item.dataIndex) {
+          previousPoint = item.dataIndex;
+          
+          $("#flot-tooltip").remove();
+          var x = item.datapoint[0].toFixed(2),
+              y = item.datapoint[1].toFixed(2);
+          
+          var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
+            group: self.chartConfig.group,
+            x: x,
+            series: item.series.label,
+            y: y
+          });
+          showTooltip(item.pageX, item.pageY, content);
+        }
+      }
+      else {
+        $("#flot-tooltip").remove();
+        previousPoint = null;            
+      }
     });
-    this.chartConfig.series = $.makeArray(series)
-    this.chartConfig.group = this.el.find('.editor-group select').val();
   },
 
   createSeries: function () {
@@ -167,7 +248,7 @@ TODO: make this less invasive (e.g. preserve other keys in query string)

}); } return series; - },

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

+ },

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

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

@@ -183,7 +264,7 @@ to be removed.

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

Public: Removes a series list item from the editor.

+ },

Public: Removes a series list item from the editor.

Also updates the labels of the remaining series elements.

  removeSeries: function (e) {
     e.preventDefault();
@@ -201,7 +282,7 @@ to be removed.

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

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

+ },

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

Returns itself.

  _updateSeries: function () {
     this.$series  = this.el.find('.editor-series select');
diff --git a/docs/view-grid.html b/docs/view-grid.html
index c4c935d5..f5472749 100644
--- a/docs/view-grid.html
+++ b/docs/view-grid.html
@@ -44,19 +44,19 @@ showDialog: function(template, data) {
 },

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

  onColumnHeaderClick: function(e) {
     this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
-    util.position('data-table-menu', e);
-    util.render('columnActions', 'data-table-menu');
   },
 
   onRowHeaderClick: function(e) {
     this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
-    util.position('data-table-menu', e);
-    util.render('rowActions', 'data-table-menu');
   },
   
   onRootHeaderClick: function(e) {
-    util.position('data-table-menu', e);
-    util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields});
+    var tmpl = ' \
+        {{#columns}} \
+        <li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
+        {{/columns}}';
+    var tmp = $.mustache(tmpl, {'columns': this.hiddenFields});
+    this.el.find('.root-header-menu .dropdown-menu').html(tmp);
   },
 
   onMenuClick: function(e) {
@@ -90,7 +90,6 @@ from DOM) while id may be int

}) } } - util.hide('data-table-menu'); actions[$(e.target).attr('data-action')](); }, @@ -141,26 +140,32 @@ from DOM) while id may be int

},

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

Templating

  template: ' \
-    <div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
-    <ul class="data-table-menu"></ul> \
-    <table class="data-table" cellspacing="0"> \
+    <table class="data-table table-striped table-condensed" cellspacing="0"> \
       <thead> \
         <tr> \
           {{#notEmpty}} \
             <th class="column-header"> \
-              <div class="column-header-title"> \
-                <a class="root-header-menu"></a> \
-                <span class="column-header-name"></span> \
+              <div class="btn-group root-header-menu"> \
+                <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
+                <ul class="dropdown-menu data-table-menu"> \
+                </ul> \
               </div> \
+              <span class="column-header-name"></span> \
             </th> \
           {{/notEmpty}} \
           {{#fields}} \
             <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \
-              <div class="column-header-title"> \
-                <a class="column-header-menu"></a> \
-                <span class="column-header-name">{{label}}</span> \
-              </div> \
+              <div class="btn-group column-header-menu"> \
+                <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
+                <ul class="dropdown-menu data-table-menu"> \
+                  <li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
+                  <li class="write-op"><a data-action="deleteColumn" href="JavaScript:void(0);">Delete this column</a></li> \
+                  <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
+                  <li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
+                  <li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
+                </ul> \
               </div> \
+              <span class="column-header-name">{{label}}</span> \
             </th> \
           {{/fields}} \
         </tr> \
@@ -239,7 +244,14 @@ var row = new DataGridRow({
   },
 
   template: ' \
-      <td><a class="row-header-menu"></a></td> \
+      <td> \
+        <div class="btn-group row-header-menu"> \
+          <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
+          <ul class="dropdown-menu data-table-menu"> \
+            <li class="write-op"><a data-action="deleteRow" href="JavaScript:void(0);">Delete this row</a></li> \
+          </ul> \
+        </div> \
+      </td> \
       {{#cells}} \
       <td data-field="{{field}}"> \
         <div class="data-table-cell-content"> \
diff --git a/docs/view.html b/docs/view.html
index 28474dd2..8e0c77f3 100644
--- a/docs/view.html
+++ b/docs/view.html
@@ -100,22 +100,40 @@ FlotGraph subview.

this.router = new Backbone.Router(); this.setupRouting(); - this.model.bind('query:start', function(eventName) { + this.model.bind('query:start', function() { my.notify('Loading data', {loader: true}); }); - this.model.bind('query:done', function(eventName) { + this.model.bind('query:done', function() { my.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'}); + my.notify('Data loaded', {category: 'success'});

update navigation

        var qs = my.parseHashQueryString();
+        qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON());
+        my.setHashQueryString(qs);
       });
-    this.model.bind('query:fail', function(eventName, error) {
+    this.model.bind('query:fail', function(error) {
         my.clearNotifications();
-        my.notify(error.message, {category: 'error', persist: true});
-      });

retrieve basic data like fields etc + var msg = ''; + if (typeof(error) == 'string') { + msg = error; + } else if (typeof(error) == 'object') { + if (error.title) { + msg = error.title + ': '; + } + if (error.message) { + msg += error.message; + } + } else { + msg = 'There was an error querying the backend'; + } + my.notify(msg, {category: 'error', persist: true}); + });

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

    this.model.fetch()
       .done(function(dataset) {
-        self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
-        self.model.query();
+        var queryState = my.parseHashQueryString().reclineQuery;
+        if (queryState) {
+          queryState = JSON.parse(queryState);
+        }
+        self.model.query(queryState);
       })
       .fail(function(error) {
         my.notify(error.message, {category: 'error', persist: true});
@@ -143,7 +161,7 @@ note this.model and dataset returned are the same

Default route

    this.router.route('', this.pageViews[0].id, function() {
+    var self = this;

Default route

    this.router.route('', this.pageViews[0].id, function() {
       self.updateNav(self.pageViews[0].id);
     });
     $.each(this.pageViews, function(idx, view) {
@@ -155,8 +173,10 @@ note this.model and dataset returned are the same

show the specific page

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

show the specific page

    _.each(this.pageViews, function(view, idx) {
       if (view.id === pageName) {
         view.view.el.show();
       } else {
@@ -171,21 +191,31 @@ note this.model and dataset returned are the same

Miscellaneous Utilities

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

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

my.parseHashUrl = function(hashUrl) {
+/* ========================================================== */

Miscellaneous Utilities

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

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

my.parseHashUrl = function(hashUrl) {
   var parsed = urlPathRegex.exec(hashUrl);
   if (parsed == null) {
     return {};
@@ -230,7 +272,10 @@ note this.model and dataset returned are the same

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 {};
+  }
   var urlParams = {},
     e, d = function (s) {
       return unescape(s.replace(/\+/g, " "));
@@ -240,17 +285,17 @@ note this.model and dataset returned are the same

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

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

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

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

Parse the query string out of the URL hash

my.parseHashQueryString = function() {
+}

Parse the query string out of the URL hash

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

Compse a Query String

my.composeQueryString = function(queryParams) {
+}

Compse a Query String

my.composeQueryString = function(queryParams) {
   var queryString = '?';
   var items = [];
   $.each(queryParams, function(key, value) {
-    items.push(key + '=' + JSON.stringify(value));
+    items.push(key + '=' + value);
   });
   queryString += items.join('&');
   return queryString;
@@ -258,7 +303,7 @@ note this.model and dataset returned are the same

notify

+}

notify

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

@@ -277,7 +322,7 @@ note this.model and dataset returned are the same

clearNotifications

+}

clearNotifications

Clear all existing notifications

my.clearNotifications = function() {
   var $notifications = $('.data-explorer .alert-messages .alert');
diff --git a/index.html b/index.html
index 17ed7698..699c9540 100644
--- a/index.html
+++ b/index.html
@@ -69,14 +69,14 @@
   

Recline combines a data grid, Google Refine-style data transforms and visualizations all in lightweight javascript and html.

Designed for standalone use or as a library to integrate into your own - app. Recline utilizes the lightweight but powerful Backbone framework, and - so is a cinch to extend or adapt.

+ app. Recline builds on the powerful but lightweight Backbone framework + making it extremely easy to extend and adapt.

Main Features

    @@ -113,27 +113,7 @@

    CSS: the demo utilizes bootstrap but you can integrate with your own HTML and CSS. Data Explorer specific CSS can be found here in the repo: https://github.com/okfn/recline/tree/master/css.

    Documentation

    -

    Recline has a simple structure layered on top of the basic Model/View - distinction inherent in Backbone. There are the following two main domain objects (all Backbone Models):

    -
      -
    • Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.
    • -
    • Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).
    • -
    -

    More on the models in the Model source docs.

    - -

    Backends (more info below) connect Dataset and Documents to data - from a specific 'Backend' data source. They provide methods for loading and - saving Datasets and individuals Documents as well as for bulk loading via a - query API and doing bulk transforms on the backend.

    - -

    Complementing the model are various Views (you can easily write your own). Each view holds a pointer to a Dataset:

    -
      -
    • DataExplorer: the parent view which manages the overall app and sets up sub views.
    • -
    • DataGrid: the data grid view.
    • -
    • FlotGraph: a simple graphing view using Flot.
    • -
    - -

    Using It

    +

    Quickstart

     // Note: you should have included the relevant JS libraries (and CSS)
     // See above for dependencies
    @@ -158,6 +138,29 @@ Backbone.history.start();
           href="demo/">Demo -- just hit view source (NB: the javascript for the
         demo is in: app.js).

    +

    Architecture and Model

    +

    Recline has a simple structure layered on top of the basic Model/View + distinction inherent in Backbone. There are the following two main domain objects (both Backbone Models):

    +
      +
    • Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.
    • +
    • Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).
    • +
    +

    More detail of how these work can be found in the Model source docs.

    + +

    Backends connect Dataset and Documents to data + from a specific 'Backend' data source. They provide methods for loading and + saving Datasets and individuals Documents as well as for bulk loading via a + query API and doing bulk transforms on the backend. More info on backends can be found below.

    + +

    Complementing the model are various Views (you can easily write your own). Each view holds a pointer to a Dataset:

    +
      +
    • DataExplorer: the parent view which manages the overall app and sets up sub views.
    • +
    • DataGrid: the data grid view.
    • +
    • FlotGraph: a simple graphing view using Flot.
    • +
    + +

    Backends

    Backends are connectors to backend data sources from which data can be retrieved.

    @@ -197,10 +200,11 @@ used by the Dataset.query method to search the backend for documents, retrieving the results in bulk. This method should also set the docCount attribute on the dataset.

    -

    queryObj should be either a recline.Model.Query object or a +

    queryObj should be either a recline.Model.Query object or a Hash. The structure of data in the Query object or Hash should follow that -defined in issue 34. (That said, if you are writing your own backend and have -control over the query object you can obviously use whatever structure you +defined in issue 34. (Of +course, if you are writing your own backend, and hence have control over the +interpretation of the query object, you can use whatever structure you like).

    Source Docs (via Docco)

    @@ -211,6 +215,7 @@ like).

  • Graph View (based on Flot)
  • Backend: Memory (local data)
  • Backend: ElasticSearch
  • +
  • Backend: DataProxy
  • Backend: Google Docs (Spreadsheet)
diff --git a/make b/make index 02a3d5b9..4d60070a 100755 --- a/make +++ b/make @@ -1,5 +1,6 @@ #!/usr/bin/env python import sys +import shutil import os def cat(): @@ -12,6 +13,8 @@ def docs(): print("** Building docs") cmd = 'docco src/model.js src/view.js src/view-grid.js src/view-flot-graph.js' os.system(cmd) + if os.path.exists('/tmp/recline-docs'): + shutil.rmtree('/tmp/recline-docs') os.makedirs('/tmp/recline-docs') os.system('mkdir -p docs/backend') files = '%s/src/backend/*.js' % os.getcwd() diff --git a/recline.js b/recline.js index 0add56e4..b52cb93e 100644 --- a/recline.js +++ b/recline.js @@ -166,7 +166,7 @@ my.Dataset = Backbone.Model.extend({ query: function(queryObj) { this.trigger('query:start'); var self = this; - this.queryState.set(queryObj, {silent: true}); + this.queryState.set(queryObj); var dfd = $.Deferred(); this.backend.query(this, this.queryState.toJSON()).done(function(rows) { var docs = _.map(rows, function(row) { @@ -255,18 +255,6 @@ my.backends = {}; var util = function() { var templates = { transformActions: '
  • Global transform...
  • ' - , columnActions: ' \ -
  • Transform...
  • \ -
  • Delete this column
  • \ -
  • Sort ascending
  • \ -
  • Sort descending
  • \ -
  • Hide this column
  • \ - ' - , rowActions: '
  • Delete this row
  • ' - , rootActions: ' \ - {{#columns}} \ -
  • Show column: {{.}}
  • \ - {{/columns}}' , cellEditor: ' \