[merge] from upstream master
This commit is contained in:
@@ -60,6 +60,7 @@
|
|||||||
<script type="text/javascript" src="js/app.js"></script>
|
<script type="text/javascript" src="js/app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="recline-app">
|
||||||
<div class="navbar navbar-fixed-top">
|
<div class="navbar navbar-fixed-top">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
<div class="data-explorer-here"></div>
|
<div class="data-explorer-here"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
177
app/js/app.js
177
app/js/app.js
@@ -1,7 +1,8 @@
|
|||||||
$(function() {
|
jQuery(function($) {
|
||||||
var qs = recline.View.parseQueryString(window.location.search);
|
var qs = recline.View.parseQueryString(window.location.search);
|
||||||
|
var dataest = null;
|
||||||
if (qs.url) {
|
if (qs.url) {
|
||||||
var dataset = new recline.Model.Dataset({
|
dataset = new recline.Model.Dataset({
|
||||||
id: 'my-dataset',
|
id: 'my-dataset',
|
||||||
url: qs.url,
|
url: qs.url,
|
||||||
webstore_url: qs.url
|
webstore_url: qs.url
|
||||||
@@ -12,67 +13,90 @@ $(function() {
|
|||||||
dataset = localDataset();
|
dataset = localDataset();
|
||||||
}
|
}
|
||||||
|
|
||||||
createExplorer(dataset);
|
var app = new ExplorerApp({
|
||||||
|
model: dataset,
|
||||||
|
el: $('.recline-app')
|
||||||
|
})
|
||||||
Backbone.history.start();
|
Backbone.history.start();
|
||||||
|
|
||||||
// setup the loader menu in top bar
|
|
||||||
setupLoader(createExplorer);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// make Explorer creation / initialization in a function so we can call it
|
var ExplorerApp = Backbone.View.extend({
|
||||||
// again and again
|
events: {
|
||||||
function createExplorer(dataset) {
|
'submit form.js-import-url': '_onImportURL',
|
||||||
// remove existing data explorer view
|
'submit .js-import-dialog-file form': '_onImportFile'
|
||||||
var reload = false;
|
},
|
||||||
if (window.dataExplorer) {
|
|
||||||
window.dataExplorer.remove();
|
|
||||||
reload = true;
|
|
||||||
}
|
|
||||||
window.dataExplorer = null;
|
|
||||||
var $el = $('<div />');
|
|
||||||
$el.appendTo($('.data-explorer-here'));
|
|
||||||
var views = standardViews(dataset);
|
|
||||||
window.dataExplorer = new recline.View.DataExplorer({
|
|
||||||
el: $el
|
|
||||||
, model: dataset
|
|
||||||
, views: views
|
|
||||||
});
|
|
||||||
// HACK (a bit). Issue is that Backbone will not trigger the route
|
|
||||||
// if you are already at that location so we have to make sure we genuinely switch
|
|
||||||
if (reload) {
|
|
||||||
window.dataExplorer.router.navigate('graph');
|
|
||||||
window.dataExplorer.router.navigate('', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convenience function
|
initialize: function() {
|
||||||
function standardViews(dataset) {
|
this.explorer = null;
|
||||||
var views = [
|
this.explorerDiv = $('.data-explorer-here');
|
||||||
{
|
this.createExplorer(this.model);
|
||||||
id: 'grid',
|
},
|
||||||
label: 'Grid',
|
|
||||||
view: new recline.View.Grid({
|
// make Explorer creation / initialization in a function so we can call it
|
||||||
model: dataset
|
// again and again
|
||||||
})
|
createExplorer: function(dataset) {
|
||||||
},
|
// remove existing data explorer view
|
||||||
{
|
var reload = false;
|
||||||
id: 'graph',
|
if (this.dataExplorer) {
|
||||||
label: 'Graph',
|
this.dataExplorer.remove();
|
||||||
view: new recline.View.Graph({
|
reload = true;
|
||||||
model: dataset
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'map',
|
|
||||||
label: 'Map',
|
|
||||||
view: new recline.View.Map({
|
|
||||||
model: dataset
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
this.dataExplorer = null;
|
||||||
|
var $el = $('<div />');
|
||||||
|
$el.appendTo(this.explorerDiv);
|
||||||
|
this.dataExplorer = new recline.View.DataExplorer({
|
||||||
|
el: $el
|
||||||
|
, model: dataset
|
||||||
|
});
|
||||||
|
// HACK (a bit). Issue is that Backbone will not trigger the route
|
||||||
|
// if you are already at that location so we have to make sure we genuinely switch
|
||||||
|
if (reload) {
|
||||||
|
this.dataExplorer.router.navigate('graph');
|
||||||
|
this.dataExplorer.router.navigate('', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
];
|
// setup the loader menu in top bar
|
||||||
return views;
|
setupLoader: function(callback) {
|
||||||
}
|
// pre-populate webstore load form with an example url
|
||||||
|
var demoUrl = 'http://thedatahub.org/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013';
|
||||||
|
$('form.js-import-url input[name="source"]').val(demoUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onImportURL: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('.modal.js-import-dialog-url').modal('hide');
|
||||||
|
var $form = $(e.target);
|
||||||
|
var source = $form.find('input[name="source"]').val();
|
||||||
|
var type = $form.find('select[name="backend_type"]').val();
|
||||||
|
var dataset = new recline.Model.Dataset({
|
||||||
|
id: 'my-dataset',
|
||||||
|
url: source,
|
||||||
|
webstore_url: source
|
||||||
|
},
|
||||||
|
type
|
||||||
|
);
|
||||||
|
this.createExplorer(dataset);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onImportFile: function(e) {
|
||||||
|
var self = this;
|
||||||
|
e.preventDefault();
|
||||||
|
var $form = $(e.target);
|
||||||
|
$('.modal.js-import-dialog-file').modal('hide');
|
||||||
|
var $file = $form.find('input[type="file"]')[0];
|
||||||
|
var file = $file.files[0];
|
||||||
|
var options = {
|
||||||
|
separator : $form.find('input[name="separator"]').val(),
|
||||||
|
encoding : $form.find('input[name="encoding"]').val()
|
||||||
|
};
|
||||||
|
recline.Backend.loadFromCSVFile(file, function(dataset) {
|
||||||
|
self.createExplorer(dataset)
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// provide a demonstration in memory dataset
|
// provide a demonstration in memory dataset
|
||||||
function localDataset() {
|
function localDataset() {
|
||||||
@@ -83,7 +107,7 @@ function localDataset() {
|
|||||||
, name: '1-my-test-dataset'
|
, name: '1-my-test-dataset'
|
||||||
, id: datasetId
|
, id: datasetId
|
||||||
},
|
},
|
||||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}],
|
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}],
|
||||||
documents: [
|
documents: [
|
||||||
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}
|
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}
|
||||||
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60}
|
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60}
|
||||||
@@ -100,40 +124,3 @@ fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'l
|
|||||||
return dataset;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup the loader menu in top bar
|
|
||||||
function setupLoader(callback) {
|
|
||||||
// pre-populate webstore load form with an example url
|
|
||||||
var demoUrl = 'http://thedatahub.org/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013';
|
|
||||||
$('form.js-import-url input[name="source"]').val(demoUrl);
|
|
||||||
$('form.js-import-url').submit(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('.modal.js-import-dialog-url').modal('hide');
|
|
||||||
var $form = $(e.target);
|
|
||||||
var source = $form.find('input[name="source"]').val();
|
|
||||||
var type = $form.find('select[name="backend_type"]').val();
|
|
||||||
var dataset = new recline.Model.Dataset({
|
|
||||||
id: 'my-dataset',
|
|
||||||
url: source,
|
|
||||||
webstore_url: source
|
|
||||||
},
|
|
||||||
type
|
|
||||||
);
|
|
||||||
callback(dataset);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.js-import-dialog-file form').submit(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $form = $(e.target);
|
|
||||||
$('.modal.js-import-dialog-file').modal('hide');
|
|
||||||
var $file = $form.find('input[type="file"]')[0];
|
|
||||||
var file = $file.files[0];
|
|
||||||
var options = {
|
|
||||||
separator : $form.find('input[name="separator"]').val(),
|
|
||||||
encoding : $form.find('input[name="encoding"]').val()
|
|
||||||
};
|
|
||||||
recline.Backend.loadFromCSVFile(file, function(dataset) {
|
|
||||||
callback(dataset)
|
|
||||||
}, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
.recline-graph-container .graph {
|
.recline-graph .graph {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
margin-right: 200px;
|
margin-right: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .legend table {
|
.recline-graph .legend table {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .legend td {
|
.recline-graph .legend td {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
line-height: 13px;
|
line-height: 13px;
|
||||||
}
|
}
|
||||||
@@ -17,34 +17,34 @@
|
|||||||
* Editor
|
* Editor
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
|
|
||||||
.recline-graph-container .editor {
|
.recline-graph .editor {
|
||||||
float: right;
|
float: right;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor-info {
|
.recline-graph .editor-info {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor-info {
|
.recline-graph .editor-info {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor form {
|
.recline-graph .editor form {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor select {
|
.recline-graph .editor select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor-info {
|
.recline-graph .editor-info {
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recline-graph-container .editor-hide-info p {
|
.recline-graph .editor-hide-info p {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
css/grid.css
20
css/grid.css
@@ -23,6 +23,10 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recline-grid td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
.recline-grid tr td:first-child, .recline-grid tr th:first-child {
|
.recline-grid tr td:first-child, .recline-grid tr th:first-child {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
@@ -63,10 +67,6 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-only a.row-header-menu {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.data-table-cell-content {
|
div.data-table-cell-content {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: #222;
|
color: #222;
|
||||||
@@ -301,15 +301,19 @@ td.expression-preview-value {
|
|||||||
* Read-only mode
|
* Read-only mode
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
|
|
||||||
.read-only .no-hidden .recline-grid tr td:first-child,
|
.recline-read-only .no-hidden .recline-grid tr td:first-child,
|
||||||
.read-only .no-hidden .recline-grid tr th:first-child
|
.recline-read-only .no-hidden .recline-grid tr th:first-child
|
||||||
{
|
{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-only .recline-grid .write-op,
|
.recline-read-only .recline-grid .write-op,
|
||||||
.read-only .recline-grid a.data-table-cell-edit
|
.recline-read-only .recline-grid a.data-table-cell-edit
|
||||||
{
|
{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recline-read-only a.row-header-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
10
css/map.css
10
css/map.css
@@ -1,4 +1,4 @@
|
|||||||
.data-map-container .map {
|
.recline-map .map {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,22 +6,22 @@
|
|||||||
* Editor
|
* Editor
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
|
|
||||||
.data-map-container .editor {
|
.recline-map .editor {
|
||||||
float: right;
|
float: right;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-map-container .editor form {
|
.recline-map .editor form {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-map-container .editor select {
|
.recline-map .editor select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-map-container .editor .editor-options {
|
.recline-map .editor .editor-options {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border-top: 1px solid gray;
|
border-top: 1px solid gray;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
|||||||
10
index.html
10
index.html
@@ -182,13 +182,9 @@ Backbone.history.start();
|
|||||||
|
|
||||||
<h4>Creating a Dataset Explicitly with a Backend</h4>
|
<h4>Creating a Dataset Explicitly with a Backend</h4>
|
||||||
<pre>
|
<pre>
|
||||||
// Backend can be an instance or string id for a backend in the
|
// Connect to ElasticSearch index/type as our data source
|
||||||
// recline.Model.backends registry
|
// There are many other backends you can use (and you can write your own)
|
||||||
var backend = 'elasticsearch'
|
var backend = new recline.Backend.ElasticSearch();
|
||||||
// alternatively you can create explicitly
|
|
||||||
// var backend = new recline.Backend.ElasticSearch();
|
|
||||||
// or even from your own backend ...
|
|
||||||
// var backend = new myModule.Backend();
|
|
||||||
|
|
||||||
// Dataset is a Backbone model so the first hash become model attributes
|
// Dataset is a Backbone model so the first hash become model attributes
|
||||||
var dataset = recline.Model.Dataset({
|
var dataset = recline.Model.Dataset({
|
||||||
|
|||||||
@@ -17,10 +17,20 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// ## recline.Backend.Base
|
// ## recline.Backend.Base
|
||||||
//
|
//
|
||||||
// Base class for backends providing a template and convenience functions.
|
// Base class for backends providing a template and convenience functions.
|
||||||
// You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
|
// You do not have to inherit from this class but even when not it does
|
||||||
|
// provide guidance on the functions you must implement.
|
||||||
//
|
//
|
||||||
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
|
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
|
||||||
my.Base = Backbone.Model.extend({
|
my.Base = Backbone.Model.extend({
|
||||||
|
// ### __type__
|
||||||
|
//
|
||||||
|
// 'type' of this backend. This should be either the class path for this
|
||||||
|
// object as a string (e.g. recline.Backend.Memory) or for Backends within
|
||||||
|
// recline.Backend module it may be their class name.
|
||||||
|
//
|
||||||
|
// This value is used as an identifier for this backend when initializing
|
||||||
|
// backends (see recline.Model.Dataset.initialize).
|
||||||
|
__type__: 'base',
|
||||||
|
|
||||||
// ### sync
|
// ### sync
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
//
|
//
|
||||||
// Note that this is a **read-only** backend.
|
// Note that this is a **read-only** backend.
|
||||||
my.DataProxy = my.Base.extend({
|
my.DataProxy = my.Base.extend({
|
||||||
|
__type__: 'dataproxy',
|
||||||
defaults: {
|
defaults: {
|
||||||
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
||||||
},
|
},
|
||||||
@@ -71,7 +72,5 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
recline.Model.backends['dataproxy'] = new my.DataProxy();
|
|
||||||
|
|
||||||
|
|
||||||
}(jQuery, this.recline.Backend));
|
}(jQuery, this.recline.Backend));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
//
|
//
|
||||||
// <pre>http://localhost:9200/twitter/tweet</pre>
|
// <pre>http://localhost:9200/twitter/tweet</pre>
|
||||||
my.ElasticSearch = my.Base.extend({
|
my.ElasticSearch = my.Base.extend({
|
||||||
|
__type__: 'elasticsearch',
|
||||||
_getESUrl: function(dataset) {
|
_getESUrl: function(dataset) {
|
||||||
var out = dataset.get('elasticsearch_url');
|
var out = dataset.get('elasticsearch_url');
|
||||||
if (out) return out;
|
if (out) return out;
|
||||||
@@ -115,7 +116,6 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
|
|
||||||
|
|
||||||
}(jQuery, this.recline.Backend));
|
}(jQuery, this.recline.Backend));
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// );
|
// );
|
||||||
// </pre>
|
// </pre>
|
||||||
my.GDoc = my.Base.extend({
|
my.GDoc = my.Base.extend({
|
||||||
|
__type__: 'gdoc',
|
||||||
getUrl: function(dataset) {
|
getUrl: function(dataset) {
|
||||||
var url = dataset.get('url');
|
var url = dataset.get('url');
|
||||||
if (url.indexOf('feeds/list') != -1) {
|
if (url.indexOf('feeds/list') != -1) {
|
||||||
@@ -134,7 +135,6 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
recline.Model.backends['gdocs'] = new my.GDoc();
|
|
||||||
|
|
||||||
}(jQuery, this.recline.Backend));
|
}(jQuery, this.recline.Backend));
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
if (!metadata.id) {
|
if (!metadata.id) {
|
||||||
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
|
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
|
||||||
}
|
}
|
||||||
var backend = recline.Model.backends['memory'];
|
var backend = new recline.Backend.Memory();
|
||||||
var datasetInfo = {
|
var datasetInfo = {
|
||||||
documents: data,
|
documents: data,
|
||||||
metadata: metadata
|
metadata: metadata
|
||||||
@@ -35,7 +35,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
backend.addDataset(datasetInfo);
|
backend.addDataset(datasetInfo);
|
||||||
var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
|
var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
|
||||||
dataset.fetch();
|
dataset.fetch();
|
||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
@@ -70,6 +70,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// etc ...
|
// etc ...
|
||||||
// </pre>
|
// </pre>
|
||||||
my.Memory = my.Base.extend({
|
my.Memory = my.Base.extend({
|
||||||
|
__type__: 'memory',
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
this.datasets = {};
|
this.datasets = {};
|
||||||
},
|
},
|
||||||
@@ -209,6 +210,5 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return facetResults;
|
return facetResults;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
recline.Model.backends['memory'] = new my.Memory();
|
|
||||||
|
|
||||||
}(jQuery, this.recline.Backend));
|
}(jQuery, this.recline.Backend));
|
||||||
|
|||||||
75
src/model.js
75
src/model.js
@@ -18,7 +18,7 @@ this.recline.Model = this.recline.Model || {};
|
|||||||
//
|
//
|
||||||
// @property {number} docCount: total number of documents in this dataset
|
// @property {number} docCount: total number of documents in this dataset
|
||||||
//
|
//
|
||||||
// @property {Backend} backend: the Backend (instance) for this Dataset
|
// @property {Backend} backend: the Backend (instance) for this Dataset.
|
||||||
//
|
//
|
||||||
// @property {Query} queryState: `Query` object which stores current
|
// @property {Query} queryState: `Query` object which stores current
|
||||||
// queryState. queryState may be edited by other components (e.g. a query
|
// queryState. queryState may be edited by other components (e.g. a query
|
||||||
@@ -28,14 +28,24 @@ this.recline.Model = this.recline.Model || {};
|
|||||||
// Facets.
|
// Facets.
|
||||||
my.Dataset = Backbone.Model.extend({
|
my.Dataset = Backbone.Model.extend({
|
||||||
__type__: 'Dataset',
|
__type__: 'Dataset',
|
||||||
|
|
||||||
// ### initialize
|
// ### initialize
|
||||||
//
|
//
|
||||||
// Sets up instance properties (see above)
|
// Sets up instance properties (see above)
|
||||||
|
//
|
||||||
|
// @param {Object} model: standard set of model attributes passed to Backbone models
|
||||||
|
//
|
||||||
|
// @param {Object or String} backend: Backend instance (see
|
||||||
|
// `recline.Backend.Base`) or a string specifying that instance. The
|
||||||
|
// string specifying may be a full class path e.g.
|
||||||
|
// 'recline.Backend.ElasticSearch' or a simple name e.g.
|
||||||
|
// 'elasticsearch' or 'ElasticSearch' (in this case must be a Backend in
|
||||||
|
// recline.Backend module)
|
||||||
initialize: function(model, backend) {
|
initialize: function(model, backend) {
|
||||||
_.bindAll(this, 'query');
|
_.bindAll(this, 'query');
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
if (backend && backend.constructor == String) {
|
if (typeof(backend) === 'string') {
|
||||||
this.backend = my.backends[backend];
|
this.backend = this._backendFromString(backend);
|
||||||
}
|
}
|
||||||
this.fields = new my.FieldList();
|
this.fields = new my.FieldList();
|
||||||
this.currentDocuments = new my.DocumentList();
|
this.currentDocuments = new my.DocumentList();
|
||||||
@@ -99,9 +109,68 @@ my.Dataset = Backbone.Model.extend({
|
|||||||
data.docCount = this.docCount;
|
data.docCount = this.docCount;
|
||||||
data.fields = this.fields.toJSON();
|
data.fields = this.fields.toJSON();
|
||||||
return data;
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ### _backendFromString(backendString)
|
||||||
|
//
|
||||||
|
// See backend argument to initialize for details
|
||||||
|
_backendFromString: function(backendString) {
|
||||||
|
var parts = backendString.split('.');
|
||||||
|
// walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
|
||||||
|
var current = window;
|
||||||
|
for(ii=0;ii<parts.length;ii++) {
|
||||||
|
if (!current) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current[parts[ii]];
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
return new current();
|
||||||
|
}
|
||||||
|
|
||||||
|
// alternatively we just had a simple string
|
||||||
|
var backend = null;
|
||||||
|
if (recline && recline.Backend) {
|
||||||
|
_.each(_.keys(recline.Backend), function(name) {
|
||||||
|
if (name.toLowerCase() === backendString.toLowerCase()) {
|
||||||
|
backend = new recline.Backend[name]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return backend;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ### Dataset.restore
|
||||||
|
//
|
||||||
|
// Restore a Dataset instance from a serialized state. Serialized state for a
|
||||||
|
// Dataset is an Object like:
|
||||||
|
//
|
||||||
|
// <pre>
|
||||||
|
// {
|
||||||
|
// backend: {backend type - i.e. value of dataset.backend.__type__}
|
||||||
|
// dataset: {result of dataset.toJSON()}
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
my.Dataset.restore = function(state) {
|
||||||
|
// hack-y - restoring a memory dataset does not mean much ...
|
||||||
|
var dataset = null;
|
||||||
|
if (state.backend === 'memory') {
|
||||||
|
dataset = recline.Backend.createDataset(
|
||||||
|
[{stub: 'this is a stub dataset because we do not restore memory datasets'}],
|
||||||
|
[],
|
||||||
|
state.dataset
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dataset = new recline.Model.Dataset(
|
||||||
|
state.dataset,
|
||||||
|
state.backend
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dataset;
|
||||||
|
};
|
||||||
|
|
||||||
// ## <a id="document">A Document (aka Row)</a>
|
// ## <a id="document">A Document (aka Row)</a>
|
||||||
//
|
//
|
||||||
// A single entry or row in the dataset
|
// A single entry or row in the dataset
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ this.recline.View = this.recline.View || {};
|
|||||||
my.Graph = Backbone.View.extend({
|
my.Graph = Backbone.View.extend({
|
||||||
|
|
||||||
tagName: "div",
|
tagName: "div",
|
||||||
className: "recline-graph-container",
|
className: "recline-graph",
|
||||||
|
|
||||||
template: ' \
|
template: ' \
|
||||||
<div class="editor"> \
|
<div class="editor"> \
|
||||||
@@ -275,9 +275,9 @@ my.Graph = Backbone.View.extend({
|
|||||||
createSeries: function () {
|
createSeries: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
var series = [];
|
var series = [];
|
||||||
$.each(this.state.attributes.series, function (seriesIndex, field) {
|
_.each(this.state.attributes.series, function(field) {
|
||||||
var points = [];
|
var points = [];
|
||||||
$.each(self.model.currentDocuments.models, function (index, doc) {
|
_.each(self.model.currentDocuments.models, function(doc, index) {
|
||||||
var x = doc.get(self.state.attributes.group);
|
var x = doc.get(self.state.attributes.group);
|
||||||
var y = doc.get(field);
|
var y = doc.get(field);
|
||||||
if (typeof x === 'string') {
|
if (typeof x === 'string') {
|
||||||
|
|||||||
@@ -12,21 +12,21 @@ this.recline.View = this.recline.View || {};
|
|||||||
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
|
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
|
||||||
// longitude coordinates.
|
// longitude coordinates.
|
||||||
//
|
//
|
||||||
// Initialization arguments:
|
// Initialization arguments are as standard for Dataset Views. State object may
|
||||||
//
|
// have the following (optional) configuration options:
|
||||||
// * options: initial options. They must contain a model:
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// model: {recline.Model.Dataset}
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// * config: (optional) map configuration hash (not yet used)
|
|
||||||
//
|
|
||||||
//
|
//
|
||||||
|
// <pre>
|
||||||
|
// {
|
||||||
|
// // geomField if specified will be used in preference to lat/lon
|
||||||
|
// geomField: {id of field containing geometry in the dataset}
|
||||||
|
// lonField: {id of field containing longitude in the dataset}
|
||||||
|
// latField: {id of field containing latitude in the dataset}
|
||||||
|
// }
|
||||||
|
// </pre>
|
||||||
my.Map = Backbone.View.extend({
|
my.Map = Backbone.View.extend({
|
||||||
|
|
||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
className: 'data-map-container',
|
className: 'recline-map',
|
||||||
|
|
||||||
template: ' \
|
template: ' \
|
||||||
<div class="editor"> \
|
<div class="editor"> \
|
||||||
@@ -101,14 +101,12 @@ my.Map = Backbone.View.extend({
|
|||||||
'change #editor-auto-zoom': 'onAutoZoomChange'
|
'change #editor-auto-zoom': 'onAutoZoomChange'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initialize: function(options) {
|
||||||
initialize: function(options, config) {
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.el = $(this.el);
|
this.el = $(this.el);
|
||||||
|
|
||||||
// Listen to changes in the fields
|
// Listen to changes in the fields
|
||||||
this.model.bind('change', function() {
|
this.model.fields.bind('change', function() {
|
||||||
self._setupGeometryField();
|
self._setupGeometryField();
|
||||||
});
|
});
|
||||||
this.model.fields.bind('add', this.render);
|
this.model.fields.bind('add', this.render);
|
||||||
@@ -136,9 +134,17 @@ my.Map = Backbone.View.extend({
|
|||||||
self.visible = false;
|
self.visible = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var stateData = _.extend({
|
||||||
|
geomField: null,
|
||||||
|
lonField: null,
|
||||||
|
latField: null
|
||||||
|
},
|
||||||
|
options.state
|
||||||
|
);
|
||||||
|
this.state = new recline.Model.ObjectState(stateData);
|
||||||
|
|
||||||
this.autoZoom = true;
|
this.autoZoom = true;
|
||||||
this.mapReady = false;
|
this.mapReady = false;
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -155,12 +161,12 @@ my.Map = Backbone.View.extend({
|
|||||||
this.$map = this.el.find('.panel.map');
|
this.$map = this.el.find('.panel.map');
|
||||||
|
|
||||||
if (this.geomReady && this.model.fields.length){
|
if (this.geomReady && this.model.fields.length){
|
||||||
if (this._geomFieldName){
|
if (this.state.get('geomField')){
|
||||||
this._selectOption('editor-geom-field',this._geomFieldName);
|
this._selectOption('editor-geom-field',this.state.get('geomField'));
|
||||||
$('#editor-field-type-geom').attr('checked','checked').change();
|
$('#editor-field-type-geom').attr('checked','checked').change();
|
||||||
} else{
|
} else{
|
||||||
this._selectOption('editor-lon-field',this._lonFieldName);
|
this._selectOption('editor-lon-field',this.state.get('lonField'));
|
||||||
this._selectOption('editor-lat-field',this._latFieldName);
|
this._selectOption('editor-lat-field',this.state.get('latField'));
|
||||||
$('#editor-field-type-latlon').attr('checked','checked').change();
|
$('#editor-field-type-latlon').attr('checked','checked').change();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +195,7 @@ my.Map = Backbone.View.extend({
|
|||||||
// * refresh: Clear existing features and add all current documents
|
// * refresh: Clear existing features and add all current documents
|
||||||
//
|
//
|
||||||
redraw: function(action,doc){
|
redraw: function(action,doc){
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
action = action || 'refresh';
|
action = action || 'refresh';
|
||||||
|
|
||||||
if (this.geomReady && this.mapReady){
|
if (this.geomReady && this.mapReady){
|
||||||
@@ -227,14 +231,19 @@ my.Map = Backbone.View.extend({
|
|||||||
onEditorSubmit: function(e){
|
onEditorSubmit: function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if ($('#editor-field-type-geom').attr('checked')){
|
if ($('#editor-field-type-geom').attr('checked')){
|
||||||
this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
|
this.state.set({
|
||||||
this._latFieldName = this._lonFieldName = false;
|
geomField: $('.editor-geom-field > select > option:selected').val(),
|
||||||
|
lonField: null,
|
||||||
|
latField: null
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this._geomFieldName = false;
|
this.state.set({
|
||||||
this._latFieldName = $('.editor-lat-field > select > option:selected').val();
|
geomField: null,
|
||||||
this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
|
lonField: $('.editor-lon-field > select > option:selected').val(),
|
||||||
|
latField: $('.editor-lat-field > select > option:selected').val()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
|
this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
|
||||||
this.redraw();
|
this.redraw();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -327,16 +336,16 @@ my.Map = Backbone.View.extend({
|
|||||||
//
|
//
|
||||||
_getGeometryFromDocument: function(doc){
|
_getGeometryFromDocument: function(doc){
|
||||||
if (this.geomReady){
|
if (this.geomReady){
|
||||||
if (this._geomFieldName){
|
if (this.state.get('geomField')){
|
||||||
// We assume that the contents of the field are a valid GeoJSON object
|
// We assume that the contents of the field are a valid GeoJSON object
|
||||||
return doc.attributes[this._geomFieldName];
|
return doc.attributes[this.state.get('geomField')];
|
||||||
} else if (this._lonFieldName && this._latFieldName){
|
} else if (this.state.get('lonField') && this.state.get('latField')){
|
||||||
// We'll create a GeoJSON like point object from the two lat/lon fields
|
// We'll create a GeoJSON like point object from the two lat/lon fields
|
||||||
return {
|
return {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [
|
coordinates: [
|
||||||
doc.attributes[this._lonFieldName],
|
doc.attributes[this.state.get('lonField')],
|
||||||
doc.attributes[this._latFieldName]
|
doc.attributes[this.state.get('latField')]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -350,12 +359,12 @@ my.Map = Backbone.View.extend({
|
|||||||
// If not found, the user can define them via the UI form.
|
// If not found, the user can define them via the UI form.
|
||||||
_setupGeometryField: function(){
|
_setupGeometryField: function(){
|
||||||
var geomField, latField, lonField;
|
var geomField, latField, lonField;
|
||||||
|
this.state.set({
|
||||||
this._geomFieldName = this._checkField(this.geometryFieldNames);
|
geomField: this._checkField(this.geometryFieldNames),
|
||||||
this._latFieldName = this._checkField(this.latitudeFieldNames);
|
latField: this._checkField(this.latitudeFieldNames),
|
||||||
this._lonFieldName = this._checkField(this.longitudeFieldNames);
|
lonField: this._checkField(this.longitudeFieldNames)
|
||||||
|
});
|
||||||
this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
|
this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Private: Check if a field in the current model exists in the provided
|
// Private: Check if a field in the current model exists in the provided
|
||||||
|
|||||||
179
src/view.js
179
src/view.js
@@ -1,16 +1,83 @@
|
|||||||
/*jshint multistr:true */
|
/*jshint multistr:true */
|
||||||
|
|
||||||
// # Core View Functionality plus Data Explorer
|
// # Recline Views
|
||||||
//
|
//
|
||||||
// ## Common view concepts
|
// Recline Views are Backbone Views and in keeping with normal Backbone views
|
||||||
|
// are Widgets / Components displaying something in the DOM. Like all Backbone
|
||||||
|
// views they have a pointer to a model or a collection and is bound to an
|
||||||
|
// element.
|
||||||
|
//
|
||||||
|
// Views provided by core Recline are crudely divided into two types:
|
||||||
|
//
|
||||||
|
// * Dataset Views: a View intended for displaying a recline.Model.Dataset
|
||||||
|
// in some fashion. Examples are the Grid, Graph and Map views.
|
||||||
|
// * Widget Views: a widget used for displaying some specific (and
|
||||||
|
// smaller) aspect of a dataset or the application. Examples are
|
||||||
|
// QueryEditor and FilterEditor which both provide a way for editing (a
|
||||||
|
// part of) a `recline.Model.Query` associated to a Dataset.
|
||||||
|
//
|
||||||
|
// ## Dataset View
|
||||||
|
//
|
||||||
|
// These views are just Backbone views with a few additional conventions:
|
||||||
|
//
|
||||||
|
// 1. The model passed to the View should always be a recline.Model.Dataset instance
|
||||||
|
// 2. Views should generate their own root element rather than having it passed
|
||||||
|
// in.
|
||||||
|
// 3. Views should apply a css class named 'recline-{view-name-lower-cased} to
|
||||||
|
// the root element (and for all CSS for this view to be qualified using this
|
||||||
|
// CSS class)
|
||||||
|
// 4. Read-only mode: CSS for this view should respect/utilize
|
||||||
|
// recline-read-only class to trigger read-only behaviour (this class will
|
||||||
|
// usually be set on some parent element of the view's root element.
|
||||||
|
// 5. State: state (configuration) information for the view should be stored on
|
||||||
|
// an attribute named state that is an instance of a Backbone Model (or, more
|
||||||
|
// speficially, be an instance of `recline.Model.ObjectState`). In addition,
|
||||||
|
// a state attribute may be specified in the Hash passed to a View on
|
||||||
|
// iniitialization and this information should be used to set the initial
|
||||||
|
// state of the view.
|
||||||
|
//
|
||||||
|
// Example of state would be the set of fields being plotted in a graph
|
||||||
|
// view.
|
||||||
|
//
|
||||||
|
// More information about State can be found below.
|
||||||
|
//
|
||||||
|
// To summarize some of this, the initialize function for a Dataset View should
|
||||||
|
// look like:
|
||||||
|
//
|
||||||
|
// <pre>
|
||||||
|
// initialize: {
|
||||||
|
// model: {a recline.Model.Dataset instance}
|
||||||
|
// // el: {do not specify - instead view should create}
|
||||||
|
// state: {(optional) Object / Hash specifying initial state}
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
// </pre>
|
||||||
|
//
|
||||||
|
// Note: Dataset Views in core Recline have a common layout on disk as
|
||||||
|
// follows, where ViewName is the named of View class:
|
||||||
|
//
|
||||||
|
// <pre>
|
||||||
|
// src/view-{lower-case-ViewName}.js
|
||||||
|
// css/{lower-case-ViewName}.css
|
||||||
|
// test/view-{lower-case-ViewName}.js
|
||||||
|
// </pre>
|
||||||
//
|
//
|
||||||
// ### State
|
// ### State
|
||||||
//
|
//
|
||||||
// TODO
|
// State information exists in order to support state serialization into the
|
||||||
|
// url or elsewhere and reloading of application from a stored state.
|
||||||
//
|
//
|
||||||
// ### Read-only
|
// State is available not only for individual views (as described above) but
|
||||||
|
// for the dataset (e.g. the current query). For an example of pulling together
|
||||||
|
// state from across multiple components see `recline.View.DataExplorer`.
|
||||||
//
|
//
|
||||||
// TODO
|
// ### Writing your own Views
|
||||||
|
//
|
||||||
|
// See the existing Views.
|
||||||
|
//
|
||||||
|
// ----
|
||||||
|
|
||||||
|
// Standard JS module setup
|
||||||
this.recline = this.recline || {};
|
this.recline = this.recline || {};
|
||||||
this.recline.View = this.recline.View || {};
|
this.recline.View = this.recline.View || {};
|
||||||
|
|
||||||
@@ -38,7 +105,8 @@ this.recline.View = this.recline.View || {};
|
|||||||
//
|
//
|
||||||
// **views**: (optional) the dataset views (Grid, Graph etc) for
|
// **views**: (optional) the dataset views (Grid, Graph etc) for
|
||||||
// DataExplorer to show. This is an array of view hashes. If not provided
|
// DataExplorer to show. This is an array of view hashes. If not provided
|
||||||
// just initialize a Grid with id 'grid'. Example:
|
// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
|
||||||
|
// and labels!).
|
||||||
//
|
//
|
||||||
// <pre>
|
// <pre>
|
||||||
// var views = [
|
// var views = [
|
||||||
@@ -59,10 +127,25 @@ this.recline.View = this.recline.View || {};
|
|||||||
// ];
|
// ];
|
||||||
// </pre>
|
// </pre>
|
||||||
//
|
//
|
||||||
// **state**: state config for this view. Options are:
|
// **state**: standard state config for this view. This state is slightly
|
||||||
|
// special as it includes config of many of the subviews.
|
||||||
//
|
//
|
||||||
// * readOnly: true/false (default: false) value indicating whether to
|
// <pre>
|
||||||
// operate in read-only mode (hiding all editing options).
|
// state = {
|
||||||
|
// query: {dataset query state - see dataset.queryState object}
|
||||||
|
// view-{id1}: {view-state for this view}
|
||||||
|
// view-{id2}: {view-state for }
|
||||||
|
// ...
|
||||||
|
// // Explorer
|
||||||
|
// currentView: id of current view (defaults to first view if not specified)
|
||||||
|
// readOnly: (default: false) run in read-only mode
|
||||||
|
// }
|
||||||
|
// </pre>
|
||||||
|
//
|
||||||
|
// Note that at present we do *not* serialize information about the actual set
|
||||||
|
// of views in use -- e.g. those specified by the views argument -- but instead
|
||||||
|
// expect either that the default views are fine or that the client to have
|
||||||
|
// initialized the DataExplorer with the relevant views themselves.
|
||||||
my.DataExplorer = Backbone.View.extend({
|
my.DataExplorer = Backbone.View.extend({
|
||||||
template: ' \
|
template: ' \
|
||||||
<div class="recline-data-explorer"> \
|
<div class="recline-data-explorer"> \
|
||||||
@@ -71,7 +154,7 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
<div class="header"> \
|
<div class="header"> \
|
||||||
<ul class="navigation"> \
|
<ul class="navigation"> \
|
||||||
{{#views}} \
|
{{#views}} \
|
||||||
<li><a href="#{{id}}" class="btn">{{label}}</a> \
|
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
|
||||||
{{/views}} \
|
{{/views}} \
|
||||||
</ul> \
|
</ul> \
|
||||||
<div class="recline-results-info"> \
|
<div class="recline-results-info"> \
|
||||||
@@ -94,7 +177,8 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
</div> \
|
</div> \
|
||||||
',
|
',
|
||||||
events: {
|
events: {
|
||||||
'click .menu-right a': 'onMenuClick'
|
'click .menu-right a': '_onMenuClick',
|
||||||
|
'click .navigation a': '_onSwitchView'
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function(options) {
|
initialize: function(options) {
|
||||||
@@ -108,14 +192,26 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
id: 'grid',
|
id: 'grid',
|
||||||
label: 'Grid',
|
label: 'Grid',
|
||||||
view: new my.Grid({
|
view: new my.Grid({
|
||||||
model: this.model
|
model: this.model
|
||||||
})
|
})
|
||||||
|
}, {
|
||||||
|
id: 'graph',
|
||||||
|
label: 'Graph',
|
||||||
|
view: new my.Graph({
|
||||||
|
model: this.model
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
id: 'map',
|
||||||
|
label: 'Map',
|
||||||
|
view: new my.Map({
|
||||||
|
model: this.model
|
||||||
|
})
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
this.state = new recline.Model.ObjectState();
|
|
||||||
// these must be called after pageViews are created
|
// these must be called after pageViews are created
|
||||||
this._setupState(options.state);
|
|
||||||
this.render();
|
this.render();
|
||||||
|
// should come after render as may need to interact with elements in the view
|
||||||
|
this._setupState(options.state);
|
||||||
|
|
||||||
this.router = new Backbone.Router();
|
this.router = new Backbone.Router();
|
||||||
this.setupRouting();
|
this.setupRouting();
|
||||||
@@ -163,7 +259,7 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setReadOnly: function() {
|
setReadOnly: function() {
|
||||||
this.el.addClass('read-only');
|
this.el.addClass('recline-read-only');
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
@@ -204,10 +300,10 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNav: function(pageName, queryString) {
|
updateNav: function(pageName) {
|
||||||
this.el.find('.navigation li').removeClass('active');
|
this.el.find('.navigation li').removeClass('active');
|
||||||
this.el.find('.navigation li a').removeClass('disabled');
|
this.el.find('.navigation li a').removeClass('disabled');
|
||||||
var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
|
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
|
||||||
$el.parent().addClass('active');
|
$el.parent().addClass('active');
|
||||||
$el.addClass('disabled');
|
$el.addClass('disabled');
|
||||||
// show the specific page
|
// show the specific page
|
||||||
@@ -222,7 +318,7 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMenuClick: function(e) {
|
_onMenuClick: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var action = $(e.target).attr('data-action');
|
var action = $(e.target).attr('data-action');
|
||||||
if (action === 'filters') {
|
if (action === 'filters') {
|
||||||
@@ -232,27 +328,47 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onSwitchView: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var viewName = $(e.target).attr('data-view');
|
||||||
|
this.updateNav(viewName);
|
||||||
|
this.state.set({currentView: viewName});
|
||||||
|
},
|
||||||
|
|
||||||
|
// create a state object for this view and do the job of
|
||||||
|
//
|
||||||
|
// a) initializing it from both data passed in and other sources (e.g. hash url)
|
||||||
|
//
|
||||||
|
// b) ensure the state object is updated in responese to changes in subviews, query etc.
|
||||||
_setupState: function(initialState) {
|
_setupState: function(initialState) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
// get data from the query string / hash url plus some defaults
|
||||||
var qs = my.parseHashQueryString();
|
var qs = my.parseHashQueryString();
|
||||||
var query = qs.reclineQuery;
|
var query = qs.reclineQuery;
|
||||||
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
|
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
|
||||||
// backwards compatability (now named view-graph but was named graph)
|
// backwards compatability (now named view-graph but was named graph)
|
||||||
var graphState = qs['view-graph'] || qs.graph;
|
var graphState = qs['view-graph'] || qs.graph;
|
||||||
graphState = graphState ? JSON.parse(graphState) : {};
|
graphState = graphState ? JSON.parse(graphState) : {};
|
||||||
|
|
||||||
|
// now get default data + hash url plus initial state and initial our state object with it
|
||||||
var stateData = _.extend({
|
var stateData = _.extend({
|
||||||
readOnly: false,
|
|
||||||
query: query,
|
query: query,
|
||||||
'view-graph': graphState,
|
'view-graph': graphState,
|
||||||
currentView: null
|
backend: this.model.backend.__type__,
|
||||||
|
dataset: this.model.toJSON(),
|
||||||
|
currentView: this.pageViews[0].id,
|
||||||
|
readOnly: false
|
||||||
},
|
},
|
||||||
initialState);
|
initialState);
|
||||||
this.state.set(stateData);
|
this.state = new recline.Model.ObjectState(stateData);
|
||||||
|
|
||||||
// now do updates based on state
|
// now do updates based on state
|
||||||
if (this.state.get('readOnly')) {
|
if (this.state.get('readOnly')) {
|
||||||
this.setReadOnly();
|
this.setReadOnly();
|
||||||
}
|
}
|
||||||
|
if (this.state.get('currentView')) {
|
||||||
|
this.updateNav(this.state.get('currentView'));
|
||||||
|
}
|
||||||
_.each(this.pageViews, function(pageView) {
|
_.each(this.pageViews, function(pageView) {
|
||||||
var viewId = 'view-' + pageView.id;
|
var viewId = 'view-' + pageView.id;
|
||||||
if (viewId in self.state.attributes) {
|
if (viewId in self.state.attributes) {
|
||||||
@@ -260,7 +376,7 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// bind for changes state in associated objects
|
// finally ensure we update our state object when state of sub-object changes so that state is always up to date
|
||||||
this.model.queryState.bind('change', function() {
|
this.model.queryState.bind('change', function() {
|
||||||
self.state.set({queryState: self.model.queryState.toJSON()});
|
self.state.set({queryState: self.model.queryState.toJSON()});
|
||||||
});
|
});
|
||||||
@@ -276,14 +392,21 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
// Get the current state of dataset and views
|
|
||||||
getState: function() {
|
|
||||||
return this.state;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ### DataExplorer.restore
|
||||||
|
//
|
||||||
|
// Restore a DataExplorer instance from a serialized state including the associated dataset
|
||||||
|
my.DataExplorer.restore = function(state) {
|
||||||
|
var dataset = recline.Model.Dataset.restore(state);
|
||||||
|
var explorer = new my.DataExplorer({
|
||||||
|
model: dataset,
|
||||||
|
state: state
|
||||||
|
});
|
||||||
|
return explorer;
|
||||||
|
}
|
||||||
|
|
||||||
my.QueryEditor = Backbone.View.extend({
|
my.QueryEditor = Backbone.View.extend({
|
||||||
className: 'recline-query-editor',
|
className: 'recline-query-editor',
|
||||||
template: ' \
|
template: ' \
|
||||||
|
|||||||
@@ -108,10 +108,11 @@ var sample_data = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test("ElasticSearch", function() {
|
test("ElasticSearch", function() {
|
||||||
|
var backend = new recline.Backend.ElasticSearch();
|
||||||
var dataset = new recline.Model.Dataset({
|
var dataset = new recline.Model.Dataset({
|
||||||
url: 'https://localhost:9200/my-es-db/my-es-type'
|
url: 'https://localhost:9200/my-es-db/my-es-type'
|
||||||
},
|
},
|
||||||
'elasticsearch'
|
backend
|
||||||
);
|
);
|
||||||
|
|
||||||
var stub = sinon.stub($, 'ajax', function(options) {
|
var stub = sinon.stub($, 'ajax', function(options) {
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ test('Memory Backend: filters', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Memory Backend: facet', function () {
|
test('Memory Backend: facet', function () {
|
||||||
console.log('here');
|
|
||||||
var dataset = makeBackendDataset();
|
var dataset = makeBackendDataset();
|
||||||
dataset.queryState.addFacet('country');
|
dataset.queryState.addFacet('country');
|
||||||
dataset.query().then(function() {
|
dataset.query().then(function() {
|
||||||
@@ -217,10 +216,11 @@ var dataProxyData = {
|
|||||||
test('DataProxy Backend', function() {
|
test('DataProxy Backend', function() {
|
||||||
// needed only if not stubbing
|
// needed only if not stubbing
|
||||||
// stop();
|
// stop();
|
||||||
|
var backend = new recline.Backend.DataProxy();
|
||||||
var dataset = new recline.Model.Dataset({
|
var dataset = new recline.Model.Dataset({
|
||||||
url: 'http://webstore.thedatahub.org/rufuspollock/gold_prices/data.csv'
|
url: 'http://webstore.thedatahub.org/rufuspollock/gold_prices/data.csv'
|
||||||
},
|
},
|
||||||
'dataproxy'
|
backend
|
||||||
);
|
);
|
||||||
|
|
||||||
var stub = sinon.stub($, 'ajax', function(options) {
|
var stub = sinon.stub($, 'ajax', function(options) {
|
||||||
@@ -419,10 +419,11 @@ var sample_gdocs_spreadsheet_data = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("GDoc Backend", function() {
|
test("GDoc Backend", function() {
|
||||||
|
var backend = new recline.Backend.GDoc();
|
||||||
var dataset = new recline.Model.Dataset({
|
var dataset = new recline.Model.Dataset({
|
||||||
url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
|
url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
|
||||||
},
|
},
|
||||||
'gdocs'
|
backend
|
||||||
);
|
);
|
||||||
|
|
||||||
var stub = sinon.stub($, 'getJSON', function(options, cb) {
|
var stub = sinon.stub($, 'getJSON', function(options, cb) {
|
||||||
@@ -450,7 +451,7 @@ test("GDoc Backend.getUrl", function() {
|
|||||||
var dataset = new recline.Model.Dataset({
|
var dataset = new recline.Model.Dataset({
|
||||||
url: 'https://docs.google.com/spreadsheet/ccc?key=' + key + '#gid=0'
|
url: 'https://docs.google.com/spreadsheet/ccc?key=' + key + '#gid=0'
|
||||||
});
|
});
|
||||||
var backend = recline.Model.backends['gdocs'];
|
var backend = new recline.Backend.GDoc();
|
||||||
var out = backend.getUrl(dataset);
|
var out = backend.getUrl(dataset);
|
||||||
var exp = 'https://spreadsheets.google.com/feeds/list/' + key + '/1/public/values?alt=json'
|
var exp = 'https://spreadsheets.google.com/feeds/list/' + key + '/1/public/values?alt=json'
|
||||||
equal(exp, out);
|
equal(exp, out);
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Qunit Tests</title>
|
<title>Qunit Tests</title>
|
||||||
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen" />
|
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen" />
|
||||||
|
<!-- need this stylesheet because flot will complain if canvas does not have a height -->
|
||||||
|
<link rel="stylesheet" href="../css/graph.css" type="text/css" media="screen" />
|
||||||
|
|
||||||
<script type="text/javascript" src="../vendor/jquery-1.7.1.js"></script>
|
<script type="text/javascript" src="../vendor/jquery-1.7.1.js"></script>
|
||||||
<script type="text/javascript" src="../vendor/underscore-1.1.6.js"></script>
|
<script type="text/javascript" src="../vendor/underscore-1.1.6.js"></script>
|
||||||
<script type="text/javascript" src="../vendor/backbone-0.5.1.js"></script>
|
<script type="text/javascript" src="../vendor/backbone-0.5.1.js"></script>
|
||||||
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script>
|
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script>
|
||||||
<script type="text/javascript" src="../vendor/jquery.flot-0.7.js"></script>
|
<script type="text/javascript" src="../vendor/jquery.flot-0.7.js"></script>
|
||||||
|
<script type="text/javascript" src="../vendor/leaflet/0.3.1/leaflet.js"></script>
|
||||||
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
|
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="qunit/qunit.js"></script>
|
<script type="text/javascript" src="qunit/qunit.js"></script>
|
||||||
@@ -19,20 +22,23 @@
|
|||||||
<script type="text/javascript" src="base.js"></script>
|
<script type="text/javascript" src="base.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../src/model.js"></script>
|
<script type="text/javascript" src="../src/model.js"></script>
|
||||||
<script type="text/javascript" src="model.test.js"></script>
|
|
||||||
<script type="text/javascript" src="../src/backend/base.js"></script>
|
<script type="text/javascript" src="../src/backend/base.js"></script>
|
||||||
<script type="text/javascript" src="../src/backend/memory.js"></script>
|
<script type="text/javascript" src="../src/backend/memory.js"></script>
|
||||||
<script type="text/javascript" src="../src/backend/dataproxy.js"></script>
|
<script type="text/javascript" src="../src/backend/dataproxy.js"></script>
|
||||||
<script type="text/javascript" src="../src/backend/gdocs.js"></script>
|
<script type="text/javascript" src="../src/backend/gdocs.js"></script>
|
||||||
<script type="text/javascript" src="../src/backend/elasticsearch.js"></script>
|
<script type="text/javascript" src="../src/backend/elasticsearch.js"></script>
|
||||||
<script type="text/javascript" src="../src/backend/localcsv.js"></script>
|
<script type="text/javascript" src="../src/backend/localcsv.js"></script>
|
||||||
|
<script type="text/javascript" src="model.test.js"></script>
|
||||||
<script type="text/javascript" src="backend.test.js"></script>
|
<script type="text/javascript" src="backend.test.js"></script>
|
||||||
<script type="text/javascript" src="backend.elasticsearch.test.js"></script>
|
<script type="text/javascript" src="backend.elasticsearch.test.js"></script>
|
||||||
<script type="text/javascript" src="backend.localcsv.test.js"></script>
|
<script type="text/javascript" src="backend.localcsv.test.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../src/view.js"></script>
|
<script type="text/javascript" src="../src/view.js"></script>
|
||||||
<script type="text/javascript" src="../src/view-grid.js"></script>
|
<script type="text/javascript" src="../src/view-grid.js"></script>
|
||||||
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
|
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
|
||||||
<script type="text/javascript" src="../src/view-graph.js"></script>
|
<script type="text/javascript" src="../src/view-graph.js"></script>
|
||||||
|
<script type="text/javascript" src="../src/view-map.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="view-grid.test.js"></script>
|
<script type="text/javascript" src="view-grid.test.js"></script>
|
||||||
<script type="text/javascript" src="view-graph.test.js"></script>
|
<script type="text/javascript" src="view-graph.test.js"></script>
|
||||||
<script type="text/javascript" src="view.test.js"></script>
|
<script type="text/javascript" src="view.test.js"></script>
|
||||||
|
|||||||
@@ -103,6 +103,16 @@ test('Dataset _prepareQuery', function () {
|
|||||||
deepEqual(out, exp);
|
deepEqual(out, exp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Dataset _backendFromString', function () {
|
||||||
|
var dataset = new recline.Model.Dataset();
|
||||||
|
|
||||||
|
var out = dataset._backendFromString('recline.Backend.Memory');
|
||||||
|
equal(out.__type__, 'memory');
|
||||||
|
|
||||||
|
var out = dataset._backendFromString('dataproxy');
|
||||||
|
equal(out.__type__, 'dataproxy');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// =================================
|
// =================================
|
||||||
// Query
|
// Query
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ test('new GridRow View', function () {
|
|||||||
var tds = $el.find('td');
|
var tds = $el.find('td');
|
||||||
equal(tds.length, 3);
|
equal(tds.length, 3);
|
||||||
equal($(tds[1]).attr('data-field'), 'a');
|
equal($(tds[1]).attr('data-field'), 'a');
|
||||||
|
view.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(this.jQuery);
|
})(this.jQuery);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test('basic explorer functionality', function () {
|
|||||||
$el.remove();
|
$el.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getState', function () {
|
test('get State', function () {
|
||||||
var $el = $('<div class="test-view-explorer-getstate" />');
|
var $el = $('<div class="test-view-explorer-getstate" />');
|
||||||
$('.fixtures .data-explorer-here').append($el);
|
$('.fixtures .data-explorer-here').append($el);
|
||||||
var dataset = Fixture.getDataset();
|
var dataset = Fixture.getDataset();
|
||||||
@@ -23,27 +23,52 @@ test('getState', function () {
|
|||||||
model: dataset,
|
model: dataset,
|
||||||
el: $el
|
el: $el
|
||||||
});
|
});
|
||||||
var state = explorer.getState();
|
var state = explorer.state;
|
||||||
ok(state.get('query'));
|
ok(state.get('query'));
|
||||||
equal(state.get('readOnly'), false);
|
equal(state.get('readOnly'), false);
|
||||||
|
equal(state.get('currentView'), 'grid');
|
||||||
equal(state.get('query').size, 100);
|
equal(state.get('query').size, 100);
|
||||||
deepEqual(state.get('view-grid').hiddenFields, []);
|
deepEqual(state.get('view-grid').hiddenFields, []);
|
||||||
|
equal(state.get('backend'), 'memory');
|
||||||
|
ok(state.get('dataset').id !== null);
|
||||||
$el.remove();
|
$el.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initialize state', function () {
|
test('initialize state', function () {
|
||||||
|
var $el = $('<div class="test-view-explorer-init-state" />');
|
||||||
|
$('.fixtures .data-explorer-here').append($el);
|
||||||
var dataset = Fixture.getDataset();
|
var dataset = Fixture.getDataset();
|
||||||
var explorer = new recline.View.DataExplorer({
|
var explorer = new recline.View.DataExplorer({
|
||||||
model: dataset,
|
model: dataset,
|
||||||
|
el: $el,
|
||||||
state: {
|
state: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
currentView: 'graph',
|
||||||
'view-grid': {
|
'view-grid': {
|
||||||
hiddenFields: ['x']
|
hiddenFields: ['x']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var state = explorer.getState();
|
ok(explorer.state.get('readOnly'));
|
||||||
ok(state.get('readOnly'));
|
ok(explorer.state.get('currentView'), 'graph');
|
||||||
|
// check the correct view is visible
|
||||||
|
var css = explorer.el.find('.navigation a[data-view="graph"]').attr('class').split(' ');
|
||||||
|
ok(_.contains(css, 'disabled'), css);
|
||||||
|
|
||||||
|
var css = explorer.el.find('.navigation a[data-view="grid"]').attr('class').split(' ');
|
||||||
|
ok(!(_.contains(css, 'disabled')), css);
|
||||||
|
$el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore (from serialized state)', function() {
|
||||||
|
var dataset = Fixture.getDataset();
|
||||||
|
var explorer = new recline.View.DataExplorer({
|
||||||
|
model: dataset,
|
||||||
|
});
|
||||||
|
var state = explorer.state.toJSON();
|
||||||
|
var explorerNew = recline.View.DataExplorer.restore(state);
|
||||||
|
var out = explorerNew.state.toJSON();
|
||||||
|
equal(out.backend, state.backend);
|
||||||
});
|
});
|
||||||
|
|
||||||
})(this.jQuery);
|
})(this.jQuery);
|
||||||
|
|||||||
Reference in New Issue
Block a user