Merge branch 'master' into gh-pages
This commit is contained in:
@@ -60,6 +60,10 @@
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recline-query-facet-editor {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
/**********************************************************
|
/**********************************************************
|
||||||
* Notifications
|
* Notifications
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
|
|||||||
@@ -81,19 +81,20 @@ function localDataset() {
|
|||||||
, name: '1-my-test-dataset'
|
, name: '1-my-test-dataset'
|
||||||
, id: datasetId
|
, id: datasetId
|
||||||
},
|
},
|
||||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'label'}],
|
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
|
||||||
documents: [
|
documents: [
|
||||||
{id: 0, x: 1, y: 2, z: 3, label: 'first'}
|
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
|
||||||
, {id: 1, x: 2, y: 4, z: 6, label: 'second'}
|
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
|
||||||
, {id: 2, x: 3, y: 6, z: 9, label: 'third'}
|
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
|
||||||
, {id: 3, x: 4, y: 8, z: 12, label: 'fourth'}
|
, {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
|
||||||
, {id: 4, x: 5, y: 10, z: 15, label: 'fifth'}
|
, {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
|
||||||
, {id: 5, x: 6, y: 12, z: 18, label: 'sixth'}
|
, {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth'}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
var backend = new recline.Backend.Memory();
|
var backend = new recline.Backend.Memory();
|
||||||
backend.addDataset(inData);
|
backend.addDataset(inData);
|
||||||
var dataset = new recline.Model.Dataset({id: datasetId}, backend);
|
var dataset = new recline.Model.Dataset({id: datasetId}, backend);
|
||||||
|
dataset.queryState.addFacet('country');
|
||||||
return dataset;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
index.html
65
index.html
@@ -9,9 +9,9 @@
|
|||||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
|
||||||
<link rel="stylesheet" href="vendor/bootstrap/2.0.0/css/bootstrap.css" />
|
<link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap.css" />
|
||||||
<link rel="stylesheet" href="http://opendatahandbook.org/en/_static/bootstrap-sphinx.css" />
|
<link rel="stylesheet" href="http://opendatahandbook.org/en/_static/bootstrap-sphinx.css" />
|
||||||
<link rel="stylesheet" href="vendor/bootstrap/2.0.0/css/bootstrap-responsive.css" />
|
<link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap-responsive.css" />
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
html, body {
|
html, body {
|
||||||
@@ -65,7 +65,6 @@
|
|||||||
<ul class="nav">
|
<ul class="nav">
|
||||||
<li><a href="demo/">Demo</a></li>
|
<li><a href="demo/">Demo</a></li>
|
||||||
<li><a href="#docs">Docs</a></li>
|
<li><a href="#docs">Docs</a></li>
|
||||||
<li><a href="#downloads">Download</a></li>
|
|
||||||
<li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li>
|
<li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<a class="nav-logo pull-right" href="http://okfn.org/" title="An Open Knowledge Foundation Project">
|
<a class="nav-logo pull-right" href="http://okfn.org/" title="An Open Knowledge Foundation Project">
|
||||||
@@ -140,7 +139,7 @@ Backbone.history.start();
|
|||||||
href="demo/">Demo</a> -- just hit view source (NB: the javascript for the
|
href="demo/">Demo</a> -- just hit view source (NB: the javascript for the
|
||||||
demo is in: <a href="demo/js/app.js">app.js</a>).</p>
|
demo is in: <a href="demo/js/app.js">app.js</a>).</p>
|
||||||
|
|
||||||
<h3 id="doc-concepts">Structure</h3>
|
<h3 id="docs-concepts">Concepts and Structure</h3>
|
||||||
<p>Recline has a simple structure layered on top of the basic Model/View
|
<p>Recline has a simple structure layered on top of the basic Model/View
|
||||||
distinction inherent in Backbone.</p>
|
distinction inherent in Backbone.</p>
|
||||||
|
|
||||||
@@ -154,8 +153,12 @@ Backbone.history.start();
|
|||||||
<p><strong>Backends</strong> connect Dataset and Documents to data
|
<p><strong>Backends</strong> connect Dataset and Documents to data
|
||||||
from a specific 'Backend' data source. They provide methods for loading and
|
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
|
saving Datasets and individuals Documents as well as for bulk loading via a
|
||||||
query API and doing bulk transforms on the backend. More <a
|
query API and doing bulk transforms on the backend.</p>
|
||||||
href="#doc-backends">info on backends can be found below</a>.</p>
|
<p>A template Base class can be found <a href="docs/backend/base.html">in
|
||||||
|
the Backend base module of the source docs</a>. It documents both the
|
||||||
|
relevant methods a Backend must have and (optionally) provides a base
|
||||||
|
'class' for inheritance. You can also find detailed examples of backend
|
||||||
|
implementations in the source documentation below.</p>
|
||||||
|
|
||||||
<p><strong>Views</strong>: complementing the model are various Views (you can also easily write your own). Each view holds a pointer to a Dataset:</p>
|
<p><strong>Views</strong>: complementing the model are various Views (you can also easily write your own). Each view holds a pointer to a Dataset:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -164,58 +167,16 @@ Backbone.history.start();
|
|||||||
<li>FlotGraph: a simple graphing view using <a href="http://code.google.com/p/flot/">Flot</a>.</li>
|
<li>FlotGraph: a simple graphing view using <a href="http://code.google.com/p/flot/">Flot</a>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 id="doc-backends">Backends</h3>
|
<h3 id="docs-source">Source Docs (via Docco)</h3>
|
||||||
<p>Backends are implemented as Backbone models but this is just a convenience
|
|
||||||
(they do not save or load themselves from any remote source). You can see
|
|
||||||
detailed examples of backend implementation in the source documentation
|
|
||||||
below.</p>
|
|
||||||
|
|
||||||
<p>A backend <em>must</em> implement two methods:</p>
|
|
||||||
<pre>
|
|
||||||
sync(method, model, options)
|
|
||||||
query(dataset, queryObj)
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h4>sync(method, model, options)</h4>
|
|
||||||
|
|
||||||
<p>This is an implemntation of Backbone.sync and is used to override
|
|
||||||
Backbone.sync on operations for Datasets and Documents which are using this
|
|
||||||
backend.</p>
|
|
||||||
|
|
||||||
<p>For read-only implementations you will need only to implement read method
|
|
||||||
for Dataset models (and even this can be a null operation). The read method
|
|
||||||
should return relevant metadata for the Dataset. We do not require read support
|
|
||||||
for Documents because they are loaded in bulk by the query method.</p>
|
|
||||||
|
|
||||||
<p>For backends supporting write operations you must implement update and
|
|
||||||
delete support for Document objects.</p>
|
|
||||||
|
|
||||||
<p>All code paths should return an object conforming to the jquery promise
|
|
||||||
API.</p>
|
|
||||||
|
|
||||||
<h4>query(dataset, queryObj)</h4>
|
|
||||||
|
|
||||||
<p>Query the backend for documents returning them in bulk. This method will be
|
|
||||||
used by the Dataset.query method to search the backend for documents,
|
|
||||||
retrieving the results in bulk. This method should also set the docCount
|
|
||||||
attribute on the dataset.</p>
|
|
||||||
|
|
||||||
<p><code>queryObj</code> should be either a recline.Model.Query object or a
|
|
||||||
Hash. The structure of data in the Query object or Hash should follow that
|
|
||||||
defined in <a href="http://github.com/okfn/recline/issues/34">issue 34</a>. (Of
|
|
||||||
course, if you are writing your own backend, and hence have control over the
|
|
||||||
interpretation of the query object, you can use whatever structure you
|
|
||||||
like).</p>
|
|
||||||
|
|
||||||
<h3>Source Docs (via Docco)</h3>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="docs/model.html">Models</a></li>
|
<li><a href="docs/model.html">Models</a></li>
|
||||||
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
|
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
|
||||||
<li><a href="docs/view-grid.html">DataGrid View</a></li>
|
<li><a href="docs/view-grid.html">DataGrid View</a></li>
|
||||||
<li><a href="docs/view-flot-graph.html">Graph View (based on Flot)</a></li>
|
<li><a href="docs/view-flot-graph.html">Graph View (based on Flot)</a></li>
|
||||||
|
<li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li>
|
||||||
<li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li>
|
<li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li>
|
||||||
<li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li>
|
<li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li>
|
||||||
<li><a href="docs/backend/dataproxy.html">Backend: DataProxy</a></li>
|
<li><a href="docs/backend/dataproxy.html">Backend: DataProxy (CSV and XLS on the Web)</a></li>
|
||||||
<li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li>
|
<li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -258,7 +219,7 @@ like).</p>
|
|||||||
<ul class="nav nav-list">
|
<ul class="nav nav-list">
|
||||||
<li><a href="#docs-using">Using it</a></li>
|
<li><a href="#docs-using">Using it</a></li>
|
||||||
<li><a href="#docs-concepts">Concepts and Structure</a></li>
|
<li><a href="#docs-concepts">Concepts and Structure</a></li>
|
||||||
<li><a href="#docs-backends">Backends</a></li>
|
<li><a href="#docs-source">Source Docs (Docco)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div><!--/.well -->
|
</div><!--/.well -->
|
||||||
</div><!--/span-->
|
</div><!--/span-->
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// Backends are connectors to backend data sources and stores
|
// Backends are connectors to backend data sources and stores
|
||||||
//
|
//
|
||||||
// This is just the base module containing various convenience methods.
|
// This is just the base module containing a template Base class and convenience methods.
|
||||||
this.recline = this.recline || {};
|
this.recline = this.recline || {};
|
||||||
this.recline.Backend = this.recline.Backend || {};
|
this.recline.Backend = this.recline.Backend || {};
|
||||||
|
|
||||||
@@ -14,12 +14,91 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return model.backend.sync(method, model, options);
|
return model.backend.sync(method, model, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ## wrapInTimeout
|
// ## recline.Backend.Base
|
||||||
//
|
//
|
||||||
// Crude way to catch backend errors
|
// 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.
|
||||||
|
//
|
||||||
|
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
|
||||||
|
my.Base = Backbone.Model.extend({
|
||||||
|
|
||||||
|
// ### sync
|
||||||
|
//
|
||||||
|
// An implementation of Backbone.sync that will be used to override
|
||||||
|
// Backbone.sync on operations for Datasets and Documents which are using this backend.
|
||||||
|
//
|
||||||
|
// For read-only implementations you will need only to implement read method
|
||||||
|
// for Dataset models (and even this can be a null operation). The read method
|
||||||
|
// should return relevant metadata for the Dataset. We do not require read support
|
||||||
|
// for Documents because they are loaded in bulk by the query method.
|
||||||
|
//
|
||||||
|
// For backends supporting write operations you must implement update and delete support for Document objects.
|
||||||
|
//
|
||||||
|
// All code paths should return an object conforming to the jquery promise API.
|
||||||
|
sync: function(method, model, options) {
|
||||||
|
},
|
||||||
|
|
||||||
|
// ### query
|
||||||
|
//
|
||||||
|
// Query the backend for documents returning them in bulk. This method will
|
||||||
|
// be used by the Dataset.query method to search the backend for documents,
|
||||||
|
// retrieving the results in bulk.
|
||||||
|
//
|
||||||
|
// @param {recline.model.Dataset} model: Dataset model.
|
||||||
|
//
|
||||||
|
// @param {Object} queryObj: object describing a query (usually produced by
|
||||||
|
// using recline.Model.Query and calling toJSON on it).
|
||||||
|
//
|
||||||
|
// The structure of data in the Query object or
|
||||||
|
// Hash should follow that defined in <a
|
||||||
|
// href="http://github.com/okfn/recline/issues/34">issue 34</a>.
|
||||||
|
// (Of course, if you are writing your own backend, and hence
|
||||||
|
// have control over the interpretation of the query object, you
|
||||||
|
// can use whatever structure you like).
|
||||||
|
//
|
||||||
|
// @returns {Promise} promise API object. The promise resolve method will
|
||||||
|
// be called on query completion with a QueryResult object.
|
||||||
|
//
|
||||||
|
// A QueryResult has the following structure (modelled closely on
|
||||||
|
// ElasticSearch - see <a
|
||||||
|
// href="https://github.com/okfn/recline/issues/57">this issue for more
|
||||||
|
// details</a>):
|
||||||
|
//
|
||||||
|
// <pre>
|
||||||
|
// {
|
||||||
|
// total: // (required) total number of results (can be null)
|
||||||
|
// hits: [ // (required) one entry for each result document
|
||||||
|
// {
|
||||||
|
// _score: // (optional) match score for document
|
||||||
|
// _type: // (optional) document type
|
||||||
|
// _source: // (required) document/row object
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// facets: { // (optional)
|
||||||
|
// // facet results (as per <http://www.elasticsearch.org/guide/reference/api/search/facets/>)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// </pre>
|
||||||
|
query: function(model, queryObj) {
|
||||||
|
},
|
||||||
|
|
||||||
|
// convenience method to convert simple set of documents / rows to a QueryResult
|
||||||
|
_docsToQueryResult: function(rows) {
|
||||||
|
var hits = _.map(rows, function(row) {
|
||||||
|
return { _source: row };
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total: null,
|
||||||
|
hits: hits
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// ## _wrapInTimeout
|
||||||
|
//
|
||||||
|
// Convenience method providing a crude way to catch backend errors on JSONP calls.
|
||||||
// Many of backends use JSONP and so will not get error messages and this is
|
// Many of backends use JSONP and so will not get error messages and this is
|
||||||
// a crude way to catch those errors.
|
// a crude way to catch those errors.
|
||||||
my.wrapInTimeout = function(ourFunction) {
|
_wrapInTimeout: function(ourFunction) {
|
||||||
var dfd = $.Deferred();
|
var dfd = $.Deferred();
|
||||||
var timeout = 5000;
|
var timeout = 5000;
|
||||||
var timer = setTimeout(function() {
|
var timer = setTimeout(function() {
|
||||||
@@ -38,5 +117,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
;
|
;
|
||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}(jQuery, this.recline.Backend));
|
}(jQuery, this.recline.Backend));
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// * format: (optional) csv | xls (defaults to csv if not specified)
|
// * format: (optional) csv | xls (defaults to csv if not specified)
|
||||||
//
|
//
|
||||||
// Note that this is a **read-only** backend.
|
// Note that this is a **read-only** backend.
|
||||||
my.DataProxy = Backbone.Model.extend({
|
my.DataProxy = my.Base.extend({
|
||||||
defaults: {
|
defaults: {
|
||||||
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
||||||
},
|
},
|
||||||
@@ -35,6 +35,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
query: function(dataset, queryObj) {
|
query: function(dataset, queryObj) {
|
||||||
|
var self = this;
|
||||||
var base = this.get('dataproxy_url');
|
var base = this.get('dataproxy_url');
|
||||||
var data = {
|
var data = {
|
||||||
url: dataset.get('url')
|
url: dataset.get('url')
|
||||||
@@ -47,7 +48,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
, dataType: 'jsonp'
|
, dataType: 'jsonp'
|
||||||
});
|
});
|
||||||
var dfd = $.Deferred();
|
var dfd = $.Deferred();
|
||||||
my.wrapInTimeout(jqxhr).done(function(results) {
|
this._wrapInTimeout(jqxhr).done(function(results) {
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
dfd.reject(results.error);
|
dfd.reject(results.error);
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
});
|
});
|
||||||
return tmp;
|
return tmp;
|
||||||
});
|
});
|
||||||
dfd.resolve(_out);
|
dfd.resolve(self._docsToQueryResult(_out));
|
||||||
})
|
})
|
||||||
.fail(function(arguments) {
|
.fail(function(arguments) {
|
||||||
dfd.reject(arguments);
|
dfd.reject(arguments);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// localhost:9200 with index twitter and type tweet it would be
|
// localhost:9200 with index twitter and type tweet it would be
|
||||||
//
|
//
|
||||||
// <pre>http://localhost:9200/twitter/tweet</pre>
|
// <pre>http://localhost:9200/twitter/tweet</pre>
|
||||||
my.ElasticSearch = Backbone.Model.extend({
|
my.ElasticSearch = my.Base.extend({
|
||||||
_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;
|
||||||
@@ -39,7 +39,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
dataType: 'jsonp'
|
dataType: 'jsonp'
|
||||||
});
|
});
|
||||||
var dfd = $.Deferred();
|
var dfd = $.Deferred();
|
||||||
my.wrapInTimeout(jqxhr).done(function(schema) {
|
this._wrapInTimeout(jqxhr).done(function(schema) {
|
||||||
// only one top level key in ES = the type so we can ignore it
|
// only one top level key in ES = the type so we can ignore it
|
||||||
var key = _.keys(schema)[0];
|
var key = _.keys(schema)[0];
|
||||||
var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
|
var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
|
||||||
@@ -93,13 +93,15 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
var dfd = $.Deferred();
|
var dfd = $.Deferred();
|
||||||
// TODO: fail case
|
// TODO: fail case
|
||||||
jqxhr.done(function(results) {
|
jqxhr.done(function(results) {
|
||||||
model.docCount = results.hits.total;
|
_.each(results.hits.hits, function(hit) {
|
||||||
var docs = _.map(results.hits.hits, function(result) {
|
if (!'id' in hit._source && hit._id) {
|
||||||
var _out = result._source;
|
hit._source.id = hit._id;
|
||||||
_out.id = result._id;
|
}
|
||||||
return _out;
|
})
|
||||||
});
|
if (results.facets) {
|
||||||
dfd.resolve(docs);
|
results.hits.facets = results.facets;
|
||||||
|
}
|
||||||
|
dfd.resolve(results.hits);
|
||||||
});
|
});
|
||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// 'gdocs'
|
// 'gdocs'
|
||||||
// );
|
// );
|
||||||
// </pre>
|
// </pre>
|
||||||
my.GDoc = Backbone.Model.extend({
|
my.GDoc = my.Base.extend({
|
||||||
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) {
|
||||||
@@ -67,7 +67,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
_.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
|
_.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
|
||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
dfd.resolve(objs);
|
dfd.resolve(this._docsToQueryResult(objs));
|
||||||
return dfd;
|
return dfd;
|
||||||
},
|
},
|
||||||
gdocsToJavascript: function(gdocsSpreadsheet) {
|
gdocsToJavascript: function(gdocsSpreadsheet) {
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
datasetInfo.fields = fields;
|
datasetInfo.fields = fields;
|
||||||
} else {
|
} else {
|
||||||
if (data) {
|
if (data) {
|
||||||
datasetInfo.fields = _.map(data[0], function(cell) {
|
datasetInfo.fields = _.map(data[0], function(value, key) {
|
||||||
return {id: cell};
|
return {id: key};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
// dataset.fetch();
|
// dataset.fetch();
|
||||||
// etc ...
|
// etc ...
|
||||||
// </pre>
|
// </pre>
|
||||||
my.Memory = Backbone.Model.extend({
|
my.Memory = my.Base.extend({
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
this.datasets = {};
|
this.datasets = {};
|
||||||
},
|
},
|
||||||
@@ -115,9 +115,10 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
query: function(model, queryObj) {
|
query: function(model, queryObj) {
|
||||||
|
var dfd = $.Deferred();
|
||||||
|
var out = {};
|
||||||
var numRows = queryObj.size;
|
var numRows = queryObj.size;
|
||||||
var start = queryObj.from;
|
var start = queryObj.from;
|
||||||
var dfd = $.Deferred();
|
|
||||||
results = this.datasets[model.id].documents;
|
results = this.datasets[model.id].documents;
|
||||||
// not complete sorting!
|
// not complete sorting!
|
||||||
_.each(queryObj.sort, function(sortObj) {
|
_.each(queryObj.sort, function(sortObj) {
|
||||||
@@ -127,9 +128,48 @@ this.recline.Backend = this.recline.Backend || {};
|
|||||||
return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
|
return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
var results = results.slice(start, start+numRows);
|
out.facets = this._computeFacets(results, queryObj);
|
||||||
dfd.resolve(results);
|
var total = results.length;
|
||||||
|
resultsObj = this._docsToQueryResult(results.slice(start, start+numRows));
|
||||||
|
_.extend(out, resultsObj);
|
||||||
|
out.total = total;
|
||||||
|
dfd.resolve(out);
|
||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
|
},
|
||||||
|
|
||||||
|
_computeFacets: function(documents, queryObj) {
|
||||||
|
var facetResults = {};
|
||||||
|
if (!queryObj.facets) {
|
||||||
|
return facetsResults;
|
||||||
|
}
|
||||||
|
_.each(queryObj.facets, function(query, facetId) {
|
||||||
|
facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
|
||||||
|
facetResults[facetId].termsall = {};
|
||||||
|
});
|
||||||
|
// faceting
|
||||||
|
_.each(documents, function(doc) {
|
||||||
|
_.each(queryObj.facets, function(query, facetId) {
|
||||||
|
var fieldId = query.terms.field;
|
||||||
|
var val = doc[fieldId];
|
||||||
|
var tmp = facetResults[facetId];
|
||||||
|
if (val) {
|
||||||
|
tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
|
||||||
|
} else {
|
||||||
|
tmp.missing = tmp.missing + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_.each(queryObj.facets, function(query, facetId) {
|
||||||
|
var tmp = facetResults[facetId];
|
||||||
|
var terms = _.map(tmp.termsall, function(count, term) {
|
||||||
|
return { term: term, count: count };
|
||||||
|
});
|
||||||
|
tmp.terms = _.sortBy(terms, function(item) {
|
||||||
|
// want descending order
|
||||||
|
return -item.count;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return facetResults;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
recline.Model.backends['memory'] = new my.Memory();
|
recline.Model.backends['memory'] = new my.Memory();
|
||||||
|
|||||||
66
src/model.js
66
src/model.js
@@ -23,9 +23,11 @@ my.Dataset = Backbone.Model.extend({
|
|||||||
}
|
}
|
||||||
this.fields = new my.FieldList();
|
this.fields = new my.FieldList();
|
||||||
this.currentDocuments = new my.DocumentList();
|
this.currentDocuments = new my.DocumentList();
|
||||||
|
this.facets = new my.FacetList();
|
||||||
this.docCount = null;
|
this.docCount = null;
|
||||||
this.queryState = new my.Query();
|
this.queryState = new my.Query();
|
||||||
this.queryState.bind('change', this.query);
|
this.queryState.bind('change', this.query);
|
||||||
|
this.queryState.bind('facet:add', this.query);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ### query
|
// ### query
|
||||||
@@ -38,18 +40,26 @@ my.Dataset = Backbone.Model.extend({
|
|||||||
// Resulting DocumentList are used to reset this.currentDocuments and are
|
// Resulting DocumentList are used to reset this.currentDocuments and are
|
||||||
// also returned.
|
// also returned.
|
||||||
query: function(queryObj) {
|
query: function(queryObj) {
|
||||||
this.trigger('query:start');
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.queryState.set(queryObj);
|
this.trigger('query:start');
|
||||||
|
var actualQuery = self._prepareQuery(queryObj);
|
||||||
var dfd = $.Deferred();
|
var dfd = $.Deferred();
|
||||||
this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
|
this.backend.query(this, actualQuery).done(function(queryResult) {
|
||||||
var docs = _.map(rows, function(row) {
|
self.docCount = queryResult.total;
|
||||||
var _doc = new my.Document(row);
|
var docs = _.map(queryResult.hits, function(hit) {
|
||||||
|
var _doc = new my.Document(hit._source);
|
||||||
_doc.backend = self.backend;
|
_doc.backend = self.backend;
|
||||||
_doc.dataset = self;
|
_doc.dataset = self;
|
||||||
return _doc;
|
return _doc;
|
||||||
});
|
});
|
||||||
self.currentDocuments.reset(docs);
|
self.currentDocuments.reset(docs);
|
||||||
|
if (queryResult.facets) {
|
||||||
|
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
|
||||||
|
facetResult.id = facetId;
|
||||||
|
return new my.Facet(facetResult);
|
||||||
|
});
|
||||||
|
self.facets.reset(facets);
|
||||||
|
}
|
||||||
self.trigger('query:done');
|
self.trigger('query:done');
|
||||||
dfd.resolve(self.currentDocuments);
|
dfd.resolve(self.currentDocuments);
|
||||||
})
|
})
|
||||||
@@ -60,6 +70,14 @@ my.Dataset = Backbone.Model.extend({
|
|||||||
return dfd.promise();
|
return dfd.promise();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_prepareQuery: function(newQueryObj) {
|
||||||
|
if (newQueryObj) {
|
||||||
|
this.queryState.set(newQueryObj);
|
||||||
|
}
|
||||||
|
var out = this.queryState.toJSON();
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
toTemplateJSON: function() {
|
toTemplateJSON: function() {
|
||||||
var data = this.toJSON();
|
var data = this.toJSON();
|
||||||
data.docCount = this.docCount;
|
data.docCount = this.docCount;
|
||||||
@@ -116,7 +134,45 @@ my.Query = Backbone.Model.extend({
|
|||||||
defaults: {
|
defaults: {
|
||||||
size: 100
|
size: 100
|
||||||
, from: 0
|
, from: 0
|
||||||
|
, facets: {}
|
||||||
|
},
|
||||||
|
// Set (update or add) a terms filter
|
||||||
|
// http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html
|
||||||
|
setFilter: function(fieldId, values) {
|
||||||
|
},
|
||||||
|
addFacet: function(fieldId) {
|
||||||
|
var facets = this.get('facets');
|
||||||
|
// Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
|
||||||
|
if (_.contains(_.keys(facets), fieldId)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
facets[fieldId] = {
|
||||||
|
terms: { field: fieldId }
|
||||||
|
};
|
||||||
|
this.set({facets: facets}, {silent: true});
|
||||||
|
this.trigger('facet:add', this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ## A Facet (Result)
|
||||||
|
my.Facet = Backbone.Model.extend({
|
||||||
|
defaults: {
|
||||||
|
_type: 'terms',
|
||||||
|
// total number of tokens in the facet
|
||||||
|
total: 0,
|
||||||
|
// number of facet values not included in the returned facets
|
||||||
|
other: 0,
|
||||||
|
// number of documents which have no value for the field
|
||||||
|
missing: 0,
|
||||||
|
// term object ({term: , count: ...})
|
||||||
|
terms: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ## A Collection/List of Facets
|
||||||
|
my.FacetList = Backbone.Collection.extend({
|
||||||
|
model: my.Facet
|
||||||
});
|
});
|
||||||
|
|
||||||
// ## Backend registry
|
// ## Backend registry
|
||||||
|
|||||||
59
src/view.js
59
src/view.js
@@ -168,6 +168,10 @@ my.DataExplorer = Backbone.View.extend({
|
|||||||
model: this.model.queryState
|
model: this.model.queryState
|
||||||
});
|
});
|
||||||
this.el.find('.header').append(queryEditor.el);
|
this.el.find('.header').append(queryEditor.el);
|
||||||
|
var queryFacetEditor = new my.FacetQueryEditor({
|
||||||
|
model: this.model
|
||||||
|
});
|
||||||
|
this.el.find('.header').append(queryFacetEditor.el);
|
||||||
},
|
},
|
||||||
|
|
||||||
setupRouting: function() {
|
setupRouting: function() {
|
||||||
@@ -275,6 +279,61 @@ my.QueryEditor = Backbone.View.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
my.FacetQueryEditor = Backbone.View.extend({
|
||||||
|
className: 'recline-query-facet-editor',
|
||||||
|
template: ' \
|
||||||
|
<div class="dropdown js-add-facet"> \
|
||||||
|
<a class="btn dropdown-toggle" data-toggle="dropdown" href=".js-add-facet">Add Facet On <i class="caret"></i></a> \
|
||||||
|
<ul class="dropdown-menu"> \
|
||||||
|
{{#fields}} \
|
||||||
|
<li><a href="#{{id}}">{{label}}</a></li> \
|
||||||
|
{{/fields}} \
|
||||||
|
</ul> \
|
||||||
|
</div> \
|
||||||
|
<div class="facets"> \
|
||||||
|
{{#facets}} \
|
||||||
|
<a class="btn js-facet-show-toggle" data-facet="{{id}}"><i class="icon-plus"></i> {{id}} {{label}}</a> \
|
||||||
|
<ul class="facet-items" data-facet="{{id}}" style="display: none;"> \
|
||||||
|
{{#terms}} \
|
||||||
|
<li>{{term}} ({{count}}) <input type="checkbox" class="facet-choice" data-facet="{{label}}" value="{{term}}" /></li> \
|
||||||
|
{{/terms}} \
|
||||||
|
</ul> \
|
||||||
|
{{/facets}} \
|
||||||
|
</div> \
|
||||||
|
',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click .js-add-facet .dropdown-menu a': 'onAddFacet',
|
||||||
|
'click .js-facet-show-toggle': 'onFacetShowToggle'
|
||||||
|
},
|
||||||
|
initialize: function(model) {
|
||||||
|
_.bindAll(this, 'render');
|
||||||
|
this.el = $(this.el);
|
||||||
|
this.model.facets.bind('all', this.render);
|
||||||
|
this.model.fields.bind('all', this.render);
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var tmplData = {
|
||||||
|
facets: this.model.facets.toJSON(),
|
||||||
|
fields: this.model.fields.toJSON()
|
||||||
|
};
|
||||||
|
var templated = $.mustache(this.template, tmplData);
|
||||||
|
this.el.html(templated);
|
||||||
|
},
|
||||||
|
onAddFacet: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var fieldId = $(e.target).attr('href').slice(1);
|
||||||
|
this.model.queryState.addFacet(fieldId);
|
||||||
|
},
|
||||||
|
onFacetShowToggle: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $a = $(e.target);
|
||||||
|
var facetId = $a.attr('data-facet');
|
||||||
|
var $ul = this.el.find('.facet-items[data-facet="' + facetId + '"]');
|
||||||
|
$ul.toggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
// ## Miscellaneous Utilities
|
// ## Miscellaneous Utilities
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ var memoryData = {
|
|||||||
, name: '1-my-test-dataset'
|
, name: '1-my-test-dataset'
|
||||||
, id: 'test-dataset'
|
, id: 'test-dataset'
|
||||||
},
|
},
|
||||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
|
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
|
||||||
documents: [
|
documents: [
|
||||||
{id: 0, x: 1, y: 2, z: 3}
|
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
|
||||||
, {id: 1, x: 2, y: 4, z: 6}
|
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
|
||||||
, {id: 2, x: 3, y: 6, z: 9}
|
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
|
||||||
, {id: 3, x: 4, y: 8, z: 12}
|
, {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
|
||||||
, {id: 4, x: 5, y: 10, z: 15}
|
, {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
|
||||||
, {id: 5, x: 6, y: 12, z: 18}
|
, {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth'}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ test('Memory Backend: createDataset', function () {
|
|||||||
|
|
||||||
test('Memory Backend: createDataset 2', function () {
|
test('Memory Backend: createDataset 2', function () {
|
||||||
var dataset = recline.Backend.createDataset(memoryData.documents);
|
var dataset = recline.Backend.createDataset(memoryData.documents);
|
||||||
|
equal(dataset.fields.length, 6);
|
||||||
|
deepEqual(['id', 'x', 'y', 'z', 'country', 'label'], dataset.fields.pluck('id'));
|
||||||
dataset.query();
|
dataset.query();
|
||||||
equal(memoryData.documents.length, dataset.currentDocuments.length);
|
equal(memoryData.documents.length, dataset.currentDocuments.length);
|
||||||
});
|
});
|
||||||
@@ -71,12 +73,35 @@ test('Memory Backend: query sort', function () {
|
|||||||
{'y': {order: 'desc'}}
|
{'y': {order: 'desc'}}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
dataset.query(queryObj).then(function(docs) {
|
dataset.query(queryObj).then(function() {
|
||||||
var doc0 = dataset.currentDocuments.models[0].toJSON();
|
var doc0 = dataset.currentDocuments.models[0].toJSON();
|
||||||
equal(doc0.x, 6);
|
equal(doc0.x, 6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Memory Backend: facet', function () {
|
||||||
|
var dataset = makeBackendDataset();
|
||||||
|
dataset.queryState.addFacet('country');
|
||||||
|
dataset.query().then(function() {
|
||||||
|
equal(dataset.facets.length, 1);
|
||||||
|
var exp = [
|
||||||
|
{
|
||||||
|
term: 'UK',
|
||||||
|
count: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: 'DE',
|
||||||
|
count: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: 'US',
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
deepEqual(dataset.facets.get('country').toJSON().terms, exp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Memory Backend: update and delete', function () {
|
test('Memory Backend: update and delete', function () {
|
||||||
var dataset = makeBackendDataset();
|
var dataset = makeBackendDataset();
|
||||||
// convenience for tests - get the data that should get changed
|
// convenience for tests - get the data that should get changed
|
||||||
@@ -377,7 +402,6 @@ test("GDoc Backend", function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
var stub = sinon.stub($, 'getJSON', function(options, cb) {
|
var stub = sinon.stub($, 'getJSON', function(options, cb) {
|
||||||
console.log('options are', options, cb);
|
|
||||||
var partialUrl = 'spreadsheets.google.com';
|
var partialUrl = 'spreadsheets.google.com';
|
||||||
if (options.indexOf(partialUrl) != -1) {
|
if (options.indexOf(partialUrl) != -1) {
|
||||||
cb(sample_gdocs_spreadsheet_data)
|
cb(sample_gdocs_spreadsheet_data)
|
||||||
@@ -385,12 +409,10 @@ test("GDoc Backend", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dataset.fetch().then(function(dataset) {
|
dataset.fetch().then(function(dataset) {
|
||||||
console.log('inside dataset:', dataset, dataset.fields, dataset.get('data'));
|
|
||||||
deepEqual(['column-2', 'column-1'], _.pluck(dataset.fields.toJSON(), 'id'));
|
deepEqual(['column-2', 'column-1'], _.pluck(dataset.fields.toJSON(), 'id'));
|
||||||
//equal(null, dataset.docCount)
|
//equal(null, dataset.docCount)
|
||||||
dataset.query().then(function(docList) {
|
dataset.query().then(function(docList) {
|
||||||
equal(3, docList.length);
|
equal(3, docList.length);
|
||||||
console.log(docList.models[0]);
|
|
||||||
equal("A", docList.models[0].get('column-1'));
|
equal("A", docList.models[0].get('column-1'));
|
||||||
// needed only if not stubbing
|
// needed only if not stubbing
|
||||||
start();
|
start();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
(function ($) {
|
(function ($) {
|
||||||
module("Model");
|
module("Model");
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Field
|
||||||
|
|
||||||
test('Field: basics', function () {
|
test('Field: basics', function () {
|
||||||
var field = new recline.Model.Field({
|
var field = new recline.Model.Field({
|
||||||
id: 'x'
|
id: 'x'
|
||||||
@@ -35,6 +38,10 @@ test('Field: basics', function () {
|
|||||||
equal('XX', out[0].label);
|
equal('XX', out[0].label);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Dataset
|
||||||
|
|
||||||
test('Dataset', function () {
|
test('Dataset', function () {
|
||||||
var meta = {id: 'test', title: 'xyz'};
|
var meta = {id: 'test', title: 'xyz'};
|
||||||
var dataset = new recline.Model.Dataset(meta);
|
var dataset = new recline.Model.Dataset(meta);
|
||||||
@@ -43,4 +50,23 @@ test('Dataset', function () {
|
|||||||
equal(out.fields.length, 2);
|
equal(out.fields.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Dataset _prepareQuery', function () {
|
||||||
|
var meta = {id: 'test', title: 'xyz'};
|
||||||
|
var dataset = new recline.Model.Dataset(meta);
|
||||||
|
|
||||||
|
var out = dataset._prepareQuery();
|
||||||
|
var exp = new recline.Model.Query().toJSON();
|
||||||
|
deepEqual(out, exp);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Query
|
||||||
|
|
||||||
|
test('Query', function () {
|
||||||
|
var query = new recline.Model.Query();
|
||||||
|
query.addFacet('xyz');
|
||||||
|
deepEqual({terms: {field: 'xyz'}}, query.get('facets')['xyz']);
|
||||||
|
});
|
||||||
|
|
||||||
})(this.jQuery);
|
})(this.jQuery);
|
||||||
|
|||||||
Reference in New Issue
Block a user