diff --git a/css/data-explorer.css b/css/data-explorer.css index 7ed4e66a..77aacfd6 100644 --- a/css/data-explorer.css +++ b/css/data-explorer.css @@ -515,12 +515,12 @@ td.expression-preview-value { * Read-only mode *********************************************************/ -/*.read-only .data-table tr td:first-child, -.read-only .data-table tr th:first-child +.read-only .no-hidden .data-table tr td:first-child, +.read-only .no-hidden .data-table tr th:first-child { display: none; } -*/ + .read-only .write-op, .read-only a.data-table-cell-edit diff --git a/demo/index.html b/demo/index.html index b385ba4b..48a0d5e0 100644 --- a/demo/index.html +++ b/demo/index.html @@ -28,6 +28,10 @@ + + + + diff --git a/index.html b/index.html index f2bc0da4..47a006d4 100644 --- a/index.html +++ b/index.html @@ -58,7 +58,7 @@

Recline combines a data grid, Google Refine-style data transforms @@ -68,31 +68,81 @@

Main Features

Demo

- +

Demo »

Downloads & Dependencies (Right-click, and use 'Save As')

-

Development Version (v0.2)

+

Recline Current Version (v0.2) »

+

Dependencies

+

Javascript Libraries:

+ +

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

Using It

-

Check out the the Demo and view source. The - javascript you want for actual integration is in: app.js.

+
+// Note: you should have included the relevant JS libraries (and CSS)
+// See above for dependencies
 
-    

Docs

+// Dataset is a Backbone model +var dataset = recline.Model.Dataset({ + id: 'my-id' + backend: { + // backend ID so we can look backend up in the registry (see below) + type: 'memory' + // other backend config (e.g. API url with which to communicate) + // this will usually be backend specific + ... + } +}); +// DataExplorer is a Backbone View +var explorer = recline.View.DataExplorer({ + model: dataset, + // you can specify any element to bind to in the dom + el: $('.data-explorer-here') +}); +// Start Backbone routing (if you want routing support) +Backbone.history.start(); +
+

More details and examples: see docs below and the Demo (hit view source). The javascript you want for + actual integration is in: app.js.

+ +

Documentation

+

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

+ +

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

+ + +

Source

diff --git a/src/util.js b/src/util.js index d93953b1..35332195 100644 --- a/src/util.js +++ b/src/util.js @@ -11,7 +11,7 @@ var util = function() { , rowActions: '
  • Delete this row
  • ' , rootActions: ' \ {{#columns}} \ -
  • Add column: {{.}}
  • \ +
  • Show column: {{.}}
  • \ {{/columns}}' , cellEditor: ' \ \ +', + + 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'); + } +}); + +})(jQuery, recline.View); + diff --git a/src/view-transform-dialog.js b/src/view-transform-dialog.js new file mode 100644 index 00000000..7d80cd4f --- /dev/null +++ b/src/view-transform-dialog.js @@ -0,0 +1,206 @@ +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +// Views module following classic module pattern +(function($, my) { + +// View (Dialog) for doing data transformations on whole dataset. +my.DataTransform = Backbone.View.extend({ + className: 'transform-view', + template: ' \ +
    \ + Recursive transform on all rows \ +
    \ +
    \ +
    \ +

    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); + } +}); + + +// View (Dialog) for doing data transformations (on columns of data). +my.ColumnTransform = Backbone.View.extend({ + className: 'transform-column-view', + template: ' \ +
    \ + Functional transform on column {{name}} \ +
    \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    \ + Expression \ +
    \ +
    \ + \ +
    \ +
    \ + No syntax error. \ +
    \ +
    \ + Preview \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ + \ + ', + + 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); + // Put in the basic (identity) transform script + // TODO: put this into the template? + 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) { + my.notify("Error with function! " + editFunc.errorMessage); + return; + } + util.hide('dialog'); + my.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(); + }); + // TODO: notify about failed docs? + var toUpdate = costco.mapDocs(docs, editFunc).edited; + var totalToUpdate = toUpdate.length; + function onCompletedUpdate() { + totalToUpdate += -1; + if (totalToUpdate === 0) { + my.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(); + } + } + // TODO: Very inefficient as we search through all docs every time! + _.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; + // if you don't setTimeout it won't grab the latest character if you call e.target.value + 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); + } +}); + +})(jQuery, recline.View); diff --git a/src/view.js b/src/view.js index 7f0acc51..5e79a62a 100644 --- a/src/view.js +++ b/src/view.js @@ -1,9 +1,8 @@ this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; // Views module following classic module pattern -recline.View = function($) { - -var my = {}; +(function($, my) { // Parse a URL query string (?xyz=abc...) into a dictionary. function parseQueryString(q) { @@ -64,881 +63,5 @@ my.clearNotifications = function() { $notifications.remove(); } -// 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: ' \ -
    \ -
    \ - \ -
    \ - \ - \ -
    \ -
    \ - \ - \ -
    \ - ', - - events: { - 'submit form.display-count': 'onDisplayCountUpdate' - }, - - 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(); - } - // 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.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() - .done(function(dataset) { - self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - self.query(); - }) - .fail(function(error) { - my.notify(error.message, {category: 'error', persist: true}); - }); - }, - - query: function() { - this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); - var queryObj = { - size: this.config.displayCount - }; - my.notify('Loading data', {loader: true}); - this.model.query(queryObj) - .done(function() { - my.clearNotifications(); - my.notify('Data loaded', {category: 'success'}); - }) - .fail(function(error) { - my.clearNotifications(); - my.notify(error.message, {category: 'error', persist: true}); - }); - }, - - onDisplayCountUpdate: function(e) { - e.preventDefault(); - this.query(); - }, - - 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); - // 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(); - }); - }, - - 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(); - } - }); - } -}); - -// 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() { - 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 = {}; - this.hiddenHeaders = []; - }, - - 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'); - // 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 - - 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'); - }, - - onRootHeaderClick: function(e) { - util.position('data-table-menu', e); - util.render('rootActions', 'data-table-menu', {'columns': this.hiddenHeaders}); - }, - - onMenuClick: function(e) { - var self = this; - e.preventDefault(); - 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" }, - urlImport: function() { showDialog('urlImport') }, - pasteImport: function() { showDialog('pasteImport') }, - uploadImport: function() { showDialog('uploadImport') }, - // END TODO - deleteColumn: function() { - var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents."; - // TODO: - 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) { - // 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); - my.notify("Row deleted successfully"); - }) - .fail(function(err) { - my.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' }); - }, - - setColumnSort: function(order) { - var query = _.extend(this.model.queryState, {sort: [[this.state.currentColumn, order]]}); - this.model.query(query); - }, - - hideColumn: function() { - this.hiddenHeaders.push(this.state.currentColumn); - this.render(); - }, - - showColumn: function(e) { - this.hiddenHeaders = _.without(this.hiddenHeaders, $(e.target).data('column')); - this.render(); - }, - - // ====================================================== - // Core Templating - template: ' \ - \ - \ - \ - \ - \ - {{#notEmpty}} \ - \ - {{/notEmpty}} \ - {{#headers}} \ - \ - {{/headers}} \ - \ - \ - \ -
    \ -
    \ - \ - \ -
    \ -
    \ -
    \ - \ - {{.}} \ -
    \ - \ -
    \ - ', - - toTemplateJSON: function() { - var modelData = this.model.toJSON() - modelData.notEmpty = ( this.headers.length > 0 ) - modelData.headers = this.headers; - return modelData; - }, - render: function() { - var self = this; - this.headers = _.filter(this.model.get('headers'), function(header) { - return _.indexOf(self.hiddenHeaders, header) == -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, - headers: self.headers, - }); - newView.render(); - }); - $(".root-header-menu").toggle((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: ' \ - \ - {{#cells}} \ - \ -
    \ -   \ -
    {{value}}
    \ -
    \ - \ - {{/cells}} \ - ', - events: { - 'click .data-table-cell-edit': 'onEditClick', - // cell editor - '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; - }, - - // ====================================================== - // Cell Editor - - 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); - my.notify("Updating row...", {loader: true}); - this.model.save().then(function(response) { - my.notify("Row updated successfully", {category: 'success'}); - }) - .fail(function() { - my.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"); - } -}); - - -// View (Dialog) for doing data transformations (on columns of data). -my.ColumnTransform = Backbone.View.extend({ - className: 'transform-column-view', - template: ' \ -
    \ - Functional transform on column {{name}} \ -
    \ -
    \ -
    \ - \ - \ - \ - \ - \ - \ -
    \ -
    \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
    \ - Expression \ -
    \ -
    \ - \ -
    \ -
    \ - No syntax error. \ -
    \ -
    \ - Preview \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ - \ - ', - - 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); - // Put in the basic (identity) transform script - // TODO: put this into the template? - 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) { - my.notify("Error with function! " + editFunc.errorMessage); - return; - } - util.hide('dialog'); - my.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(); - }); - // TODO: notify about failed docs? - var toUpdate = costco.mapDocs(docs, editFunc).edited; - var totalToUpdate = toUpdate.length; - function onCompletedUpdate() { - totalToUpdate += -1; - if (totalToUpdate === 0) { - my.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(); - } - } - // TODO: Very inefficient as we search through all docs every time! - _.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; - // if you don't setTimeout it won't grab the latest character if you call e.target.value - 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); - } -}); - -// View (Dialog) for doing data transformations on whole dataset. -my.DataTransform = Backbone.View.extend({ - className: 'transform-view', - template: ' \ -
    \ - Recursive transform on all rows \ -
    \ -
    \ -
    \ -

    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);