Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock 2012-02-16 18:31:40 +00:00
commit ae7d830eb6
9 changed files with 983 additions and 899 deletions

View File

@ -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

View File

@ -28,6 +28,10 @@
<script type="text/javascript" src="../src/model.js"></script>
<script type="text/javascript" src="../src/backend.js"></script>
<script type="text/javascript" src="../src/view.js"></script>
<script type="text/javascript" src="../src/view-data-explorer.js"></script>
<script type="text/javascript" src="../src/view-data-table.js"></script>
<script type="text/javascript" src="../src/view-flot-graph.js"></script>
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</head>
<body>

View File

@ -58,7 +58,7 @@
<div class="content">
<div class="page-header">
<h1>
Recline Data Explorer <small>Data explorer and refinery all in pure Javascript</small>
Recline Data Explorer <small>Data explorer library and app all in pure Javascript</small>
</h1>
</div>
<h4>Recline combines a data grid, Google Refine-style data transforms
@ -68,31 +68,81 @@
<h2 id="features">Main Features</h2>
<ul>
<li>Open-source (and heavy reuser of existing open-source libraries like
Backbone)</li>
<li>Pure javascript (no Flash) and designed for integration -- so it is
easy to embed in other sites and applications</li>
<li>View and edit your data in a clean grid interface</li>
<li>View and edit your data in a clean grid / table interface</li>
<li>Bulk update/clean your data using an easy scripting UI</li>
<li>Easily extensible with new Backends so you can connect to your
database or storage layer</li>
<li>Visualize data</li>
<li>Pure javascript (no Flash) and designed for integration -- so it is
easy to embed in other sites and applications</li>
<li>Open-source</li>
<li>Built on <a
href="http://documentcloud.github.com/backbone/">Backbone</a> - so
robust design and extremely easy to exend</li>
<li>Properly designed model with clean separation of data and presentation</li>
<li>Componentized design means you use only what you need</li>
</ul>
<h2 id="demo">Demo</h2>
<ul>
<li><a href="demo/index.html">Demo</a></li>
</ul>
<p><a href="demo/index.html" class="btn">Demo &raquo;</a></p>
<h2 id="downloads">Downloads & Dependencies <small>(Right-click, and use 'Save As')</small></h2>
<p><a href="recline.js" class="btn">Development Version (v0.2)</a></p>
<p><a href="recline.js" class="btn">Recline Current Version (v0.2) &raquo;</a></p>
<h3>Dependencies</h3>
<p>Javascript Libraries:</p>
<ul>
<li>Jquery >= 1.6</li>
<li>Underscore and Backbone (0.5.1)</li>
<li>JQuery Mustache</li>
<li>(Optional) JQuery Flot - for graphing</li>
</ul>
<p><strong>CSS</strong>: 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: <a href="https://github.com/okfn/recline/tree/master/css">https://github.com/okfn/recline/tree/master/css</a>.</p>
<h2 id="using">Using It</h2>
<p>Check out the the <a href="demo/">Demo</a> and view source. The
javascript you want for actual integration is in: <a
href="demo/js/app.js">app.js</a>.</p>
<pre>
// Note: you should have included the relevant JS libraries (and CSS)
// See above for dependencies
<h2 id="docs">Docs</h2>
// 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();
</pre>
<p>More details and examples: see docs below and the <a
href="demo/">Demo</a> (hit view source). The javascript you want for
actual integration is in: <a href="demo/js/app.js">app.js</a>.</p>
<h2 id="docs">Documentation</h2>
<p>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):</p>
<ul>
<li>Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.</li>
<li>Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).</li>
<li>Backend: provides a way to get data from a specific 'Backend' data source. They provide methods for loading and saving Datasets and individuals Documents as well as for bulk loading via a query API and doing bulk transforms on the backend</li>
</ul>
<p>There are then various Views (you can easily write your own). Each view holds a pointer to a Dataset:</p>
<ul>
<li>DataExplorer: the parent view which manages the overall app and sets up sub views.</li>
<li>DataTable: the data grid / table view.</li>
<li>FlotGraph: a simple graphing view using <a href="http://code.google.com/p/flot/">Flot</a>.</li>
</ul>
<h3>Source</h3>
<ul>
<li><a href="docs/model.html">Models</a></li>
<li><a href="docs/backend.html">Backends</a></li>
@ -117,7 +167,7 @@
href="https://github.com/maxogden/recline">Max's original recline
codebase</a> combining some portions of the <a
href="http://github.com/okfn/dataexplorer">original Data Explorer</a>.
However, it has been rapidly rewritten from the ground up using Backbone.</p>
However, it has been rewritten from the ground up using Backbone.</p>
</div>
</div>

