diff --git a/README.md b/README.md
index 00dc3d9f..aeb750ff 100755
--- a/README.md
+++ b/README.md
@@ -1,26 +1,60 @@
-# Recline
-
-A pure javascript data explorer and data refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization toolkit, all in pure javascript and html.
+Recline DataExplorer is an open-source pure javascript data explorer and data
+refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization
+toolkit, all in pure javascript and html.
Designed for standalone use or as a library to integrate into your own app.
+
## Features
-* CSV/JSON export your entire database for integration with spreadsheets or
- [Google Refine](http://code.google.com/p/google-refine/)
+* Open-source (and heavy reuser of existing open-source libraries)
+* Pure javascript (no Flash) and designed for integration -- so it is easy to
+ embed in other sites and applications
+* View and edit your data in clean tabular interface
* Bulk update/clean your data using an easy scripting UI
-* Import by directly downloading from JSON APIs or by uploading files
+* Visualize your data

+
## Demo App
Open demo/index.html in your favourite browser.
-## Minifying dependencies
+
+## Developer Notes
+
+### Minifying dependencies
npm install -g uglify
cd vendor
cat *.js | uglifyjs -o ../src/deps-min.js
note: make sure underscore.js goes in at the top of the file as a few deps currently depend on it
+
+
+## Copyright and License
+
+Copyright 2011 Max Ogden and Rufus Pollock.
+
+Licensed under the MIT license:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
diff --git a/demo/js/app.js b/demo/js/app.js
index 21f360bc..1bf6b6f6 100755
--- a/demo/js/app.js
+++ b/demo/js/app.js
@@ -3,14 +3,14 @@ $(function() {
// window.$container = $('.container .right-panel');
window.$container = $('.container');
var dataset = demoDataset();
- window.dataExplorer = new recline.DataExplorer({
+ window.dataExplorer = new recline.View.DataExplorer({
model: dataset
});
window.$container.append(window.dataExplorer.el);
setupLoadFromWebstore(function(dataset) {
window.dataExplorer.remove();
window.dataExplorer = null;
- window.dataExplorer = new recline.DataExplorer({
+ window.dataExplorer = new recline.View.DataExplorer({
model: dataset,
});
window.$container.append(window.dataExplorer.el);
@@ -27,21 +27,20 @@ function demoDataset() {
var indata = {
headers: ['x', 'y', 'z']
, rows: [
- {x: 1, y: 2, z: 3}
- , {x: 2, y: 4, z: 6}
- , {x: 3, y: 6, z: 9}
- , {x: 4, y: 8, z: 12}
- , {x: 5, y: 10, z: 15}
- , {x: 6, y: 12, z: 18}
+ {id: 0, x: 1, y: 2, z: 3}
+ , {id: 1, x: 2, y: 4, z: 6}
+ , {id: 2, x: 3, y: 6, z: 9}
+ , {id: 3, x: 4, y: 8, z: 12}
+ , {id: 4, x: 5, y: 10, z: 15}
+ , {id: 5, x: 6, y: 12, z: 18}
]
};
// this is all rather artificial here but would make more sense with more complex backend
- var backend = new recline.BackendMemory();
- backend.addDataset({
+ var backend = new recline.Model.BackendMemory({
metadata: metadata,
data: indata
});
- recline.setBackend(backend);
+ recline.Model.setBackend(backend);
var dataset = backend.getDataset(datasetId);
return dataset;
}
@@ -54,10 +53,10 @@ function setupLoadFromWebstore(callback) {
e.preventDefault();
var $form = $(e.target);
var source = $form.find('input[name="source"]').val();
- var backend = new recline.BackendWebstore({
+ var backend = new recline.Model.BackendWebstore({
url: source
});
- recline.setBackend(backend);
+ recline.Model.setBackend(backend);
var dataset = backend.getDataset();
callback(dataset);
});
diff --git a/demo/style/style.css b/demo/style/style.css
index 5e073b3a..40c295f4 100755
--- a/demo/style/style.css
+++ b/demo/style/style.css
@@ -239,3 +239,13 @@ span.tooltip-status {
right: 0px;
background: white;
}
+
+.nav-pagination {
+ display: inline;
+ float: right;
+ list-style-type: none;
+}
+
+.nav-pagination input {
+ width: 40px;
+}
diff --git a/src/backbone-webstore.js b/src/backbone-webstore.js
deleted file mode 100644
index 14d02ae3..00000000
--- a/src/backbone-webstore.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// replaces `Backbone.sync` with a OKFN webstore based tabular data source
-
-var WebStore = function(url) {
- this.url = url;
- this.headers = [];
- this.totalRows = 0;
- this.getTabularData = function() {
- var dfd = $.Deferred();
- var tabularData = {
- headers: ['x', 'y', 'z']
- , rows: [
- {x: 1, y: 2, z: 3}
- , {x: 2, y: 4, z: 6}
- , {x: 3, y: 6, z: 9}
- , {x: 4, y: 8, z: 12}
- , {x: 5, y: 10, z: 15}
- , {x: 6, y: 12, z: 18}
- ]
- , getLength: function() { return this.rows.length; }
- , getRows: function(numRows, start) {
- if (start === undefined) {
- start = 0;
- }
- var dfd = $.Deferred();
- var results = this.rows.slice(start, start + numRows);
- dfd.resolve(results);
- return dfd.promise();
- }
- }
- dfd.resolve(tabularData);
- return dfd.promise();
- }
-};
-
-
-// Override `Backbone.sync` to delegate to the model or collection's
-// webStore property, which should be an instance of `WebStore`.
-Backbone.sync = function(method, model, options) {
- var resp;
- var store = model.webStore || model.collection.webStore;
-
- if (method === "read") {
- store.getTabularData().then(function(tabularData) {
- tabularData.getRows(10).then(options.success, options.error)
- })
- }
-
-};
\ No newline at end of file
diff --git a/src/costco.js b/src/costco.js
index 7c5b089c..edce7590 100755
--- a/src/costco.js
+++ b/src/costco.js
@@ -25,6 +25,7 @@ var costco = function() {
preview.push({before: JSON.stringify(before), after: JSON.stringify(after)});
}
}
+ // TODO: 2012-01-05 Move this out of this function and up into (view) functions that call this
util.render('editPreview', 'expression-preview-container', {rows: preview});
}
@@ -166,4 +167,4 @@ var costco = function() {
ensureCommit: ensureCommit,
uploadCSV: uploadCSV
};
-}();
\ No newline at end of file
+}();
diff --git a/src/model.js b/src/model.js
index 83f4a49a..ba143881 100644
--- a/src/model.js
+++ b/src/model.js
@@ -1,61 +1,105 @@
this.recline = this.recline || {};
+// Models module following classic module pattern
+recline.Model = function($) {
+
+var my = {};
+
// A Dataset model.
-recline.Dataset = Backbone.Model.extend({
+my.Dataset = Backbone.Model.extend({
__type__: 'Dataset',
+ initialize: function() {
+ this.currentDocuments = new my.DocumentList();
+ },
+
getLength: function() {
return this.rowCount;
},
+
+ // Get rows (documents) from the backend returning a recline.DocumentList
+ //
+ // TODO: ? rename to 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
getRows: function(numRows, start) {
- return this.backend.getRows(this.id, numRows, start);
+ var self = this;
+ var dfd = $.Deferred();
+ this.backend.getRows(this.id, numRows, start).then(function(rows) {
+ var docs = _.map(rows, function(row) {
+ return new my.Document(row);
+ });
+ self.currentDocuments.reset(docs);
+ dfd.resolve(self.currentDocuments);
+ });
+ return dfd.promise();
}
});
-recline.Document = Backbone.Model.extend({});
+my.Document = Backbone.Model.extend({
+ __type__: 'Document'
+});
-recline.DocumentList = Backbone.Collection.extend({
+my.DocumentList = Backbone.Collection.extend({
+ __type__: 'DocumentList',
// webStore: new WebStore(this.url),
- model: recline.Document
-})
+ model: my.Document
+});
// Backends section
// ================
-recline.setBackend = function(backend) {
+my.setBackend = function(backend) {
Backbone.sync = backend.sync;
};
// Backend which just caches in memory
//
// Does not need to be a backbone model but provides some conveience
-recline.BackendMemory = Backbone.Model.extend({
- initialize: function() {
- this._datasetCache = {}
+my.BackendMemory = Backbone.Model.extend({
+ // Initialize a Backend with a local in-memory dataset.
+ //
+ // NB: We can handle one and only one dataset at a time.
+ //
+ // :param dataset: the data for a dataset on which operations will be
+ // performed. In the form of a hash with metadata and data attributes.
+ // - metadata: hash of key/value attributes of any kind (but usually with title attribute)
+ // - 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:
+ //
+ // {
+ // headers: ['x', 'y', 'z']
+ // , rows: [
+ // {id: 0, x: 1, y: 2, z: 3}
+ // , {id: 1, x: 2, y: 4, z: 6}
+ // ]
+ // };
+ initialize: function(dataset) {
+ // deep copy
+ this._datasetAsData = $.extend(true, {}, dataset);
+ _.bindAll(this, 'sync');
},
- // dataset is object with metadata and data attributes
- addDataset: function(dataset) {
- this._datasetCache[dataset.metadata.id] = dataset;
- },
- getDataset: function(id) {
- var dataset = new recline.Dataset({
- id: id
+ getDataset: function() {
+ var dataset = new my.Dataset({
+ id: this._datasetAsData.metadata.id
});
// this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly
dataset.backend = this;
return dataset;
},
sync: function(method, model, options) {
+ var self = this;
if (method === "read") {
var dfd = $.Deferred();
// this switching on object type is rather horrible
// think may make more sense to do work in individual objects rather than in central Backbone.sync
- if (this.__type__ == 'Dataset') {
- var dataset = this;
- var rawDataset = this.backend._datasetCache[model.id];
+ if (model.__type__ == 'Dataset') {
+ var dataset = model;
+ var rawDataset = this._datasetAsData;
dataset.set(rawDataset.metadata);
- // here we munge it all onto Dataset
dataset.set({
headers: rawDataset.data.headers
});
@@ -63,6 +107,28 @@ recline.BackendMemory = Backbone.Model.extend({
dfd.resolve(dataset);
}
return dfd.promise();
+ } else if (method === 'update') {
+ var dfd = $.Deferred();
+ if (model.__type__ == 'Document') {
+ _.each(this._datasetAsData.data.rows, function(row, idx) {
+ if(row.id === model.id) {
+ self._datasetAsData.data.rows[idx] = model.toJSON();
+ }
+ });
+ dfd.resolve(model);
+ }
+ return dfd.promise();
+ } else if (method === 'delete') {
+ var dfd = $.Deferred();
+ if (model.__type__ == 'Document') {
+ this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) {
+ return (row.id === model.id);
+ });
+ dfd.resolve(model);
+ }
+ return dfd.promise();
+ } else {
+ alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model);
}
},
getRows: function(datasetId, numRows, start) {
@@ -73,7 +139,7 @@ recline.BackendMemory = Backbone.Model.extend({
numRows = 10;
}
var dfd = $.Deferred();
- rows = this._datasetCache[datasetId].data.rows;
+ rows = this._datasetAsData.data.rows;
var results = rows.slice(start, start+numRows);
dfd.resolve(results);
return dfd.promise();
@@ -82,15 +148,15 @@ recline.BackendMemory = Backbone.Model.extend({
// Webstore Backend for connecting to the Webstore
//
-// Designed to only attached to only dataset and one dataset only ...
+// Designed to only attach to one dataset and one dataset only ...
// Could generalize to support attaching to different datasets
-recline.BackendWebstore = Backbone.Model.extend({
+my.BackendWebstore = Backbone.Model.extend({
// require url attribute in initialization data
initialize: function() {
this.webstoreTableUrl = this.get('url');
},
getDataset: function(id) {
- var dataset = new recline.Dataset({
+ var dataset = new my.Dataset({
id: id
});
dataset.backend = this;
@@ -146,3 +212,8 @@ recline.BackendWebstore = Backbone.Model.extend({
return dfd.promise();
}
});
+
+return my;
+
+}(jQuery);
+
diff --git a/src/recline.js b/src/recline.js
index 62e4a132..967788b8 100755
--- a/src/recline.js
+++ b/src/recline.js
@@ -1,41 +1,5 @@
-window.recline = {};
-
-recline.Document = Backbone.Model.extend({});
-
-recline.DocumentList = Backbone.Collection.extend({
- webStore: new WebStore(this.url),
- model: recline.Document
-});
-
-recline.DataTable = Backbone.View.extend({
-
- el: ".data-table-container",
-
- documents: new recline.DocumentList(this.url),
-
- // template: TODO ???
-
- events: {
-
- },
-
- initialize: function() {
- var that = this;
- this.documents.fetch({
- success: function(collection, resp) {
- that.render()
- }
- })
- },
-
- render: function() {
- var template = $( ".dataTableTemplate:first" ).html()
- , htmls = $.mustache(template, {rows: this.documents.toJSON()} )
- ;
- $(this.el).html(htmls);
- return this;
- }
-});
+// Original Recline code - **Deprecated**
+// Left intact while functionality is transferred across to new Backbone setup
// var recline = function() {
//
diff --git a/src/view.js b/src/view.js
index f580beae..e416d697 100644
--- a/src/view.js
+++ b/src/view.js
@@ -1,6 +1,11 @@
this.recline = this.recline || {};
-recline.DataExplorer = Backbone.View.extend({
+// Views module following classic module pattern
+recline.View = function($) {
+
+var my = {};
+
+my.DataExplorer = Backbone.View.extend({
tagName: 'div',
className: 'data-explorer',
template: ' \
@@ -11,37 +16,58 @@ recline.DataExplorer = Backbone.View.extend({
\
\
\
+
\
+ \
+
\
\
\
',
events: {
- 'change input[name="nav-toggle"]': 'navChange'
+ 'change input[name="nav-toggle"]': 'navChange',
+ 'submit form.display-count': 'displayCountUpdate'
},
- initialize: function() {
+ initialize: function(options) {
this.el = $(this.el);
+ this.config = options.config || {};
+ _.extend(this.config, {
+ displayCount: 10
+ });
+ this.draw();
+ },
+
+ displayCountUpdate: function(e) {
+ e.preventDefault();
+ this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
+ this.draw();
+ },
+
+ draw: function() {
+ var self = this;
+ this.el.empty();
this.render();
this.$dataViewContainer = this.el.find('.data-view-container');
- var self = this;
// retrieve basic data like headers etc
// note this.model and dataset returned are the same
this.model.fetch().then(function(dataset) {
// initialize of dataTable calls render
- self.dataTable = new recline.DataTable({
+ self.dataTable = new my.DataTable({
model: dataset
});
- self.flotGraph = new recline.FlotGraph({
+ self.flotGraph = new my.FlotGraph({
model: dataset
});
self.flotGraph.el.hide();
self.$dataViewContainer.append(self.dataTable.el)
self.$dataViewContainer.append(self.flotGraph.el);
+ self.model.getRows(self.config.displayCount);
});
},
render: function() {
- $(this.el).html($(this.template));
+ var template = $.mustache(this.template, this.config);
+ $(this.el).html(template);
},
navChange: function(e) {
@@ -62,17 +88,20 @@ recline.DataExplorer = Backbone.View.extend({
}
});
-recline.DataTable = Backbone.View.extend({
+// DataTable provides a tabular view on a Dataset.
+//
+// Initialize it with a recline.Dataset object.
+my.DataTable = Backbone.View.extend({
tagName: "div",
className: "data-table-container",
initialize: function() {
- this.el = $(this.el);
var self = this;
- this.model.getRows().then(function(rows) {
- self._currentRows = rows;
- self.render()
- });
+ this.el = $(this.el);
+ _.bindAll(this, 'render');
+ this.model.currentDocuments.bind('add', this.render);
+ this.model.currentDocuments.bind('reset', this.render);
+ this.model.currentDocuments.bind('remove', this.render);
this.state = {};
// this is nasty. Due to fact that .menu element is not inside this view but is elsewhere in DOM
$('.menu li a').live('click', function(e) {
@@ -85,11 +114,7 @@ recline.DataTable = Backbone.View.extend({
// see initialize
// 'click .menu li': 'onMenuClick',
'click .column-header-menu': 'onColumnHeaderClick',
- 'click .row-header-menu': 'onRowHeaderClick',
- 'click .data-table-cell-edit': 'onEditClick',
- // cell editor
- 'click .data-table-cell-editor .okButton': 'onEditorOK',
- 'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
+ 'click .row-header-menu': 'onRowHeaderClick'
},
showDialog: function(template, data) {
@@ -122,8 +147,8 @@ recline.DataTable = Backbone.View.extend({
var self = this;
e.preventDefault();
var actions = {
- bulkEdit: function() { self.showDialog('bulkEdit', {name: self.state.currentColumn}) },
- transform: function() { showDialog('transform') },
+ bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
+ transform: function() { self.showTransformDialog('transform') },
csv: function() { window.location.href = app.csvUrl },
json: function() { window.location.href = "_rewrite/api/json" },
urlImport: function() { showDialog('urlImport') },
@@ -137,25 +162,146 @@ recline.DataTable = Backbone.View.extend({
if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
},
deleteRow: function() {
- // TODO:
- alert('This function needs to be re-implemented');
- return;
- var doc = _.find(app.cache, function(doc) { return doc._id === app.currentRow });
- doc._deleted = true;
- costco.uploadDocs([doc]).then(
- function(updatedDocs) {
+ var doc = _.find(self.model.currentDocuments.models, function(doc) {
+ // important this is == as the currentRow will be string (as comes
+ // from DOM) while id may be int
+ return doc.id == self.state.currentRow
+ });
+ doc.destroy().then(function() {
+ self.model.currentDocuments.remove(doc);
util.notify("Row deleted successfully");
- recline.initializeTable(app.offset);
- },
- function(err) { util.notify("Errorz! " + err) }
- )
+ })
+ .fail(function(err) {
+ util.notify("Errorz! " + err)
+ })
}
}
-
util.hide('menu');
actions[$(e.target).attr('data-action')]();
},
+ showTransformColumnDialog: function() {
+ var $el = $('.dialog-content');
+ util.show('dialog');
+ var view = new my.ColumnTransform({
+ model: this.model
+ });
+ view.state = this.state;
+ view.render();
+ $el.empty();
+ $el.append(view.el);
+ util.observeExit($el, function() {
+ util.hide('dialog');
+ })
+ $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
+ },
+
+ showTransformDialog: function() {
+ var $el = $('.dialog-content');
+ util.show('dialog');
+ var view = new my.DataTransform({
+ });
+ view.render();
+ $el.empty();
+ $el.append(view.el);
+ util.observeExit($el, function() {
+ util.hide('dialog');
+ })
+ $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
+ },
+
+
+ // ======================================================
+ // Core Templating
+ template: ' \
+
');
+ self.el.find('tbody').append(tr);
+ var newView = new my.DataTableRow({
+ model: doc,
+ el: tr,
+ headers: self.model.get('headers')
+ });
+ newView.render();
+ });
+ 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: ' \
+