';
- var _templated = $.mustache(_template, tmplData);
- _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
- if (!options.persist) {
- setTimeout(function() {
- $(_templated).remove();
- }, 3000);
- }
- }
-
- function formatMetadata(data) {
- out = '
';
- $.each(data, function(key, val) {
- if (typeof(val) == 'string' && key[0] != '_') {
- out = out + '
' + key + '
' + val;
- } else if (typeof(val) == 'object' && key != "geometry" && val != null) {
- if (key == 'properties') {
- $.each(val, function(attr, value){
- out = out + '
' + attr + '
' + value;
- })
- } else {
- out = out + '
' + key + '
' + val.join(', ');
- }
- }
- });
- out = out + '
';
- return out;
- }
-
- function getBaseURL(url) {
- var baseURL = "";
- if ( inURL(url, '_design') ) {
- if (inURL(url, '_rewrite')) {
- var path = url.split("#")[0];
- if (path[path.length - 1] === "/") {
- baseURL = "";
- } else {
- baseURL = '_rewrite/';
- }
- } else {
- baseURL = '_rewrite/';
- }
- }
- return baseURL;
- }
-
- var persist = {
- restore: function() {
- $('.persist').each(function(i, el) {
- var inputId = $(el).attr('id');
- if(localStorage.getItem(inputId)) $('#' + inputId).val(localStorage.getItem(inputId));
- })
- },
- save: function(id) {
- localStorage.setItem(id, $('#' + id).val());
- },
- clear: function() {
- $('.persist').each(function(i, el) {
- localStorage.removeItem($(el).attr('id'));
- })
- }
- }
-
- // simple debounce adapted from underscore.js
- function delay(func, wait) {
- return function() {
- var context = this, args = arguments;
- var throttler = function() {
- delete app.timeout;
- func.apply(context, args);
- };
- if (!app.timeout) app.timeout = setTimeout(throttler, wait);
- };
- };
-
- function resetForm(form) {
- $(':input', form)
- .not(':button, :submit, :reset, :hidden')
- .val('')
- .removeAttr('checked')
- .removeAttr('selected');
- }
-
- function largestWidth(selector, min) {
- var min_width = min || 0;
- $(selector).each(function(i, n){
- var this_width = $(n).width();
- if (this_width > min_width) {
- min_width = this_width;
- }
- });
- return min_width;
- }
-
- function getType(obj) {
- if (obj === null) {
- return 'null';
- }
- if (typeof obj === 'object') {
- if (obj.constructor.toString().indexOf("Array") !== -1) {
- return 'array';
- } else {
- return 'object';
- }
- } else {
- return typeof obj;
- }
- }
-
- function lookupPath(path) {
- var docs = app.apiDocs;
- try {
- _.each(path, function(node) {
- docs = docs[node];
- })
- } catch(e) {
- util.notify("Error selecting documents" + e);
- docs = [];
- }
- return docs;
- }
-
- function nodePath(docField) {
- if (docField.children('.object-key').length > 0) return docField.children('.object-key').text();
- if (docField.children('.array-key').length > 0) return docField.children('.array-key').text();
- if (docField.children('.doc-key').length > 0) return docField.children('.doc-key').text();
- return "";
- }
-
- function selectedTreePath() {
- var nodes = []
- , parent = $('.chosen');
- while (parent.length > 0) {
- nodes.push(nodePath(parent));
- parent = parent.parents('.doc-field:first');
- }
- return _.compact(nodes).reverse();
- }
-
- // TODO refactor handlers so that they dont stack up as the tree gets bigger
- function handleTreeClick(e) {
- var clicked = $(e.target);
- if(clicked.hasClass('expand')) return;
- if (clicked.children('.array').length > 0) {
- var field = clicked;
- } else if (clicked.siblings('.array').length > 0) {
- var field = clicked.parents('.doc-field:first');
- } else {
- var field = clicked.parents('.array').parents('.doc-field:first');
- }
- $('.chosen').removeClass('chosen');
- field.addClass('chosen');
- return false;
- }
-
- var createTreeNode = {
- "string": function (obj, key) {
- var val = $('');
- if (obj[key].length > 45) {
- val.append($('')
- .text(obj[key].slice(0, 45)))
- .append(
- $('...')
- .click(function () {
- val.html('')
- .append($('')
- .text(obj[key].length ? obj[key] : " ")
- )
- })
- )
- }
- else {
- var val = $('');
- val.append(
- $('')
- .text(obj[key].length ? obj[key] : " ")
- )
- }
- return val;
- }
- , "number": function (obj, key) {
- var val = $('')
- val.append($('' + obj[key] + ''))
- return val;
- }
- , "null": function (obj, key) {
- var val = $('')
- val.append($('' + obj[key] + ''))
- return val;
- }
- , "boolean": function (obj, key) {
- var val = $('')
- val.append($('' + obj[key] + ''))
- return val;
- }
- , "array": function (obj, key, indent) {
- if (!indent) indent = 1;
- var val = $('')
- $('[...]')
- .click(function (e) {
- var n = $(this).parent();
- var cls = 'sub-'+key+'-'+indent
- n.html('')
- n.append('[')
- for (i in obj[key]) {
- var field = $('').click(handleTreeClick);
- n.append(
- field
- .append('
'+i+'
')
- .append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1))
- )
- }
- n.append(']')
- $('div.'+cls).width(largestWidth('div.'+cls))
- })
- .appendTo($('').appendTo(val))
- return val;
- }
- , "object": function (obj, key, indent) {
- if (!indent) indent = 1;
- var val = $('')
- $('{...}')
- .click(function (e) {
- var n = $(this).parent();
- n.html('')
- n.append('{')
- for (i in obj[key]) {
- var field = $('').click(handleTreeClick);
- var p = $('');
- var di = $('
'+i+'
')
- field.append(p)
- .append(di)
- .append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1))
- n.append(field)
- }
-
- n.append('}')
- di.width(largestWidth('div.object-key'))
- })
- .appendTo($('').appendTo(val))
- return val;
- }
- }
-
- function renderTree(doc) {
- var d = $('div#document-editor');
- for (i in doc) {
- var field = $('').click(handleTreeClick);
- $('').appendTo(field);
- field.append('
'+i+'
')
- field.append(createTreeNode[getType(doc[i])](doc, i));
- d.append(field);
- }
-
- $('div.doc-key-base').width(largestWidth('div.doc-key-base'))
- }
-
-
return {
- inURL: inURL,
registerEmitter: registerEmitter,
listenFor: listenFor,
show: show,
hide: hide,
position: position,
render: render,
- notify: notify,
- observeExit: observeExit,
- formatMetadata:formatMetadata,
- getBaseURL:getBaseURL,
- resetForm: resetForm,
- delay: delay,
- persist: persist,
- lookupPath: lookupPath,
- selectedTreePath: selectedTreePath,
- renderTree: renderTree
+ observeExit: observeExit
};
}();
this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
-// Views module following classic module pattern
-recline.View = function($) {
-
-var my = {};
-
-// Parse a URL query string (?xyz=abc...) into a dictionary.
-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)) {
- // TODO: have values be array as query string allow repetition of keys
- urlParams[d(e[1])] = d(e[2]);
- }
- return urlParams;
-}
+(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.
+// ## Graph view for a Dataset using Flot graphing library.
//
-// var explorer = new DataExplorer({
-// config: {...}
-// })
+// Initialization arguments:
//
-// Config options:
+// * model: recline.Model.Dataset
+// * config: (optional) graph configuration hash of form:
//
-// * 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).
+// {
+// group: {column name for x-axis},
+// series: [{column name for series A}, {column name series B}, ... ],
+// graphType: 'line'
+// }
//
-// All other views as contained in this one.
-my.DataExplorer = Backbone.View.extend({
- template: ' \
-
\
- ',
+// 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({
- events: {
- 'submit form.display-count': 'onDisplayCountUpdate'
- },
+ tagName: "div",
+ className: "data-graph-container",
- initialize: function(options) {
- var self = this;
- this.el = $(this.el);
- this.config = _.extend({
- displayCount: 50
- , readOnly: false
- },
- options.config);
- if (this.config.readOnly) {
- this.setReadOnly();
+ 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);
}
- // Hash of 'page' views (i.e. those for whole page) keyed by page name
- this.pageViews = {
- grid: new my.DataTable({
- model: this.model
- })
- , graph: new my.FlotGraph({
- model: this.model
- })
- };
- // this must be called after pageViews are created
+ this.chartConfig = _.extend({
+ group: null,
+ series: [],
+ graphType: 'line'
+ },
+ configFromHash,
+ config
+ );
this.render();
+ },
- this.router = new Backbone.Router();
- this.setupRouting();
-
- // retrieve basic data like headers etc
- // note this.model and dataset returned are the same
- this.model.fetch().then(function(dataset) {
- self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
- // initialize of dataTable calls render
- self.model.getDocuments(self.config.displayCount);
- });
+ 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;
},
- onDisplayCountUpdate: function(e) {
- e.preventDefault();
- this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
- this.model.getDocuments(this.config.displayCount);
+ 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();
},
- setReadOnly: function() {
- this.el.addClass('read-only');
+ 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 ((!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();
},
- 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)
+ _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();
},
- setupRouting: function() {
+ createSeries: 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);
- // 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
- qsParsed = parseQueryString(queryString);
- if ('graph' in qsParsed) {
- var chartConfig = JSON.parse(qsParsed['graph']);
- _.extend(self.pageViews['graph'].chartConfig, chartConfig);
- }
- self.pageViews['graph'].redraw();
- });
+ 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;
},
- 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');
- // show the specific page
- _.each(this.pageViews, function(view, pageViewName) {
- if (pageViewName === pageName) {
- view.el.show();
- } else {
- view.el.hide();
+ // 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');
}
});
-// DataTable provides a tabular view on a Dataset.
+})(jQuery, recline.View);
+
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+// ## DataGrid
+//
+// Provides a tabular view on a Dataset.
//
// Initialize it with a recline.Dataset object.
-my.DataTable = Backbone.View.extend({
+//
+// Additional options passed in second arguments. Options:
+//
+// * cellRenderer: function used to render individual cells. See DataGridRow for more.
+my.DataGrid = Backbone.View.extend({
tagName: "div",
className: "data-table-container",
- initialize: function() {
+ initialize: function(modelEtc, options) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
@@ -1170,11 +675,14 @@ my.DataTable = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
this.state = {};
+ this.hiddenFields = [];
+ this.options = options;
},
events: {
'click .column-header-menu': 'onColumnHeaderClick'
, 'click .row-header-menu': 'onRowHeaderClick'
+ , 'click .root-header-menu': 'onRootHeaderClick'
, 'click .data-table-menu li a': 'onMenuClick'
},
@@ -1194,7 +702,7 @@ my.DataTable = Backbone.View.extend({
// Column and row menus
onColumnHeaderClick: function(e) {
- this.state.currentColumn = $(e.target).siblings().text();
+ this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
util.position('data-table-menu', e);
util.render('columnActions', 'data-table-menu');
},
@@ -1204,6 +712,11 @@ my.DataTable = Backbone.View.extend({
util.position('data-table-menu', e);
util.render('rowActions', 'data-table-menu');
},
+
+ onRootHeaderClick: function(e) {
+ util.position('data-table-menu', e);
+ util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields});
+ },
onMenuClick: function(e) {
var self = this;
@@ -1211,6 +724,10 @@ my.DataTable = Backbone.View.extend({
var actions = {
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
transform: function() { self.showTransformDialog('transform') },
+ sortAsc: function() { self.setColumnSort('asc') },
+ sortDesc: function() { self.setColumnSort('desc') },
+ hideColumn: function() { self.hideColumn() },
+ showColumn: function() { self.showColumn(e) },
// TODO: Delete or re-implement ...
csv: function() { window.location.href = app.csvUrl },
json: function() { window.location.href = "_rewrite/api/json" },
@@ -1233,10 +750,10 @@ my.DataTable = Backbone.View.extend({
});
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)
})
}
}
@@ -1263,7 +780,7 @@ my.DataTable = Backbone.View.extend({
showTransformDialog: function() {
var $el = $('.dialog-content');
util.show('dialog');
- var view = new my.DataTransform({
+ var view = new recline.View.DataTransform({
});
view.render();
$el.empty();
@@ -1274,25 +791,47 @@ my.DataTable = Backbone.View.extend({
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
},
+ setColumnSort: function(order) {
+ var sort = [{}];
+ sort[0][this.state.currentColumn] = {order: order};
+ this.model.query({sort: sort});
+ },
+
+ hideColumn: function() {
+ this.hiddenFields.push(this.state.currentColumn);
+ this.render();
+ },
+
+ showColumn: function(e) {
+ this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
+ this.render();
+ },
// ======================================================
- // Core Templating
+ // #### Templating
template: ' \
');
self.el.find('tbody').append(tr);
- var newView = new my.DataTableRow({
+ var newView = new my.DataGridRow({
model: doc,
el: tr,
- headers: self.model.get('headers')
- });
+ fields: self.fields,
+ },
+ self.options
+ );
newView.render();
});
+ this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
return this;
}
});
-// DataTableRow View for rendering an individual document.
+// ## DataGridRow 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) {
+//
+// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
+//
+// Additional options can be passed in a second hash argument. Options:
+//
+// * cellRenderer: function to render cells. Signature: function(value,
+// field, doc) where value is the value of this cell, field is
+// corresponding field object and document is the document object. Note
+// that implementing functions can ignore arguments (e.g.
+// function(value) would be a valid cellRenderer function).
+//
+// Example:
+//
+//
+// var row = new DataGridRow({
+// model: dataset-document,
+// el: dom-element,
+// fields: mydatasets.fields // a FieldList object
+// }, {
+// cellRenderer: my-cell-renderer-function
+// }
+// );
+//
\
+(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 DataGrid with id 'grid'. Example:
+//
+//
+// var views = [
+// {
+// id: 'grid', // used for routing
+// label: 'Grid', // used for view switcher
+// view: new recline.View.DataGrid({
+// model: dataset
+// })
+// },
+// {
+// id: 'graph',
+// label: 'Graph',
+// view: new recline.View.FlotGraph({
+// model: dataset
+// })
+// }
+// ];
+//
+//
+// **config**: Config options like:
+//
+// * 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: ' \
+
+ my.GDoc = Backbone.Model.extend({
+ sync: function(method, model, options) {
+ var self = this;
+ if (method === "read") {
+ var dfd = $.Deferred();
+ var dataset = model;
- redraw: function() {
- // There appear to be issues generating a Flot graph if either:
+ $.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);
+ })
+ return dfd.promise(); }
+ },
- // * 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'
+ query: function(dataset, queryObj) {
+ var dfd = $.Deferred();
+ var fields = _.pluck(dataset.fields.toJSON(), 'id');
+
+ // zip the fields with the data rows to produce js objs
+ // TODO: factor this out as a common method with other backends
+ var objs = _.map(dataset._dataCache, function (d) {
+ var obj = {};
+ _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
+ return obj;
+ });
+ dfd.resolve(objs);
+ return dfd;
+ },
+ gdocsToJavascript: function(gdocsSpreadsheet) {
+ /*
+ :options: (optional) optional argument dictionary:
+ 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: field and data).
+
+ Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
+ */
+ var options = {};
+ if (arguments.length > 1) {
+ options = arguments[1];
+ }
+ var results = {
+ 'field': [],
+ 'data': []
};
- 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();
- },
+ // default is no special info on type of columns
+ var colTypes = {};
+ if (options.colTypes) {
+ colTypes = options.colTypes;
+ }
+ // either extract column headings from spreadsheet directly, or used supplied ones
+ if (options.columnsToUse) {
+ // columns set to subset supplied
+ results.field = options.columnsToUse;
+ } else {
+ // set columns to use to be all available
+ if (gdocsSpreadsheet.feed.entry.length > 0) {
+ for (var k in gdocsSpreadsheet.feed.entry[0]) {
+ if (k.substr(0, 3) == 'gsx') {
+ var col = k.substr(4)
+ results.field.push(col);
+ }
+ }
+ }
+ }
- 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;
+ // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])
+ var rep = /^([\d\.\-]+)\%$/;
+ $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
+ var row = [];
+ for (var k in results.field) {
+ var col = results.field[k];
+ var _keyname = 'gsx$' + col;
+ var value = entry[_keyname]['$t'];
+ // if labelled as % and value contains %, convert
+ if (colTypes[col] == 'percent') {
+ if (rep.test(value)) {
+ var value2 = rep.exec(value);
+ var value3 = parseFloat(value2);
+ value = value3 / 100;
+ }
}
- points.push([x, y]);
- });
- series.push({data: points, label: field});
+ row.push(value);
+ }
+ results.data.push(row);
});
+ return results;
}
- return series;
- },
+ });
+ recline.Model.backends['gdocs'] = new my.GDoc();
- // 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;
+}(jQuery, this.recline.Backend));
- 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;
- },
+this.recline = this.recline || {};
+this.recline.Backend = this.recline.Backend || {};
- // Public: Removes a series list item from the editor.
+(function($, my) {
+ // ## Memory Backend - uses in-memory data
//
- // 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));
+ // To use it you should provide in your constructor data:
+ //
+ // * metadata (including fields array)
+ // * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique.
+ //
+ // Example:
+ //
+ //