View File

@ -11,7 +11,7 @@ var util = function() {
, rowActions: '<li><a data-action="deleteRow" class="menuAction write-op" href="JavaScript:void(0);">Delete this row</a></li>'
, rootActions: ' \
{{#columns}} \
<li><a data-action="showColumn" data-column="{{.}}" class="menuAction" href="JavaScript:void(0);">Add column: {{.}}</a></li> \
<li><a data-action="showColumn" data-column="{{.}}" class="menuAction" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
{{/columns}}'
, cellEditor: ' \
<div class="menu-container data-table-cell-editor"> \

171
src/view-data-explorer.js Normal file
View File

@ -0,0 +1,171 @@
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
// Views module following classic module pattern
(function($, my) {
// The primary view for the entire application.
//
// It should be initialized with a recline.Model.Dataset object and an existing
// dom element to attach to (the existing DOM element is important for
// rendering of FlotGraph subview).
//
// To pass in configuration options use the config key in initialization hash
// e.g.
//
// var explorer = new DataExplorer({
// config: {...}
// })
//
// Config options:
//
// * displayCount: how many documents to display initially (default: 10)
// * readOnly: true/false (default: false) value indicating whether to
// operate in read-only mode (hiding all editing options).
//
// All other views as contained in this one.
my.DataExplorer = Backbone.View.extend({
template: ' \
<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}}" title="Edit and hit enter to change the number of rows displayed" /> 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; ">&nbsp;</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 = _.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,
config: this.config
})
, 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();
}
});
}
});
})(jQuery, recline.View);

298
src/view-data-table.js Normal file
View File

@ -0,0 +1,298 @@
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
// Views module following classic module pattern
(function($, my) {
// 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 recline.View.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: ' \
<div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
<ul class="data-table-menu"></ul> \
<table class="data-table" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}} \
<th class="column-header"> \
<div class="column-header-title"> \
<a class="root-header-menu"></a> \
<span class="column-header-name"></span> \
</div> \
</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 = ( 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 = $('<tr />');
self.el.find('tbody').append(tr);
var newView = new my.DataTableRow({
model: doc,
el: tr,
headers: self.headers,
});
newView.render();
});
this.el.toggleClass('no-hidden', (self.hiddenHeaders.length == 0));
return this;
}
});
// DataTableRow View for rendering an individual document.
//
// Since we want this to update in place it is up to creator to provider the element to attach to.
// In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.
my.DataTableRow = Backbone.View.extend({
initialize: function(options) {
_.bindAll(this, 'render');
this._headers = options.headers;
this.el = $(this.el);
this.model.bind('change', this.render);
},
template: ' \
<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">&nbsp;</a> \
<div class="data-table-cell-value">{{value}}</div> \
</div> \
</td> \
{{/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");
}
});
})(jQuery, recline.View);

232
src/view-flot-graph.js Normal file
View File

@ -0,0 +1,232 @@
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
// Views module following classic module pattern
(function($, my) {
// 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: ' \
<div class="editor"> \
<div class="editor-info editor-hide-info"> \
<h3 class="action-toggle-help">Help &raquo;</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');
// 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(' [<a href="#remove" class="action-remove-series">Remove</a>]');
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);

View File

@ -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: ' \
<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">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton button">Cancel</button> \
</div> \
',
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: ' \
<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">&nbsp;&nbsp;Update All&nbsp;&nbsp;</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);
// 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);

View File

@ -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: ' \
<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}}" title="Edit and hit enter to change the number of rows displayed" /> 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; ">&nbsp;</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 = _.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: ' \
<div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
<ul class="data-table-menu"></ul> \
<table class="data-table" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}} \
<th class="column-header"> \
<div class="column-header-title"> \
<a class="root-header-menu"></a> \
<span class="column-header-name"></span> \
</div> \
</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 = ( 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 = $('<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: ' \
<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">&nbsp;</a> \
<div class="data-table-cell-value">{{value}}</div> \
</div> \
</td> \
{{/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: ' \
<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">&nbsp;&nbsp;Update All&nbsp;&nbsp;</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);
// 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: ' \
<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">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton button">Cancel</button> \
</div> \
',
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: ' \
<div class="editor"> \
<div class="editor-info editor-hide-info"> \
<h3 class="action-toggle-help">Help &raquo;</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');
// 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(' [<a href="#remove" class="action-remove-series">Remove</a>]');
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);