+ Recline DataExplorer +
+This is a documentation page for the Recline DataExplorer project.
+Demos
+ + +Docs
+Coming soon!
+From 6d8a0a27006a8203902267944184c2db36c89800 Mon Sep 17 00:00:00 2001
From: rgrp This is a documentation page for the Recline DataExplorer project. Coming soon! This is a documentation page for the Recline DataExplorer project. Designed for standalone use or as a library to integrate into your own
+ app. Coming soon! Max Ogden was developing Recline as the frontend data browser and editor his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team at the Open Knowledge Foundation had been working on a Data Explorer for use in the DataHub and CKAN software. When they met up, they realized that they were pretty much working on the same thing and so decided to join forces to produce the new Recline Data Explorer. The new project forked off Max's original recline codebase combining some portions of the original Data Explorer. However, it was rapidly rewritten from the ground up using Backbone. Models module following classic module pattern A Dataset model. Other than standard list of Backbone attributes it has two important attributes: 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 webStore: new WebStore(this.url), Backend which just caches in memory Does not need to be a backbone model but provides some conveniences 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. Its form should be a hash with metadata and data
+attributes. data: hash with 2 keys: Example of data: {
+ headers: ['x', 'y', 'z']
+ , rows: [
+ {id: 0, x: 1, y: 2, z: 3}
+ , {id: 1, x: 2, y: 4, z: 6}
+ ]
+ }; deep copy 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 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 Webstore Backend for connecting to the Webstore Initializing model argument must contain a url attribute pointing to
+relevant Webstore table. Designed to only attach to one dataset and one dataset only ...
+Could generalize to support attaching to different datasets 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 get the schema and return DataProxy Backend for connecting to the DataProxy Example initialization: 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 get the schema and return TODO: should we cache for extra efficiency , cache: true Views module following classic module pattern Parse a URL query string (?xyz=abc...) into a dictionary. TODO: have values be array as query string allow repetition of keys 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. Config options: All other views as contained in this one. Hash of 'page' views (i.e. those for whole page) keyed by page name this must be called after pageViews are created retrieve basic data like headers etc
+note this.model and dataset returned are the same initialize of dataTable calls render we have to call here due to fact plot may not have been able to draw
+if it was hidden until now - see comments in FlotGraph.redraw show the specific page DataTable provides a tabular view on a Dataset. Initialize it with a recline.Dataset object. 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');
+ util.render(template, 'dialog-content', data);
+ util.observeExit($('.dialog-content'), function() {
+ util.hide('dialog');
+ })
+ $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
+}, ======================================================
+Column and row menus TODO: Delete or re-implement ... END TODO TODO: important this is == as the currentRow will be string (as comes
+from DOM) while id may be int ======================================================
+Core Templating 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. cell editor ======================================================
+Cell Editor View (Dialog) for doing data transformations (on columns of data). Put in the basic (identity) transform script
+TODO: put this into the template? TODO: notify about failed docs? TODO: Very inefficient as we search through all docs every time! if you don't setTimeout it won't grab the latest character if you call e.target.value View (Dialog) for doing data transformations on whole dataset. Graph view for a Dataset using Flot graphing library. Initialization arguments: 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. we need the model.headers to render properly now set a load of stuff up for use later when adding additional series
+could be simpler just to have a common template! update navigation
+TODO: make this less invasive (e.g. preserve other keys in query string) 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 create this.plot and cache it only lines for the present 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. Public: Removes a series list item from the editor. Also updates the labels of the remaining series elements. Private: Resets the series property to reference the select elements. Returns itself. Coming soon! Want to see how to embed this in your own application. Check out the the
+ Demo and view source. Max Ogden was developing Recline as the frontend data browser and editor his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team at the Open Knowledge Foundation had been working on a Data Explorer for use in the DataHub and CKAN software. When they met up, they realized that they were pretty much working on the same thing and so decided to join forces to produce the new Recline Data Explorer. The new project forked off Max's original recline codebase combining some portions of the original Data Explorer. However, it was rapidly rewritten from the ground up using Backbone. Max Ogden was developing Recline as the frontend data browser and editor
+ his http://datacouch.com/ project.
+ Meanwhile, Rufus Pollock and the CKAN team
+ at the Open Knowledge Foundation had been
+ working on a Data
+ Explorer for use in the DataHub
+ and CKAN software. When they met up,
+ they realized that they were pretty much working on the same thing and so
+ decided to join forces to produce the new Recline Data Explorer. The
+ new project forked off Max's
+ original recline codebase combining some portions of the original Data Explorer.
+ However, it was rapidly rewritten from the ground up using Backbone. Designed for standalone use or as a library to integrate into your own
app. Want to see how to embed this in your own application. Check out the the
Demo and view source. Max Ogden was developing Recline as the frontend data browser and editor
his http://datacouch.com/ project.
Meanwhile, Rufus Pollock and the CKAN team
From 99b8cb425bed94f72e0fa2742ca7a1bf219213d9 Mon Sep 17 00:00:00 2001
From: Rufus Pollock Max Ogden was developing Recline as the frontend data browser and editor
- his http://datacouch.com/ project.
+ for his http://datacouch.com/ project.
Meanwhile, Rufus Pollock and the CKAN team
at the Open Knowledge Foundation had been
working on a Data
Explorer for use in the DataHub
- and CKAN software. When they met up,
- they realized that they were pretty much working on the same thing and so
- decided to join forces to produce the new Recline Data Explorer. The
- new project forked off Max's
- original recline codebase combining some portions of the CKAN software. When they met up, they realized that they were pretty much working on
+ the same thing and so decided to join forces to produce the new Recline
+ Data Explorer. The new project forked off Max's original recline
+ codebase combining some portions of the original Data Explorer.
- However, it was rapidly rewritten from the ground up using Backbone.
+ Recline DataExplorer
+
+ Demos
+
+
+ Docs
+
- Recline DataExplorer
+ Recline DataExplorer Data explorer and refinery in pure Javascript
Recline DataExplorer is 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.
+ More information can be found in the project README on Recline DataExplorer GitHub
+ project page.
Demos
+
- Recline DataExplorer Data explorer and refinery in pure Javascript
+ Recline Data Explorer Data explorer and refinery all in pure Javascript
Recline DataExplorer is 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.
- More information can be found in the project README on Recline DataExplorer GitHub
- project page.
- Demos
+ Recline combines a data grid, Google Refine-style data transforms
+ and visualizations all in lightweight javascript and html.
+ Main Features
+
+
+
+ Demo
Docs
History
+
model.js
this.recline = this.recline || {};
recline.Model = function($) {
+
+var my = {};
+
my.Dataset = Backbone.Model.extend({
+ __type__: 'Dataset',
+ initialize: function() {
+ this.currentDocuments = new my.DocumentList();
+ this.docCount = null;
+ },
getDocuments: function(numRows, start) {
+ var self = this;
+ var dfd = $.Deferred();
+ this.backend.getDocuments(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();
+ },
+
+ toTemplateJSON: function() {
+ var data = this.toJSON();
+ data.docCount = this.docCount;
+ return data;
+ }
+});
+
+my.Document = Backbone.Model.extend({
+ __type__: 'Document'
+});
+
+my.DocumentList = Backbone.Collection.extend({
+ __type__: 'DocumentList',
model: my.Document
+});
Backends section
my.setBackend = function(backend) {
+ Backbone.sync = backend.sync;
+};
my.BackendMemory = Backbone.Model.extend({
+
+
+ initialize: function(dataset) {
this._datasetAsData = $.extend(true, {}, dataset);
+ _.bindAll(this, 'sync');
+ },
+ getDataset: function() {
+ var dataset = new my.Dataset({
+ id: this._datasetAsData.metadata.id
+ });
dataset.backend = this;
+ return dataset;
+ },
+ sync: function(method, model, options) {
+ var self = this;
+ if (method === "read") {
+ var dfd = $.Deferred();
if (model.__type__ == 'Dataset') {
+ var dataset = model;
+ var rawDataset = this._datasetAsData;
+ dataset.set(rawDataset.metadata);
+ dataset.set({
+ headers: rawDataset.data.headers
+ });
+ dataset.docCount = rawDataset.data.rows.length;
+ 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);
+ }
+ },
+ getDocuments: function(datasetId, numRows, start) {
+ if (start === undefined) {
+ start = 0;
+ }
+ if (numRows === undefined) {
+ numRows = 10;
+ }
+ var dfd = $.Deferred();
+ rows = this._datasetAsData.data.rows;
+ var results = rows.slice(start, start+numRows);
+ dfd.resolve(results);
+ return dfd.promise();
+ }
+});
my.BackendWebstore = Backbone.Model.extend({
+ getDataset: function(id) {
+ var dataset = new my.Dataset({
+ id: id
+ });
+ dataset.backend = this;
+ return dataset;
+ },
+ sync: function(method, model, options) {
+ if (method === "read") {
if (this.__type__ == 'Dataset') {
+ var dataset = this;
var base = this.backend.get('url');
+ var schemaUrl = base + '/schema.json';
+ var jqxhr = $.ajax({
+ url: schemaUrl,
+ dataType: 'jsonp',
+ jsonp: '_callback'
+ });
+ var dfd = $.Deferred();
+ jqxhr.then(function(schema) {
+ headers = _.map(schema.data, function(item) {
+ return item.name;
+ });
+ dataset.set({
+ headers: headers
+ });
+ dataset.docCount = schema.count;
+ dfd.resolve(dataset, jqxhr);
+ });
+ return dfd.promise();
+ }
+ }
+ },
+ getDocuments: function(datasetId, numRows, start) {
+ if (start === undefined) {
+ start = 0;
+ }
+ if (numRows === undefined) {
+ numRows = 10;
+ }
+ var base = this.get('url');
+ var jqxhr = $.ajax({
+ url: base + '.json?_limit=' + numRows,
+ dataType: 'jsonp',
+ jsonp: '_callback',
+ cache: true
+ });
+ var dfd = $.Deferred();
+ jqxhr.then(function(results) {
+ dfd.resolve(results.data);
+ });
+ return dfd.promise();
+ }
+});
BackendDataProxy({
+ model: {
+ url: {url-of-data-to-proxy},
+ type: xls || csv,
+ format: json || jsonp # return format (defaults to jsonp)
+ dataproxy: {url-to-proxy} # defaults to http://jsonpdataproxy.appspot.com
+ }
+})
+ my.BackendDataProxy = Backbone.Model.extend({
+ defaults: {
+ dataproxy: 'http://jsonpdataproxy.appspot.com'
+ , type: 'csv'
+ , format: 'jsonp'
+ },
+ getDataset: function(id) {
+ var dataset = new my.Dataset({
+ id: id
+ });
+ dataset.backend = this;
+ return dataset;
+ },
+ sync: function(method, model, options) {
+ if (method === "read") {
if (this.__type__ == 'Dataset') {
+ var dataset = this;
var base = this.backend.get('dataproxy');
+ var data = this.backend.toJSON();
+ delete data['dataproxy'];
data['max-results'] = 1;
+ var jqxhr = $.ajax({
+ url: base
+ , data: data
+ , dataType: 'jsonp'
+ });
+ var dfd = $.Deferred();
+ jqxhr.then(function(results) {
+ dataset.set({
+ headers: results.fields
+ });
+ dfd.resolve(dataset, jqxhr);
+ });
+ return dfd.promise();
+ }
+ } else {
+ alert('This backend only supports read operations');
+ }
+ },
+ getDocuments: function(datasetId, numRows, start) {
+ if (start === undefined) {
+ start = 0;
+ }
+ if (numRows === undefined) {
+ numRows = 10;
+ }
+ var base = this.get('dataproxy');
+ var data = this.toJSON();
+ delete data['dataproxy'];
+ data['max-results'] = numRows;
+ var jqxhr = $.ajax({
+ url: base
+ , data: data
+ , dataType: 'jsonp'
});
+ var dfd = $.Deferred();
+ jqxhr.then(function(results) {
+ var _out = _.map(results.data, function(row) {
+ var tmp = {};
+ _.each(results.fields, function(key, idx) {
+ tmp[key] = row[idx];
+ });
+ return tmp;
+ });
+ dfd.resolve(_out);
+ });
+ return dfd.promise();
+ }
+});
+
+return my;
+
+}(jQuery);
+
+
view.js
this.recline = this.recline || {};
recline.View = function($) {
+
+var my = {};
function parseQueryString(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)) {
urlParams[d(e[1])] = d(e[2]);
+ }
+ return urlParams;
+}
+
+ var explorer = new DataExplorer({
+ config: {...}
+ })
+
+
+
+ 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> \
+ </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> \
+ </form> \
+ </div> \
+ </div> \
+ <div class="data-view-container"></div> \
+ <div class="dialog-overlay" style="display: none; z-index: 101; "> </div> \
+ <div class="dialog ui-draggable" style="display: none; z-index: 102; top: 101px; "> \
+ <div class="dialog-frame" style="width: 700px; visibility: visible; "> \
+ <div class="dialog-content dialog-border"></div> \
+ </div> \
+ </div> \
+ </div> \
+ ',
+
+ events: {
+ 'submit form.display-count': 'onDisplayCountUpdate'
+ },
+
+ initialize: function(options) {
+ var self = this;
+ this.el = $(this.el);
+ this.config = options.config || {};
+ _.extend(this.config, {
+ displayCount: 10
+ , readOnly: false
+ });
+ if (this.config.readOnly) {
+ this.setReadOnly();
+ }
this.pageViews = {
+ grid: new my.DataTable({
+ model: this.model
+ })
+ , graph: new my.FlotGraph({
+ model: this.model
+ })
+ };
this.render();
+
+ this.router = new Backbone.Router();
+ this.setupRouting();
this.model.fetch().then(function(dataset) {
+ self.el.find('.doc-count').text(self.model.docCount);
self.model.getDocuments(self.config.displayCount);
+ });
+ },
+
+ onDisplayCountUpdate: function(e) {
+ e.preventDefault();
+ this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
+ this.model.getDocuments(this.config.displayCount);
+ },
+
+ setReadOnly: function() {
+ this.el.addClass('read-only');
+ },
+
+ render: function() {
+ var tmplData = this.model.toTemplateJSON();
+ tmplData.displayCount = this.config.displayCount;
+ var template = $.mustache(this.template, tmplData);
+ $(this.el).html(template);
+ var $dataViewContainer = this.el.find('.data-view-container');
+ _.each(this.pageViews, function(view, pageName) {
+ $dataViewContainer.append(view.el)
+ });
+ },
+
+ setupRouting: function() {
+ var self = this;
+ this.router.route('', 'grid', function() {
+ self.updateNav('grid');
+ });
+ this.router.route(/grid(\?.*)?/, 'view', function(queryString) {
+ self.updateNav('grid', queryString);
+ });
+ this.router.route(/graph(\?.*)?/, 'graph', function(queryString) {
+ self.updateNav('graph', queryString);
qsParsed = parseQueryString(queryString);
+ if ('graph' in qsParsed) {
+ var chartConfig = JSON.parse(qsParsed['graph']);
+ _.extend(self.pageViews['graph'].chartConfig, chartConfig);
+ }
+ self.pageViews['graph'].redraw();
+ });
+ },
+
+ updateNav: function(pageName, queryString) {
+ this.el.find('.navigation li').removeClass('active');
+ var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
+ $el.parent().addClass('active');
_.each(this.pageViews, function(view, pageViewName) {
+ if (pageViewName === pageName) {
+ view.el.show();
+ } else {
+ view.el.hide();
+ }
+ });
+ }
+});
my.DataTable = Backbone.View.extend({
+ tagName: "div",
+ className: "data-table-container",
+
+ initialize: function() {
+ var self = this;
+ 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 = {};
+ },
+
+ events: {
+ 'click .column-header-menu': 'onColumnHeaderClick'
+ , 'click .row-header-menu': 'onRowHeaderClick'
+ , 'click .data-table-menu li a': 'onMenuClick'
+ },
onColumnHeaderClick: function(e) {
+ this.state.currentColumn = $(e.target).siblings().text();
+ 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');
+ },
+
+ onMenuClick: function(e) {
+ var self = this;
+ e.preventDefault();
+ var actions = {
+ 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') },
+ pasteImport: function() { showDialog('pasteImport') },
+ uploadImport: function() { showDialog('uploadImport') },
deleteColumn: function() {
+ var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";
alert('This function needs to be re-implemented');
+ return;
+ if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
+ },
+ deleteRow: function() {
+ var doc = _.find(self.model.currentDocuments.models, function(doc) {
return doc.id == self.state.currentRow
+ });
+ doc.destroy().then(function() {
+ self.model.currentDocuments.remove(doc);
+ util.notify("Row deleted successfully");
+ })
+ .fail(function(err) {
+ util.notify("Errorz! " + err)
+ })
+ }
+ }
+ util.hide('data-table-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' });
+ },
template: ' \
+ <div class="data-table-menu-overlay" style="display: none; z-index: 101; "> </div> \
+ <ul class="data-table-menu"></ul> \
+ <table class="data-table" cellspacing="0"> \
+ <thead> \
+ <tr> \
+ {{#notEmpty}}<th class="column-header"></th>{{/notEmpty}} \
+ {{#headers}} \
+ <th class="column-header"> \
+ <div class="column-header-title"> \
+ <a class="column-header-menu"></a> \
+ <span class="column-header-name">{{.}}</span> \
+ </div> \
+ </div> \
+ </th> \
+ {{/headers}} \
+ </tr> \
+ </thead> \
+ <tbody></tbody> \
+ </table> \
+ ',
+
+ toTemplateJSON: function() {
+ var modelData = this.model.toJSON()
+ modelData.notEmpty = ( modelData.headers.length > 0 )
+ return modelData;
+ },
+ render: function() {
+ var self = this;
+ var htmls = $.mustache(this.template, this.toTemplateJSON());
+ this.el.html(htmls);
+ this.model.currentDocuments.forEach(function(doc) {
+ var tr = $('<tr />');
+ self.el.find('tbody').append(tr);
+ var newView = new my.DataTableRow({
+ model: doc,
+ el: tr,
+ headers: self.model.get('headers')
+ });
+ newView.render();
+ });
+ return this;
+ }
+});
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: ' \
+ <td><a class="row-header-menu"></a></td> \
+ {{#cells}} \
+ <td data-header="{{header}}"> \
+ <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> \
+ </div> \
+ </td> \
+ {{/cells}} \
+ ',
+ events: {
+ 'click .data-table-cell-edit': 'onEditClick',
'click .data-table-cell-editor .okButton': 'onEditorOK',
+ 'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
+ },
+
+ toTemplateJSON: function() {
+ var doc = this.model;
+ var cellData = _.map(this._headers, function(header) {
+ return {header: header, value: doc.get(header)}
+ })
+ return { id: this.id, cells: cellData }
+ },
+
+ render: function() {
+ this.el.attr('data-id', this.model.id);
+ var html = $.mustache(this.template, this.toTemplateJSON());
+ $(this.el).html(html);
+ return this;
+ },
onEditClick: function(e) {
+ var editing = this.el.find('.data-table-cell-editor-editor');
+ if (editing.length > 0) {
+ editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
+ }
+ $(e.target).addClass("hidden");
+ var cell = $(e.target).siblings('.data-table-cell-value');
+ cell.data("previousContents", cell.text());
+ util.render('cellEditor', cell, {value: cell.text()});
+ },
+
+ onEditorOK: function(e) {
+ var cell = $(e.target);
+ var rowId = cell.parents('tr').attr('data-id');
+ var header = cell.parents('td').attr('data-header');
+ var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
+ var newData = {};
+ newData[header] = newValue;
+ this.model.set(newData);
+ util.notify("Updating row...", {loader: true});
+ this.model.save().then(function(response) {
+ util.notify("Row updated successfully", {category: 'success'});
+ })
+ .fail(function() {
+ util.notify('Error saving row', {
+ category: 'error',
+ persist: true
+ });
+ });
+ },
+
+ onEditorCancel: function(e) {
+ var cell = $(e.target).parents('.data-table-cell-value');
+ cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
+ }
+});
my.ColumnTransform = Backbone.View.extend({
+ className: 'transform-column-view',
+ template: ' \
+ <div class="dialog-header"> \
+ Functional transform on column {{name}} \
+ </div> \
+ <div class="dialog-body"> \
+ <div class="grid-layout layout-tight layout-full"> \
+ <table> \
+ <tbody> \
+ <tr> \
+ <td colspan="4"> \
+ <div class="grid-layout layout-tight layout-full"> \
+ <table rows="4" cols="4"> \
+ <tbody> \
+ <tr style="vertical-align: bottom;"> \
+ <td colspan="4"> \
+ Expression \
+ </td> \
+ </tr> \
+ <tr> \
+ <td colspan="3"> \
+ <div class="input-container"> \
+ <textarea class="expression-preview-code"></textarea> \
+ </div> \
+ </td> \
+ <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
+ No syntax error. \
+ </td> \
+ </tr> \
+ <tr> \
+ <td colspan="4"> \
+ <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
+ <span>Preview</span> \
+ <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
+ <div class="expression-preview-container" style="width: 652px; "> \
+ </div> \
+ </div> \
+ </div> \
+ </td> \
+ </tr> \
+ </tbody> \
+ </table> \
+ </div> \
+ </td> \
+ </tr> \
+ </tbody> \
+ </table> \
+ </div> \
+ </div> \
+ <div class="dialog-footer"> \
+ <button class="okButton btn primary"> Update All </button> \
+ <button class="cancelButton btn danger">Cancel</button> \
+ </div> \
+ ',
+
+ events: {
+ 'click .okButton': 'onSubmit'
+ , 'keydown .expression-preview-code': 'onEditorKeydown'
+ },
+
+ initialize: function() {
+ this.el = $(this.el);
+ },
+
+ render: function() {
+ var htmls = $.mustache(this.template,
+ {name: this.state.currentColumn}
+ )
+ this.el.html(htmls);
var editor = this.el.find('.expression-preview-code');
+ editor.val("function(doc) {\n doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n return doc;\n}");
+ editor.focus().get(0).setSelectionRange(18, 18);
+ editor.keydown();
+ },
+
+ onSubmit: function(e) {
+ var self = this;
+ var funcText = this.el.find('.expression-preview-code').val();
+ var editFunc = costco.evalFunction(funcText);
+ if (editFunc.errorMessage) {
+ util.notify("Error with function! " + editFunc.errorMessage);
+ return;
+ }
+ util.hide('dialog');
+ util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true});
+ var docs = self.model.currentDocuments.map(function(doc) {
+ return doc.toJSON();
+ });
var toUpdate = costco.mapDocs(docs, editFunc).edited;
+ var totalToUpdate = toUpdate.length;
+ function onCompletedUpdate() {
+ 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();
+ }
+ }
_.each(toUpdate, function(editedDoc) {
+ var realDoc = self.model.currentDocuments.get(editedDoc.id);
+ realDoc.set(editedDoc);
+ realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
+ });
+ },
+
+ onEditorKeydown: function(e) {
+ var self = this;
window.setTimeout( function() {
+ var errors = self.el.find('.expression-preview-parsing-status');
+ var editFunc = costco.evalFunction(e.target.value);
+ if (!editFunc.errorMessage) {
+ errors.text('No syntax error.');
+ var docs = self.model.currentDocuments.map(function(doc) {
+ return doc.toJSON();
+ });
+ var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
+ util.render('editPreview', 'expression-preview-container', {rows: previewData});
+ } else {
+ errors.text(editFunc.errorMessage);
+ }
+ }, 1, true);
+ }
+});
my.DataTransform = Backbone.View.extend({
+ className: 'transform-view',
+ template: ' \
+ <div class="dialog-header"> \
+ Recursive transform on all rows \
+ </div> \
+ <div class="dialog-body"> \
+ <div class="grid-layout layout-full"> \
+ <p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
+ <table> \
+ <tbody> \
+ <tr> \
+ <td colspan="4"> \
+ <div class="grid-layout layout-tight layout-full"> \
+ <table rows="4" cols="4"> \
+ <tbody> \
+ <tr style="vertical-align: bottom;"> \
+ <td colspan="4"> \
+ Expression \
+ </td> \
+ </tr> \
+ <tr> \
+ <td colspan="3"> \
+ <div class="input-container"> \
+ <textarea class="expression-preview-code"></textarea> \
+ </div> \
+ </td> \
+ <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
+ No syntax error. \
+ </td> \
+ </tr> \
+ <tr> \
+ <td colspan="4"> \
+ <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
+ <span>Preview</span> \
+ <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
+ <div class="expression-preview-container" style="width: 652px; "> \
+ </div> \
+ </div> \
+ </div> \
+ </td> \
+ </tr> \
+ </tbody> \
+ </table> \
+ </div> \
+ </td> \
+ </tr> \
+ </tbody> \
+ </table> \
+ </div> \
+ </div> \
+ <div class="dialog-footer"> \
+ <button class="okButton button"> Update All </button> \
+ <button class="cancelButton button">Cancel</button> \
+ </div> \
+ ',
+
+ initialize: function() {
+ this.el = $(this.el);
+ },
+
+ render: function() {
+ this.el.html(this.template);
+ }
+});
+
+
+ my.FlotGraph = Backbone.View.extend({
+
+ tagName: "div",
+ className: "data-graph-container",
+
+ template: ' \
+ <div class="editor"> \
+ <div class="editor-info editor-hide-info"> \
+ <h3 class="action-toggle-help">Help »</h3> \
+ <p>To create a chart select a column (group) to use as the x-axis \
+ then another column (Series A) to plot against it.</p> \
+ <p>You can add add \
+ additional series by clicking the "Add series" button</p> \
+ </div> \
+ <form class="form-stacked"> \
+ <div class="clearfix"> \
+ <label>Graph Type</label> \
+ <div class="input editor-type"> \
+ <select> \
+ <option value="line">Line</option> \
+ </select> \
+ </div> \
+ <label>Group Column (x-axis)</label> \
+ <div class="input editor-group"> \
+ <select> \
+ {{#headers}} \
+ <option value="{{.}}">{{.}}</option> \
+ {{/headers}} \
+ </select> \
+ </div> \
+ <div class="editor-series-group"> \
+ <div class="editor-series"> \
+ <label>Series <span>A (y-axis)</span></label> \
+ <div class="input"> \
+ <select> \
+ {{#headers}} \
+ <option value="{{.}}">{{.}}</option> \
+ {{/headers}} \
+ </select> \
+ </div> \
+ </div> \
+ </div> \
+ </div> \
+ <div class="editor-buttons"> \
+ <button class="btn editor-add">Add Series</button> \
+ </div> \
+ <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
+ <button class="editor-save">Save</button> \
+ <input type="hidden" class="editor-id" value="chart-1" /> \
+ </div> \
+ </form> \
+ </div> \
+ <div class="panel graph"></div> \
+</div> \
+',
+
+ 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');
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);
this.$graph = this.el.find('.panel.graph');
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();
window.location.hash = window.location.hash.split('?')[0] +
+ '?graph=' + JSON.stringify(this.chartConfig);
+ this.redraw();
+ },
+
+ redraw: function() {
+
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
+ if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) {
+ return
+ }
if (!this.plot) {
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;
+ },
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(' [<a href="#remove" class="action-remove-series">Remove</a>]');
+ label.find('span').text(String.fromCharCode(this.$series.length + 64));
+ return this;
+ },
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');
+ },
_updateSeries: function () {
+ this.$series = this.el.find('.editor-series select');
+ }
+});
+
+return my;
+
+}(jQuery);
+
+
Main Features
-
@@ -82,12 +85,27 @@
Docs
- History
- Main Features
+ Main Features
- Demo
+ Demo
- Docs
+ Docs
@@ -92,7 +92,7 @@
- History
+ History
History