diff --git a/README.md b/README.md
index 6db0a2e9..ed2bc918 100755
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ toolkit, all in pure javascript and html.
Designed for standalone use or as a library to integrate into your own app.
-Live demo: http://okfnlabs.org/recline/demo/
+
zip the fields with the data rows to produce js objs
TODO: factor this out as a common method with other backends
varobjs=_.map(dataset._dataCache,function(d){varobj={};_.each(_.zip(fields,d),function(x){obj[x[0]]=x[1];})
@@ -296,9 +301,9 @@ TODO: factor this out as a common method with other backends gdocsToJavascript:function(gdocsSpreadsheet){/* :options: (optional) optional argument dictionary:
- columnsToUse: list of columns to use (specified by header names)
+ columnsToUse: list of columns to use (specified by field names) colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
- :return: tabular data object (hash with keys: header and data).
+ :return: tabular data object (hash with keys: field and data). Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. */
@@ -307,25 +312,25 @@ TODO: factor this out as a common method with other backends options =arguments[1];}varresults={
- 'header':[],
+ 'field':[],'data':[]};
Other than standard list of Backbone methods it has two important attributes:
+
A model must have the following (Backbone) attributes:
+
fields: (aka columns) is a FieldList listing all the fields on this
+Dataset (this can be set explicitly, or, on fetch() of Dataset
+information from the backend, or as is perhaps most common on the first
+query)
currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here.
-This also illustrates the limitations of separating the Dataset and the Backend
\ No newline at end of file
diff --git a/docs/view.html b/docs/view.html
index 11cf161d..18edf6c3 100644
--- a/docs/view.html
+++ b/docs/view.html
@@ -1,33 +1,49 @@
- view.js
The primary view for the entire application. Usage:
-
It should be initialized with a recline.Model.Dataset object and an existing
-dom element to attach to (the existing DOM element is important for
-rendering of FlotGraph subview).
+
+var myExplorer = new model.recline.DataExplorer({
+ model: {{recline.Model.Dataset instance}}
+ el: {{an existing dom element}}
+ views: {{page views}}
+ config: {{config options -- see below}}
+});
+
-
To pass in configuration options use the config key in initialization hash
-e.g.
+
Parameters
-
var explorer = new DataExplorer({
- config: {...}
- })
-
+
model: (required) Dataset instance.
-
Config options:
+
el: (required) DOM element.
+
+
views: (optional) the views (Grid, Graph etc) for DataExplorer to
+show. This is an array of view hashes. If not provided
+just initialize a DataTable with id 'grid'. Example:
+
+
+var views = [
+ {
+ id: 'grid', // used for routing
+ label: 'Grid', // used for view switcher
+ view: new recline.View.DataTable({
+ model: dataset
+ })
+ },
+ {
+ id: 'graph',
+ label: 'Graph',
+ view: new recline.View.FlotGraph({
+ model: dataset
+ })
+ }
+];
+
+
+
config: Config options like:
displayCount: how many documents to display initially (default: 10)
@@ -35,19 +51,21 @@ e.g.
operate in read-only mode (hiding all editing options).
-
All other views as contained in this one.
my.DataExplorer=Backbone.View.extend({
+
NB: the element already being in the DOM is important for rendering of
+FlotGraph subview.
my.DataExplorer=Backbone.View.extend({template:' \ <div class="data-explorer"> \ <div class="alert-messages"></div> \ \ <div class="header"> \ <ul class="navigation"> \
- <li class="active"><a href="#grid" class="btn">Grid</a> \
- <li><a href="#graph" class="btn">Graph</a></li> \
+ {{#views}} \
+ <li><a href="#{{id}}" class="btn">{{label}}</a> \
+ {{/views}} \ </ul> \ <div class="pagination"> \ <form class="display-count"> \
- Showing 0 to <input name="displayCount" type="text" value="{{displayCount}}" /> of <span class="doc-count">{{docCount}}</span> \
+ Showing 0 to <input name="displayCount" type="text" value="{{displayCount}}" title="Edit and hit enter to change the number of rows displayed" /> of <span class="doc-count">{{docCount}}</span> \ </form> \ </div> \ </div> \
@@ -68,33 +86,57 @@ operate in read-only mode (hiding all editing options).
initialize:function(options){varself=this;this.el=$(this.el);
- this.config=options.config||{};
- _.extend(this.config,{
- displayCount:10
- ,readOnly:false
- });
+ this.config=_.extend({
+ displayCount:50
+ ,readOnly:false
+ },
+ options.config);if(this.config.readOnly){this.setReadOnly();
- }
my.DataTable=Backbone.View.extend({tagName:"div",
@@ -156,13 +193,15 @@ if it was hidden until now - see comments in FlotGraph.redraw
this.model.currentDocuments.bind('reset',this.render);this.model.currentDocuments.bind('remove',this.render);this.state={};
+ this.hiddenFields=[];},events:{'click .column-header-menu':'onColumnHeaderClick','click .row-header-menu':'onRowHeaderClick'
+ ,'click .root-header-menu':'onRootHeaderClick','click .data-table-menu li a':'onMenuClick'
- },
TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)).
showDialog: function(template, data) {
if (!data) data = {};
util.show('dialog');
@@ -171,9 +210,9 @@ showDialog: function(template, data) {
util.hide('dialog');
})
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
-},
alert('This function needs to be re-implemented');return;if(confirm(msg))costco.deleteColumn(self.state.currentColumn);},deleteRow:function(){
- vardoc=_.find(self.model.currentDocuments.models,function(doc){
important this is == as the currentRow will be string (as comes
from DOM) while id may be int
returndoc.id==self.state.currentRow});doc.destroy().then(function(){self.model.currentDocuments.remove(doc);
- util.notify("Row deleted successfully");
+ my.notify("Row deleted successfully");}).fail(function(err){
- util.notify("Errorz! "+err)
+ my.notify("Errorz! "+err)})}}
@@ -234,7 +282,7 @@ from DOM) while id may be int
showTransformDialog:function(){var$el=$('.dialog-content');util.show('dialog');
- varview=newmy.DataTransform({
+ varview=newrecline.View.DataTransform({});view.render();$el.empty();
@@ -243,23 +291,49 @@ from DOM) while id may be int
DataTableRow View for rendering an individual document.
Since we want this to update in place it is up to creator to provider the element to attach to.
-In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.
my.DataTableRow=Backbone.View.extend({
+In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable.
my.DataTableRow=Backbone.View.extend({initialize:function(options){_.bindAll(this,'render');
- this._headers=options.headers;
+ this._fields=options.fields;this.el=$(this.el);this.model.bind('change',this.render);},
+
template:' \ <td><a class="row-header-menu"></a></td> \ {{#cells}} \
- <td data-header="{{header}}"> \
+ <td data-field="{{field}}"> \ <div class="data-table-cell-content"> \ <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell"> </a> \ <div class="data-table-cell-value">{{value}}</div> \
@@ -309,14 +388,14 @@ In addition you must pass in a headers in the constructor options. This should b
{{/cells}} \ ',events:{
- 'click .data-table-cell-edit':'onEditClick',
'click .data-table-cell-editor .okButton':'onEditorOK','click .data-table-cell-editor .cancelButton':'onEditorCancel'},toTemplateJSON:function(){vardoc=this.model;
- varcellData=_.map(this._headers,function(header){
- return{header:header,value:doc.get(header)}
+ varcellData=this._fields.map(function(field){
+ return{field:field.id,value:doc.get(field.id)}})return{id:this.id,cells:cellData}},
@@ -326,8 +405,7 @@ In addition you must pass in a headers in the constructor options. This should b
varhtml=$.mustache(this.template,this.toTemplateJSON());$(this.el).html(html);returnthis;
- },
var cell=$(e.target).parents('.data-table-cell-value');cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");}
-});
vartoUpdate=costco.mapDocs(docs,editFunc).edited;
- vartotalToUpdate=toUpdate.length;
- functiononCompletedUpdate(){
- totalToUpdate+=-1;
- if(totalToUpdate===0){
- util.notify(toUpdate.length+" documents updated successfully");
- alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
- self.remove();
- }
- }
diff --git a/recline.js b/recline.js
index 51a91b0e..c52f5f53 100644
--- a/recline.js
+++ b/recline.js
@@ -9,13 +9,11 @@ this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) {
- my.backends = {};
-
// ## Backbone.sync
//
// Override Backbone.sync to hand off to sync function in relevant backend
Backbone.sync = function(method, model, options) {
- return my.backends[model.backendConfig.type].sync(method, model, options);
+ return model.backend.sync(method, model, options);
}
// ## wrapInTimeout
@@ -45,83 +43,96 @@ this.recline.Model = this.recline.Model || {};
// ## BackendMemory - uses in-memory data
//
- // To use you should:
+ // This is very artificial and is really only designed for testing
+ // purposes.
+ //
+ // To use it you should provide in your constructor data:
//
- // A. provide metadata as model data to the Dataset
+ // * metadata (including fields array)
+ // * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique.
//
- // B. Set backendConfig on your dataset with attributes:
- //
- // - type: 'memory'
- // - data: hash with 2 keys:
- //
- // * headers: list of header names/labels
- // * rows: list of hashes, each hash being one row. A row *must* have an id attribute which is unique.
- //
- // Example of data:
+ // Example:
//
//
my.BackendGDoc = Backbone.Model.extend({
sync: function(method, model, options) {
+ var self = this;
if (method === "read") {
var dfd = $.Deferred();
var dataset = model;
- $.getJSON(model.backendConfig.url, function(d) {
- result = my.backends['gdocs'].gdocsToJavascript(d);
- model.set({'headers': result.header});
+ $.getJSON(model.get('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;
dfd.resolve(model);
@@ -292,9 +308,9 @@ this.recline.Model = this.recline.Model || {};
query: function(dataset, queryObj) {
var dfd = $.Deferred();
- var fields = dataset.get('headers');
+ var fields = _.pluck(dataset.fields.toJSON(), 'id');
- // zip the field headers with the data rows to produce js objs
+ // 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 = {};
@@ -307,9 +323,9 @@ this.recline.Model = this.recline.Model || {};
gdocsToJavascript: function(gdocsSpreadsheet) {
/*
:options: (optional) optional argument dictionary:
- columnsToUse: list of columns to use (specified by header names)
+ columnsToUse: list of columns to use (specified by field names)
colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
- :return: tabular data object (hash with keys: header and data).
+ :return: tabular data object (hash with keys: field and data).
Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
*/
@@ -318,7 +334,7 @@ this.recline.Model = this.recline.Model || {};
options = arguments[1];
}
var results = {
- 'header': [],
+ 'field': [],
'data': []
};
// default is no special info on type of columns
@@ -329,14 +345,14 @@ this.recline.Model = this.recline.Model || {};
// either extract column headings from spreadsheet directly, or used supplied ones
if (options.columnsToUse) {
// columns set to subset supplied
- results.header = options.columnsToUse;
+ results.field = options.columnsToUse;
} else {
// set columns to use to be all available
if (gdocsSpreadsheet.feed.entry.length > 0) {
for (var k in gdocsSpreadsheet.feed.entry[0]) {
if (k.substr(0, 3) == 'gsx') {
var col = k.substr(4)
- results.header.push(col);
+ results.field.push(col);
}
}
}
@@ -346,8 +362,8 @@ this.recline.Model = this.recline.Model || {};
var rep = /^([\d\.\-]+)\%$/;
$.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
var row = [];
- for (var k in results.header) {
- var col = results.header[k];
+ 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
@@ -498,78 +514,125 @@ this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) {
- // ## A Dataset model
- //
- // Other than standard list of Backbone methods it has two important attributes:
- //
- // * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
- // * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
- my.Dataset = Backbone.Model.extend({
- __type__: 'Dataset',
- initialize: function(options) {
- this.currentDocuments = new my.DocumentList();
- this.docCount = null;
- this.backend = null;
- this.defaultQuery = {
- size: 100
- , offset: 0
- };
- // this.queryState = {};
- },
- // ### getDocuments
- //
- // AJAX method with promise API to get rows (documents) from the backend.
- //
- // Resulting DocumentList are used to reset this.currentDocuments and are
- // also returned.
- //
- // :param numRows: passed onto backend getDocuments.
- // :param start: passed onto backend getDocuments.
- //
- // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here.
- // This also illustrates the limitations of separating the Dataset and the Backend
- query: function(queryObj) {
- var self = this;
- var backend = my.backends[this.backendConfig.type];
- this.queryState = queryObj || this.defaultQuery;
- this.queryState = _.extend({size: 100, offset: 0}, this.queryState);
- var dfd = $.Deferred();
- backend.query(this, this.queryState).done(function(rows) {
- var docs = _.map(rows, function(row) {
- var _doc = new my.Document(row);
- _doc.backendConfig = self.backendConfig;
- _doc.backend = backend;
- return _doc;
- });
- self.currentDocuments.reset(docs);
- dfd.resolve(self.currentDocuments);
- })
- .fail(function(arguments) {
- dfd.reject(arguments);
- });
- return dfd.promise();
- },
-
- toTemplateJSON: function() {
- var data = this.toJSON();
- data.docCount = this.docCount;
- return data;
+// ## A Dataset model
+//
+// A model must have the following (Backbone) attributes:
+//
+// * fields: (aka columns) is a FieldList listing all the fields on this
+// Dataset (this can be set explicitly, or, on fetch() of Dataset
+// information from the backend, or as is perhaps most common on the first
+// query)
+// * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
+// * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
+my.Dataset = Backbone.Model.extend({
+ __type__: 'Dataset',
+ initialize: function(model, backend) {
+ _.bindAll(this, 'query');
+ this.backend = backend;
+ if (backend && backend.constructor == String) {
+ this.backend = my.backends[backend];
}
- });
+ this.fields = new my.FieldList();
+ this.currentDocuments = new my.DocumentList();
+ this.docCount = null;
+ this.queryState = new my.Query();
+ this.queryState.bind('change', this.query);
+ },
- // ## A Document (aka Row)
- //
- // A single entry or row in the dataset
- my.Document = Backbone.Model.extend({
- __type__: 'Document'
- });
+ // ### query
+ //
+ // AJAX method with promise API to get documents from the backend.
+ //
+ // It will query based on current query state (given by this.queryState)
+ // updated by queryObj (if provided).
+ //
+ // Resulting DocumentList are used to reset this.currentDocuments and are
+ // also returned.
+ query: function(queryObj) {
+ var self = this;
+ this.queryState.set(queryObj, {silent: true});
+ var dfd = $.Deferred();
+ this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
+ var docs = _.map(rows, function(row) {
+ var _doc = new my.Document(row);
+ _doc.backend = self.backend;
+ _doc.dataset = self;
+ return _doc;
+ });
+ self.currentDocuments.reset(docs);
+ dfd.resolve(self.currentDocuments);
+ })
+ .fail(function(arguments) {
+ dfd.reject(arguments);
+ });
+ return dfd.promise();
+ },
+
+ toTemplateJSON: function() {
+ var data = this.toJSON();
+ data.docCount = this.docCount;
+ data.fields = this.fields.toJSON();
+ return data;
+ }
+});
+
+// ## A Document (aka Row)
+//
+// A single entry or row in the dataset
+my.Document = Backbone.Model.extend({
+ __type__: 'Document'
+});
+
+// ## A Backbone collection of Documents
+my.DocumentList = Backbone.Collection.extend({
+ __type__: 'DocumentList',
+ model: my.Document
+});
+
+// ## A Field (aka Column) on a Dataset
+//
+// Following attributes as standard:
+//
+// * id: a unique identifer for this field- usually this should match the key in the documents hash
+// * label: the visible label used for this field
+// * type: the type of the data
+my.Field = Backbone.Model.extend({
+ defaults: {
+ id: null,
+ label: null,
+ type: 'String'
+ },
+ // In addition to normal backbone initialization via a Hash you can also
+ // just pass a single argument representing id to the ctor
+ initialize: function(data) {
+ // if a hash not passed in the first argument is set as value for key 0
+ 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) {
+ this.set({label: this.id});
+ }
+ }
+});
+
+my.FieldList = Backbone.Collection.extend({
+ model: my.Field
+});
+
+// ## A Query object storing Dataset Query state
+my.Query = Backbone.Model.extend({
+ defaults: {
+ size: 100
+ , offset: 0
+ }
+});
+
+// ## Backend registry
+//
+// Backends will register themselves by id into this registry
+my.backends = {};
- // ## A Backbone collection of Documents
- my.DocumentList = Backbone.Collection.extend({
- __type__: 'DocumentList',
- model: my.Document
- });
}(jQuery, this.recline.Model));
var util = function() {
@@ -585,7 +648,7 @@ var util = function() {
, rowActions: '
';
- var _templated = $.mustache(_template, tmplData);
- _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
- if (!options.persist) {
- setTimeout(function() {
- $(_templated).fadeOut(1000, function() {
- $(this).remove();
+// * model: recline.Model.Dataset
+// * config: (optional) graph configuration hash of form:
+//
+// {
+// group: {column name for x-axis},
+// series: [{column name for series A}, {column name series B}, ... ],
+// graphType: 'line'
+// }
+//
+// NB: should *not* provide an el argument to the view but must let the view
+// generate the element itself (you can then append view.el to the DOM.
+my.FlotGraph = Backbone.View.extend({
+
+ tagName: "div",
+ className: "data-graph-container",
+
+ template: ' \
+
\
+
\
+
Help »
\
+
To create a chart select a column (group) to use as the x-axis \
+ then another column (Series A) to plot against it.
\
+
You can add add \
+ additional series by clicking the "Add series" button
\
+
\
+ \
+
\
+ \
+
\
+',
+
+ events: {
+ 'change form select': 'onEditorSubmit'
+ , 'click .editor-add': 'addSeries'
+ , 'click .action-remove-series': 'removeSeries'
+ , 'click .action-toggle-help': 'toggleHelp'
+ },
+
+ initialize: function(options, config) {
+ var self = this;
+ this.el = $(this.el);
+ _.bindAll(this, 'render', 'redraw');
+ // we need the model.fields to render properly
+ this.model.bind('change', this.render);
+ this.model.fields.bind('reset', this.render);
+ this.model.fields.bind('add', this.render);
+ this.model.currentDocuments.bind('add', this.redraw);
+ this.model.currentDocuments.bind('reset', this.redraw);
+ var configFromHash = my.parseHashQueryString().graph;
+ if (configFromHash) {
+ configFromHash = JSON.parse(configFromHash);
+ }
+ this.chartConfig = _.extend({
+ group: null,
+ series: [],
+ graphType: 'line'
+ },
+ configFromHash,
+ config
+ );
+ this.render();
+ },
+
+ render: function() {
+ htmls = $.mustache(this.template, this.model.toTemplateJSON());
+ $(this.el).html(htmls);
+ // now set a load of stuff up
+ this.$graph = this.el.find('.panel.graph');
+ // for use later when adding additional series
+ // could be simpler just to have a common template!
+ this.$seriesClone = this.el.find('.editor-series').clone();
+ this._updateSeries();
+ return this;
+ },
+
+ onEditorSubmit: function(e) {
+ var select = this.el.find('.editor-group select');
+ this._getEditorData();
+ // update navigation
+ // TODO: make this less invasive (e.g. preserve other keys in query string)
+ var qs = my.parseHashQueryString();
+ qs['graph'] = this.chartConfig;
+ my.setHashQueryString(qs);
+ this.redraw();
+ },
+
+ redraw: function() {
+ // There appear to be issues generating a Flot graph if either:
+
+ // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
+ //
+ // Uncaught Invalid dimensions for plot, width = 0, height = 0
+ // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
+ var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
+ if (!this.plot && (!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();
+ },
+
+ _getEditorData: function() {
+ $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();
+ },
+
+ createSeries: function () {
+ var self = this;
+ var series = [];
+ if (this.chartConfig) {
+ $.each(this.chartConfig.series, function (seriesIndex, field) {
+ var points = [];
+ $.each(self.model.currentDocuments.models, function (index, doc) {
+ var x = doc.get(self.chartConfig.group);
+ var y = doc.get(field);
+ if (typeof x === 'string') {
+ x = index;
+ }
+ points.push([x, y]);
+ });
+ series.push({data: points, label: field});
});
- }, 1000);
+ }
+ return series;
+ },
+
+ // Public: Adds a new empty series select box to the editor.
+ //
+ // All but the first select box will have a remove button that allows them
+ // to be removed.
+ //
+ // Returns itself.
+ addSeries: function (e) {
+ e.preventDefault();
+ var element = this.$seriesClone.clone(),
+ label = element.find('label'),
+ index = this.$series.length;
+
+ this.el.find('.editor-series-group').append(element);
+ this._updateSeries();
+ label.append(' [Remove]');
+ label.find('span').text(String.fromCharCode(this.$series.length + 64));
+ return this;
+ },
+
+ // Public: Removes a series list item from the editor.
+ //
+ // Also updates the labels of the remaining series elements.
+ removeSeries: function (e) {
+ e.preventDefault();
+ var $el = $(e.target);
+ $el.parent().parent().remove();
+ this._updateSeries();
+ this.$series.each(function (index) {
+ if (index > 0) {
+ var labelSpan = $(this).prev().find('span');
+ labelSpan.text(String.fromCharCode(index + 65));
+ }
+ });
+ this.onEditorSubmit();
+ },
+
+ toggleHelp: function() {
+ this.el.find('.editor-info').toggleClass('editor-hide-info');
+ },
+
+ // Private: Resets the series property to reference the select elements.
+ //
+ // Returns itself.
+ _updateSeries: function () {
+ this.$series = this.el.find('.editor-series select');
}
-}
+});
-// ## clearNotifications
-//
-// Clear all existing notifications
-my.clearNotifications = function() {
- var $notifications = $('.data-explorer .alert-message');
- $notifications.remove();
-}
+})(jQuery, recline.View);
-// The primary view for the entire application.
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+// ## DataExplorer
//
-// It should be initialized with a recline.Model.Dataset object and an existing
-// dom element to attach to (the existing DOM element is important for
-// rendering of FlotGraph subview).
+// The primary view for the entire application. Usage:
//
-// To pass in configuration options use the config key in initialization hash
-// e.g.
+//
+// var myExplorer = new model.recline.DataExplorer({
+// model: {{recline.Model.Dataset instance}}
+// el: {{an existing dom element}}
+// views: {{page views}}
+// config: {{config options -- see below}}
+// });
+//
//
-// var explorer = new DataExplorer({
-// config: {...}
-// })
+// ### Parameters
+//
+// **model**: (required) Dataset instance.
//
-// Config options:
+// **el**: (required) DOM element.
//
-// * displayCount: how many documents to display initially (default: 10)
-// * readOnly: true/false (default: false) value indicating whether to
-// operate in read-only mode (hiding all editing options).
+// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to
+// show. This is an array of view hashes. If not provided
+// just initialize a DataTable with id 'grid'. Example:
//
-// All other views as contained in this one.
+//
+// var views = [
+// {
+// id: 'grid', // used for routing
+// label: 'Grid', // used for view switcher
+// view: new recline.View.DataTable({
+// model: dataset
+// })
+// },
+// {
+// id: 'graph',
+// label: 'Graph',
+// view: new recline.View.FlotGraph({
+// model: dataset
+// })
+// }
+// ];
+//
+//
+// **config**: Config options like:
+//
+// * displayCount: how many documents to display initially (default: 10)
+// * readOnly: true/false (default: false) value indicating whether to
+// operate in read-only mode (hiding all editing options).
+//
+// NB: the element already being in the DOM is important for rendering of
+// FlotGraph subview.
my.DataExplorer = Backbone.View.extend({
template: ' \
\
\
\
@@ -1139,14 +1409,15 @@ my.DataTable = Backbone.View.extend({
toTemplateJSON: function() {
var modelData = this.model.toJSON()
- modelData.notEmpty = ( this.headers.length > 0 )
- modelData.headers = this.headers;
+ modelData.notEmpty = ( this.fields.length > 0 )
+ // TODO: move this sort of thing into a toTemplateJSON method on Dataset?
+ modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
return modelData;
},
render: function() {
var self = this;
- this.headers = _.filter(this.model.get('headers'), function(header) {
- return _.indexOf(self.hiddenHeaders, header) == -1;
+ this.fields = this.model.fields.filter(function(field) {
+ return _.indexOf(self.hiddenFields, field.id) == -1;
});
var htmls = $.mustache(this.template, this.toTemplateJSON());
this.el.html(htmls);
@@ -1156,23 +1427,23 @@ my.DataTable = Backbone.View.extend({
var newView = new my.DataTableRow({
model: doc,
el: tr,
- headers: self.headers,
+ fields: self.fields,
});
newView.render();
});
- $(".root-header-menu").toggle((self.hiddenHeaders.length > 0));
+ this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
return this;
}
});
-// DataTableRow View for rendering an individual document.
+// ## DataTableRow View for rendering an individual document.
//
// Since we want this to update in place it is up to creator to provider the element to attach to.
-// In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.
+// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable.
my.DataTableRow = Backbone.View.extend({
initialize: function(options) {
_.bindAll(this, 'render');
- this._headers = options.headers;
+ this._fields = options.fields;
this.el = $(this.el);
this.model.bind('change', this.render);
},
@@ -1180,7 +1451,7 @@ my.DataTableRow = Backbone.View.extend({
template: ' \
\
{{#cells}} \
-
\
+
\
\
\
{{value}}
\
@@ -1197,8 +1468,8 @@ my.DataTableRow = Backbone.View.extend({
toTemplateJSON: function() {
var doc = this.model;
- var cellData = _.map(this._headers, function(header) {
- return {header: header, value: doc.get(header)}
+ var cellData = this._fields.map(function(field) {
+ return {field: field.id, value: doc.get(field.id)}
})
return { id: this.id, cells: cellData }
},
@@ -1210,8 +1481,8 @@ my.DataTableRow = Backbone.View.extend({
return this;
},
- // ======================================================
// Cell Editor
+ // ===========
onEditClick: function(e) {
var editing = this.el.find('.data-table-cell-editor-editor');
@@ -1227,10 +1498,10 @@ my.DataTableRow = Backbone.View.extend({
onEditorOK: function(e) {
var cell = $(e.target);
var rowId = cell.parents('tr').attr('data-id');
- var header = cell.parents('td').attr('data-header');
+ var field = cell.parents('td').attr('data-field');
var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
var newData = {};
- newData[header] = newValue;
+ newData[field] = newValue;
this.model.set(newData);
my.notify("Updating row...", {loader: true});
this.model.save().then(function(response) {
@@ -1251,6 +1522,180 @@ my.DataTableRow = Backbone.View.extend({
});
+/* ========================================================== */
+// ## 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 {};
+ } else {
+ return {
+ path: parsed[1],
+ query: parsed[2] || ''
+ }
+ }
+}
+
+// Parse a URL query string (?xyz=abc...) into a dictionary.
+my.parseQueryString = function(q) {
+ var urlParams = {},
+ e, d = function (s) {
+ return unescape(s.replace(/\+/g, " "));
+ },
+ r = /([^&=]+)=?([^&]*)/g;
+
+ 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]);
+ }
+ return urlParams;
+}
+
+// Parse the query string out of the URL hash
+my.parseHashQueryString = function() {
+ q = my.parseHashUrl(window.location.hash).query;
+ return my.parseQueryString(q);
+}
+
+// Compse a Query String
+my.composeQueryString = function(queryParams) {
+ var queryString = '?';
+ var items = [];
+ $.each(queryParams, function(key, value) {
+ items.push(key + '=' + JSON.stringify(value));
+ });
+ queryString += items.join('&');
+ return queryString;
+}
+
+my.setHashQueryString = function(queryParams) {
+ window.location.hash = window.location.hash.split('?')[0] + my.composeQueryString(queryParams);
+}
+
+// ## notify
+//
+// Create a notification (a div.alert-message in div.alert-messsages) using provide messages and options. Options are:
+//
+// * category: warning (default), success, error
+// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
+// * loader: if true show loading spinner
+my.notify = function(message, options) {
+ if (!options) var options = {};
+ var tmplData = _.extend({
+ msg: message,
+ category: 'warning'
+ },
+ options);
+ var _template = ' \
+
Traverse and transform objects by visiting every node on a recursive walk using js-traverse.
\
-
\
- \
-
\
-
\
-
\
-
\
- \
-
\
-
\
- Expression \
-
\
-
\
-
\
-
\
-
\
- \
-
\
-
\
-
\
- No syntax error. \
-
\
-
\
-
\
-
\
-
\
- Preview \
-
\
-
\
-
\
-
\
-
\
-
\
-
\
- \
-
\
-
\
-
\
-
\
- \
-
\
-
\
-
\
- \
- ',
-
- initialize: function() {
- this.el = $(this.el);
- },
-
- render: function() {
- this.el.html(this.template);
- }
-});
-
-
-// Graph view for a Dataset using Flot graphing library.
-//
-// Initialization arguments:
-//
-// * model: recline.Model.Dataset
-// * config: (optional) graph configuration hash of form:
-//
-// {
-// group: {column name for x-axis},
-// series: [{column name for series A}, {column name series B}, ... ],
-// graphType: 'line'
-// }
-//
-// NB: should *not* provide an el argument to the view but must let the view
-// generate the element itself (you can then append view.el to the DOM.
-my.FlotGraph = Backbone.View.extend({
-
- tagName: "div",
- className: "data-graph-container",
-
- template: ' \
-
\
-
\
-
Help »
\
-
To create a chart select a column (group) to use as the x-axis \
- then another column (Series A) to plot against it.
\
-
You can add add \
- additional series by clicking the "Add series" button
\
-
\
- \
-
\
- \
-
\
-',
-
- events: {
- 'change form select': 'onEditorSubmit'
- , 'click .editor-add': 'addSeries'
- , 'click .action-remove-series': 'removeSeries'
- , 'click .action-toggle-help': 'toggleHelp'
- },
-
- initialize: function(options, config) {
- var self = this;
- this.el = $(this.el);
- _.bindAll(this, 'render', 'redraw');
- // we need the model.headers to render properly
- this.model.bind('change', this.render);
- this.model.currentDocuments.bind('add', this.redraw);
- this.model.currentDocuments.bind('reset', this.redraw);
- this.chartConfig = _.extend({
- group: null,
- series: [],
- graphType: 'line'
- },
- config)
- this.render();
- },
-
- toTemplateJSON: function() {
- return this.model.toJSON();
- },
-
- render: function() {
- htmls = $.mustache(this.template, this.toTemplateJSON());
- $(this.el).html(htmls);
- // now set a load of stuff up
- this.$graph = this.el.find('.panel.graph');
- // for use later when adding additional series
- // could be simpler just to have a common template!
- this.$seriesClone = this.el.find('.editor-series').clone();
- this._updateSeries();
- return this;
- },
-
- onEditorSubmit: function(e) {
- var select = this.el.find('.editor-group select');
- this._getEditorData();
- // update navigation
- // TODO: make this less invasive (e.g. preserve other keys in query string)
- window.location.hash = window.location.hash.split('?')[0] +
- '?graph=' + JSON.stringify(this.chartConfig);
- this.redraw();
- },
-
- redraw: function() {
- // There appear to be issues generating a Flot graph if either:
-
- // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
- //
- // Uncaught Invalid dimensions for plot, width = 0, height = 0
- // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
- var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
- if (!this.plot && (!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();
- },
-
- _getEditorData: function() {
- $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();
- },
-
- createSeries: function () {
- var self = this;
- var series = [];
- if (this.chartConfig) {
- $.each(this.chartConfig.series, function (seriesIndex, field) {
- var points = [];
- $.each(self.model.currentDocuments.models, function (index, doc) {
- var x = doc.get(self.chartConfig.group);
- var y = doc.get(field);
- if (typeof x === 'string') {
- x = index;
- }
- points.push([x, y]);
- });
- series.push({data: points, label: field});
- });
- }
- return series;
- },
-
- // Public: Adds a new empty series select box to the editor.
- //
- // All but the first select box will have a remove button that allows them
- // to be removed.
- //
- // Returns itself.
- addSeries: function (e) {
- e.preventDefault();
- var element = this.$seriesClone.clone(),
- label = element.find('label'),
- index = this.$series.length;
-
- this.el.find('.editor-series-group').append(element);
- this._updateSeries();
- label.append(' [Remove]');
- label.find('span').text(String.fromCharCode(this.$series.length + 64));
- return this;
- },
-
- // Public: Removes a series list item from the editor.
- //
- // Also updates the labels of the remaining series elements.
- removeSeries: function (e) {
- e.preventDefault();
- var $el = $(e.target);
- $el.parent().parent().remove();
- this._updateSeries();
- this.$series.each(function (index) {
- if (index > 0) {
- var labelSpan = $(this).prev().find('span');
- labelSpan.text(String.fromCharCode(index + 65));
- }
- });
- this.onEditorSubmit();
- },
-
- toggleHelp: function() {
- this.el.find('.editor-info').toggleClass('editor-hide-info');
- },
-
- // Private: Resets the series property to reference the select elements.
- //
- // Returns itself.
- _updateSeries: function () {
- this.$series = this.el.find('.editor-series select');
- }
-});
-
-return my;
-
-}(jQuery);
-
+})(jQuery, recline.View);
diff --git a/src/backend.js b/src/backend.js
index 98dff9da..f723aac8 100644
--- a/src/backend.js
+++ b/src/backend.js
@@ -48,7 +48,7 @@ this.recline.Model = this.recline.Model || {};
//
// To use it you should provide in your constructor data:
//
- // * metadata (including headers array)
+ // * metadata (including fields array)
// * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique.
//
// Example:
@@ -59,9 +59,9 @@ this.recline.Model = this.recline.Model || {};
// backend.addDataset({
// metadata: {
// id: 'my-id',
- // title: 'My Title',
- // headers: ['x', 'y', 'z'],
+ // title: 'My Title'
// },
+ // fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
// documents: [
// {id: 0, x: 1, y: 2, z: 3},
// {id: 1, x: 2, y: 4, z: 6}
@@ -86,6 +86,7 @@ this.recline.Model = this.recline.Model || {};
if (model.__type__ == 'Dataset') {
var rawDataset = this.datasets[model.id];
model.set(rawDataset.metadata);
+ model.fields.reset(rawDataset.fields);
model.docCount = rawDataset.documents.length;
dfd.resolve(model);
}
@@ -153,12 +154,12 @@ this.recline.Model = this.recline.Model || {};
});
var dfd = $.Deferred();
wrapInTimeout(jqxhr).done(function(schema) {
- headers = _.map(schema.data, function(item) {
- return item.name;
- });
- model.set({
- headers: headers
+ 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);
})
@@ -227,9 +228,10 @@ this.recline.Model = this.recline.Model || {};
});
var dfd = $.Deferred();
wrapInTimeout(jqxhr).done(function(results) {
- model.set({
- headers: results.fields
- });
+ model.fields.reset(_.map(results.fields, function(fieldId) {
+ return {id: fieldId};
+ })
+ );
dfd.resolve(model, jqxhr);
})
.fail(function(arguments) {
@@ -293,7 +295,10 @@ this.recline.Model = this.recline.Model || {};
$.getJSON(model.get('url'), function(d) {
result = self.gdocsToJavascript(d);
- model.set({'headers': result.header});
+ 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;
dfd.resolve(model);
@@ -303,9 +308,9 @@ this.recline.Model = this.recline.Model || {};
query: function(dataset, queryObj) {
var dfd = $.Deferred();
- var fields = dataset.get('headers');
+ var fields = _.pluck(dataset.fields.toJSON(), 'id');
- // zip the field headers with the data rows to produce js objs
+ // 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 = {};
@@ -318,9 +323,9 @@ this.recline.Model = this.recline.Model || {};
gdocsToJavascript: function(gdocsSpreadsheet) {
/*
:options: (optional) optional argument dictionary:
- columnsToUse: list of columns to use (specified by header names)
+ columnsToUse: list of columns to use (specified by field names)
colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
- :return: tabular data object (hash with keys: header and data).
+ :return: tabular data object (hash with keys: field and data).
Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
*/
@@ -329,7 +334,7 @@ this.recline.Model = this.recline.Model || {};
options = arguments[1];
}
var results = {
- 'header': [],
+ 'field': [],
'data': []
};
// default is no special info on type of columns
@@ -340,14 +345,14 @@ this.recline.Model = this.recline.Model || {};
// either extract column headings from spreadsheet directly, or used supplied ones
if (options.columnsToUse) {
// columns set to subset supplied
- results.header = options.columnsToUse;
+ results.field = options.columnsToUse;
} else {
// set columns to use to be all available
if (gdocsSpreadsheet.feed.entry.length > 0) {
for (var k in gdocsSpreadsheet.feed.entry[0]) {
if (k.substr(0, 3) == 'gsx') {
var col = k.substr(4)
- results.header.push(col);
+ results.field.push(col);
}
}
}
@@ -357,8 +362,8 @@ this.recline.Model = this.recline.Model || {};
var rep = /^([\d\.\-]+)\%$/;
$.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
var row = [];
- for (var k in results.header) {
- var col = results.header[k];
+ 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
diff --git a/src/model.js b/src/model.js
index 034b196a..932902d9 100644
--- a/src/model.js
+++ b/src/model.js
@@ -3,85 +3,124 @@ this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) {
- // ## A Dataset model
- //
- // Other than standard list of Backbone methods it has two important attributes:
- //
- // * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
- // * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
- my.Dataset = Backbone.Model.extend({
- __type__: 'Dataset',
- initialize: function(model, backend) {
- this.backend = backend;
- if (backend && backend.constructor == String) {
- this.backend = my.backends[backend];
- }
- this.currentDocuments = new my.DocumentList();
- this.docCount = null;
- this.defaultQuery = {
- size: 100
- , offset: 0
- };
- // this.queryState = {};
- },
- // ### getDocuments
- //
- // AJAX method with promise API to get rows (documents) from the backend.
- //
- // Resulting DocumentList are used to reset this.currentDocuments and are
- // also returned.
- //
- // :param numRows: passed onto backend getDocuments.
- // :param start: passed onto backend getDocuments.
- //
- // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here.
- // This also illustrates the limitations of separating the Dataset and the Backend
- query: function(queryObj) {
- var self = this;
- this.queryState = queryObj || this.defaultQuery;
- this.queryState = _.extend({size: 100, offset: 0}, this.queryState);
- var dfd = $.Deferred();
- this.backend.query(this, this.queryState).done(function(rows) {
- var docs = _.map(rows, function(row) {
- var _doc = new my.Document(row);
- _doc.backend = self.backend;
- _doc.dataset = self;
- return _doc;
- });
- self.currentDocuments.reset(docs);
- dfd.resolve(self.currentDocuments);
- })
- .fail(function(arguments) {
- dfd.reject(arguments);
- });
- return dfd.promise();
- },
-
- toTemplateJSON: function() {
- var data = this.toJSON();
- data.docCount = this.docCount;
- return data;
+// ## A Dataset model
+//
+// A model must have the following (Backbone) attributes:
+//
+// * fields: (aka columns) is a FieldList listing all the fields on this
+// Dataset (this can be set explicitly, or, on fetch() of Dataset
+// information from the backend, or as is perhaps most common on the first
+// query)
+// * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
+// * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
+my.Dataset = Backbone.Model.extend({
+ __type__: 'Dataset',
+ initialize: function(model, backend) {
+ _.bindAll(this, 'query');
+ this.backend = backend;
+ if (backend && backend.constructor == String) {
+ this.backend = my.backends[backend];
}
- });
+ this.fields = new my.FieldList();
+ this.currentDocuments = new my.DocumentList();
+ this.docCount = null;
+ this.queryState = new my.Query();
+ this.queryState.bind('change', this.query);
+ },
- // ## A Document (aka Row)
- //
- // A single entry or row in the dataset
- my.Document = Backbone.Model.extend({
- __type__: 'Document'
- });
-
- // ## A Backbone collection of Documents
- my.DocumentList = Backbone.Collection.extend({
- __type__: 'DocumentList',
- model: my.Document
- });
-
- // ## Backend registry
+ // ### query
//
- // Backends will register themselves by id into this registry
- my.backends = {};
+ // AJAX method with promise API to get documents from the backend.
+ //
+ // It will query based on current query state (given by this.queryState)
+ // updated by queryObj (if provided).
+ //
+ // Resulting DocumentList are used to reset this.currentDocuments and are
+ // also returned.
+ query: function(queryObj) {
+ var self = this;
+ this.queryState.set(queryObj, {silent: true});
+ var dfd = $.Deferred();
+ this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
+ var docs = _.map(rows, function(row) {
+ var _doc = new my.Document(row);
+ _doc.backend = self.backend;
+ _doc.dataset = self;
+ return _doc;
+ });
+ self.currentDocuments.reset(docs);
+ dfd.resolve(self.currentDocuments);
+ })
+ .fail(function(arguments) {
+ dfd.reject(arguments);
+ });
+ return dfd.promise();
+ },
+
+ toTemplateJSON: function() {
+ var data = this.toJSON();
+ data.docCount = this.docCount;
+ data.fields = this.fields.toJSON();
+ return data;
+ }
+});
+
+// ## A Document (aka Row)
+//
+// A single entry or row in the dataset
+my.Document = Backbone.Model.extend({
+ __type__: 'Document'
+});
+
+// ## A Backbone collection of Documents
+my.DocumentList = Backbone.Collection.extend({
+ __type__: 'DocumentList',
+ model: my.Document
+});
+
+// ## A Field (aka Column) on a Dataset
+//
+// Following attributes as standard:
+//
+// * id: a unique identifer for this field- usually this should match the key in the documents hash
+// * label: the visible label used for this field
+// * type: the type of the data
+my.Field = Backbone.Model.extend({
+ defaults: {
+ id: null,
+ label: null,
+ type: 'String'
+ },
+ // In addition to normal backbone initialization via a Hash you can also
+ // just pass a single argument representing id to the ctor
+ initialize: function(data) {
+ // if a hash not passed in the first argument is set as value for key 0
+ 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) {
+ this.set({label: this.id});
+ }
+ }
+});
+
+my.FieldList = Backbone.Collection.extend({
+ model: my.Field
+});
+
+// ## A Query object storing Dataset Query state
+my.Query = Backbone.Model.extend({
+ defaults: {
+ size: 100
+ , offset: 0
+ }
+});
+
+// ## Backend registry
+//
+// Backends will register themselves by id into this registry
+my.backends = {};
}(jQuery, this.recline.Model));
diff --git a/src/view-data-explorer.js b/src/view-data-explorer.js
deleted file mode 100644
index 7e3c2258..00000000
--- a/src/view-data-explorer.js
+++ /dev/null
@@ -1,171 +0,0 @@
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-// Views module following classic module pattern
-(function($, my) {
-
-// The primary view for the entire application.
-//
-// It should be initialized with a recline.Model.Dataset object and an existing
-// dom element to attach to (the existing DOM element is important for
-// rendering of FlotGraph subview).
-//
-// To pass in configuration options use the config key in initialization hash
-// e.g.
-//
-// var explorer = new DataExplorer({
-// config: {...}
-// })
-//
-// Config options:
-//
-// * displayCount: how many documents to display initially (default: 10)
-// * readOnly: true/false (default: false) value indicating whether to
-// operate in read-only mode (hiding all editing options).
-//
-// All other views as contained in this one.
-my.DataExplorer = Backbone.View.extend({
- template: ' \
-
');
- self.el.find('tbody').append(tr);
- var newView = new my.DataTableRow({
- model: doc,
- el: tr,
- headers: self.headers,
- });
- newView.render();
- });
- this.el.toggleClass('no-hidden', (self.hiddenHeaders.length == 0));
- return this;
- }
-});
-
-// DataTableRow View for rendering an individual document.
-//
-// Since we want this to update in place it is up to creator to provider the element to attach to.
-// In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.
-my.DataTableRow = Backbone.View.extend({
- initialize: function(options) {
- _.bindAll(this, 'render');
- this._headers = options.headers;
- this.el = $(this.el);
- this.model.bind('change', this.render);
- },
-
- template: ' \
-
\
@@ -86,25 +85,29 @@ my.FlotGraph = Backbone.View.extend({
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render', 'redraw');
- // we need the model.headers to render properly
+ // we need the model.fields to render properly
this.model.bind('change', this.render);
+ this.model.fields.bind('reset', this.render);
+ this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
this.model.currentDocuments.bind('reset', this.redraw);
+ var configFromHash = my.parseHashQueryString().graph;
+ if (configFromHash) {
+ configFromHash = JSON.parse(configFromHash);
+ }
this.chartConfig = _.extend({
group: null,
series: [],
graphType: 'line'
},
- config)
+ configFromHash,
+ config
+ );
this.render();
},
- toTemplateJSON: function() {
- return this.model.toJSON();
- },
-
render: function() {
- htmls = $.mustache(this.template, this.toTemplateJSON());
+ htmls = $.mustache(this.template, this.model.toTemplateJSON());
$(this.el).html(htmls);
// now set a load of stuff up
this.$graph = this.el.find('.panel.graph');
@@ -120,8 +123,9 @@ my.FlotGraph = Backbone.View.extend({
this._getEditorData();
// update navigation
// TODO: make this less invasive (e.g. preserve other keys in query string)
- window.location.hash = window.location.hash.split('?')[0] +
- '?graph=' + JSON.stringify(this.chartConfig);
+ var qs = my.parseHashQueryString();
+ qs['graph'] = this.chartConfig;
+ my.setHashQueryString(qs);
this.redraw();
},
diff --git a/src/view.js b/src/view.js
index 5e79a62a..32f7cc9c 100644
--- a/src/view.js
+++ b/src/view.js
@@ -1,11 +1,511 @@
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
-// Views module following classic module pattern
(function($, my) {
+// ## DataExplorer
+//
+// The primary view for the entire application. Usage:
+//
+//
+// var myExplorer = new model.recline.DataExplorer({
+// model: {{recline.Model.Dataset instance}}
+// el: {{an existing dom element}}
+// views: {{page views}}
+// config: {{config options -- see below}}
+// });
+//
+//
+// ### Parameters
+//
+// **model**: (required) Dataset instance.
+//
+// **el**: (required) DOM element.
+//
+// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to
+// show. This is an array of view hashes. If not provided
+// just initialize a DataTable with id 'grid'. Example:
+//
+//
+// var views = [
+// {
+// id: 'grid', // used for routing
+// label: 'Grid', // used for view switcher
+// view: new recline.View.DataTable({
+// model: dataset
+// })
+// },
+// {
+// id: 'graph',
+// label: 'Graph',
+// view: new recline.View.FlotGraph({
+// model: dataset
+// })
+// }
+// ];
+//
+//
+// **config**: Config options like:
+//
+// * displayCount: how many documents to display initially (default: 10)
+// * readOnly: true/false (default: false) value indicating whether to
+// operate in read-only mode (hiding all editing options).
+//
+// NB: the element already being in the DOM is important for rendering of
+// FlotGraph subview.
+my.DataExplorer = Backbone.View.extend({
+ template: ' \
+
\
+ ',
+
+ toTemplateJSON: function() {
+ var modelData = this.model.toJSON()
+ modelData.notEmpty = ( this.fields.length > 0 )
+ // TODO: move this sort of thing into a toTemplateJSON method on Dataset?
+ modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
+ return modelData;
+ },
+ render: function() {
+ var self = this;
+ this.fields = this.model.fields.filter(function(field) {
+ return _.indexOf(self.hiddenFields, field.id) == -1;
+ });
+ var htmls = $.mustache(this.template, this.toTemplateJSON());
+ this.el.html(htmls);
+ this.model.currentDocuments.forEach(function(doc) {
+ var tr = $('
');
+ self.el.find('tbody').append(tr);
+ var newView = new my.DataTableRow({
+ model: doc,
+ el: tr,
+ fields: self.fields,
+ });
+ newView.render();
+ });
+ this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
+ return this;
+ }
+});
+
+// ## DataTableRow View for rendering an individual document.
+//
+// Since we want this to update in place it is up to creator to provider the element to attach to.
+// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable.
+my.DataTableRow = Backbone.View.extend({
+ initialize: function(options) {
+ _.bindAll(this, 'render');
+ this._fields = options.fields;
+ this.el = $(this.el);
+ this.model.bind('change', this.render);
+ },
+
+ template: ' \
+