diff --git a/css/map.css b/css/map.css index 705feebc..c8adde77 100644 --- a/css/map.css +++ b/css/map.css @@ -2,3 +2,22 @@ height: 500px; } +/********************************************************** + * Editor + *********************************************************/ + +.data-map-container .editor { + float: right; + width: 200px; + padding-left: 0px; + margin-left: 10px; +} + +.data-map-container .editor form { + padding-left: 4px; +} + +.data-map-container .editor select { + width: 100%; +} + diff --git a/docs/backend/base.html b/docs/backend/base.html index 61eab0d5..2be99510 100644 --- a/docs/backend/base.html +++ b/docs/backend/base.html @@ -1,59 +1,109 @@ -
base.js | ||
|---|---|---|
Recline Backends- + + + + +
+
+
+
+ base.js
+
+
+
+ #
+
+ Recline BackendsBackends are connectors to backend data sources and stores - -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 || {};
-(function($, my) { | |
Backbone.sync- -Override Backbone.sync to hand off to sync function in relevant backend | Backbone.sync = function(method, model, options) {
+(function($, my) {
+
+
+ Backbone.sync = function(method, model, options) {
return model.backend.sync(method, model, options);
- } | |
recline.Backend.Base- + }; + + + +
+
+
+ #
+
+ recline.Backend.BaseBase 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- +Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience. + +
+
+
+
+ my.Base = Backbone.Model.extend({
+
+
+ #
+
+ syncAn 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- +All code paths should return an object conforming to the jquery promise API. + +
+
+
+
+ sync: function(method, model, options) {
+ },
+
+
+ #
+
+ queryQuery 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 issue 34. (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 this issue for more details): -
{
total: // (required) total number of results (can be null)
@@ -68,8 +118,23 @@ details):
// facet results (as per | query: function(model, queryObj) {
- }, | |
| convenience method to convert simple set of documents / rows to a QueryResult | _docsToQueryResult: function(rows) {
+
+
+
+
+
+ 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 };
});
@@ -77,11 +142,22 @@ details):
total: null,
hits: hits
};
- }, | |
_wrapInTimeout- + }, + + + +
+
+
+ #
+
+ _wrapInTimeoutConvenience 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 -a crude way to catch those errors. | _wrapInTimeout: function(ourFunction) {
+a crude way to catch those errors.
+
+ _wrapInTimeout: function(ourFunction) {
var dfd = $.Deferred();
var timeout = 5000;
var timer = setTimeout(function() {
@@ -104,4 +180,9 @@ a crude way to catch those errors. | }(jQuery, this.recline.Backend)); - |
dataproxy.js | |||
|---|---|---|---|
this.recline = this.recline || {};
+
+
+
+
+ | |||
DataProxy Backend- +(function($, my) { + + + +
+
+
+ #
+
+ DataProxy BackendFor connecting to DataProxy-s. -When initializing the DataProxy backend you can set the following attributes: -
Datasets using using this backend should set the following attributes: -
Note that this is a read-only backend. | my.DataProxy = my.Base.extend({
+
+ my.DataProxy = my.Base.extend({
defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
},
sync: function(method, model, options) {
var self = this;
if (method === "read") {
- if (model.__type__ == 'Dataset') { | ||
| Do nothing as we will get fields in query step (and no metadata to -retrieve) | var dfd = $.Deferred();
+ if (model.__type__ == 'Dataset') {
+
+
+
+ #
+
+ Do nothing as we will get fields in query step (and no metadata to +retrieve) +
+ var dfd = $.Deferred();
dfd.resolve(model);
return dfd.promise();
}
@@ -38,14 +78,14 @@ retrieve) | var self = this;
var base = this.get('dataproxy_url');
var data = {
- url: dataset.get('url')
- , 'max-results': queryObj.size
- , type: dataset.get('format')
+ url: dataset.get('url'),
+ 'max-results': queryObj.size,
+ type: dataset.get('format')
};
var jqxhr = $.ajax({
- url: base
- , data: data
- , dataType: 'jsonp'
+ url: base,
+ data: data,
+ dataType: 'jsonp'
});
var dfd = $.Deferred();
this._wrapInTimeout(jqxhr).done(function(results) {
@@ -76,4 +116,9 @@ retrieve) | }(jQuery, this.recline.Backend));
- |
elasticsearch.js | |
|---|---|
this.recline = this.recline || {};
+
+
+
+
+ | |
ElasticSearch Backend- +(function($, my) { + + + +
+
+
+ #
+
+ ElasticSearch BackendConnecting to ElasticSearch. -To use this backend ensure your Dataset has one of the following attributes (first one found is used): -elasticsearch_url webstore_url @@ -16,8 +44,10 @@ url | my.ElasticSearch = my.Base.extend({
+
+ my.ElasticSearch = my.Base.extend({
_getESUrl: function(dataset) {
var out = dataset.get('elasticsearch_url');
if (out) return out;
@@ -37,7 +67,19 @@ localhost:9200 with index twitter and type tweet it would be
dataType: 'jsonp'
});
var dfd = $.Deferred();
- this._wrapInTimeout(jqxhr).done(function(schema) { |
| only one top level key in ES = the type so we can ignore it | var key = _.keys(schema)[0];
+ this._wrapInTimeout(jqxhr).done(function(schema) {
+
+
+
+ #
+
+ only one top level key in ES = the type so we can ignore it +
+ var key = _.keys(schema)[0];
var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
dict.id = fieldName;
return dict;
@@ -55,35 +97,43 @@ localhost:9200 with index twitter and type tweet it would be
}
},
_normalizeQuery: function(queryObj) {
- if (queryObj.toJSON) {
- var out = queryObj.toJSON();
- } else {
- var out = _.extend({}, queryObj);
- }
- if (out.q != undefined && out.q.trim() === '') {
+ var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
+ if (out.q !== undefined && out.q.trim() === '') {
delete out.q;
}
if (!out.q) {
out.query = {
match_all: {}
- }
+ };
} else {
out.query = {
query_string: {
query: out.q
}
- }
+ };
delete out.q;
- } |
| now do filters (note the plural) | if (out.filters && out.filters.length) {
+ }
+
+
+
+ #
+
+ now do filters (note the plural) +
+ if (out.filters && out.filters.length) {
if (!out.filter) {
- out.filter = {}
+ out.filter = {};
}
if (!out.filter.and) {
out.filter.and = [];
}
out.filter.and = out.filter.and.concat(out.filters);
}
- if (out.filters != undefined) {
+ if (out.filters !== undefined) {
delete out.filters;
}
return out;
@@ -97,12 +147,24 @@ localhost:9200 with index twitter and type tweet it would be
data: data,
dataType: 'jsonp'
});
- var dfd = $.Deferred(); |
| TODO: fail case | jqxhr.done(function(results) {
+ var dfd = $.Deferred();
+
+
+
+ #
+
+ TODO: fail case +
+ jqxhr.done(function(results) {
_.each(results.hits.hits, function(hit) {
- if (!'id' in hit._source && hit._id) {
+ if (!('id' in hit._source) && hit._id) {
hit._source.id = hit._id;
}
- })
+ });
if (results.facets) {
results.hits.facets = results.facets;
}
@@ -115,4 +177,9 @@ localhost:9200 with index twitter and type tweet it would be
}(jQuery, this.recline.Backend));
- |
gdocs.js | |
|---|---|
this.recline = this.recline || {};
+
+
+
+
+ | |
Google spreadsheet backend- +(function($, my) { + + + +
+
+
+ #
+
+ Google spreadsheet backendConnect to Google Docs spreadsheet. -Dataset must have a url attribute pointing to the Gdocs spreadsheet's JSON feed e.g. -
var dataset = new recline.Model.Dataset({
url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
},
'gdocs'
);
- | my.GDoc = my.Base.extend({
+
+
+ my.GDoc = my.Base.extend({
getUrl: function(dataset) {
var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) {
return url;
- } else { |
| https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0 | var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/
+ } else {
+
+
+
+ #
+
+ https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0 +
+ var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
var matches = url.match(regex);
if (matches) {
var key = matches[1];
var worksheet = 1;
- var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
+ var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
return out;
} else {
alert('Failed to extract gdocs key from ' + url);
@@ -44,58 +87,164 @@ var dataset = new recline.Model.Dataset({
model.fields.reset(_.map(result.field, function(fieldId) {
return {id: fieldId};
})
- ); |
| cache data onto dataset (we have loaded whole gdoc it seems!) | model._dataCache = result.data;
+ );
+
+
+
+ #
+
+ cache data onto dataset (we have loaded whole gdoc it seems!) +
+ model._dataCache = result.data;
dfd.resolve(model);
- })
- return dfd.promise(); }
+ });
+ return dfd.promise();
+ }
},
query: function(dataset, queryObj) {
var dfd = $.Deferred();
- var fields = _.pluck(dataset.fields.toJSON(), 'id'); |
| zip the fields with the data rows to produce js objs -TODO: factor this out as a common method with other backends | var objs = _.map(dataset._dataCache, function (d) {
+ var fields = _.pluck(dataset.fields.toJSON(), 'id');
+
+
+
+
+
+ #
+
+ zip the fields with the data rows to produce js objs +TODO: factor this out as a common method with other backends +
+
+ var objs = _.map(dataset._dataCache, function (d) {
var obj = {};
- _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
+ _.each(_.zip(fields, d), function (x) {
+ obj[x[0]] = x[1];
+ });
return obj;
});
dfd.resolve(this._docsToQueryResult(objs));
return dfd;
},
- gdocsToJavascript: function(gdocsSpreadsheet) {
- /*
- :options: (optional) optional argument dictionary:
- columnsToUse: list of columns to use (specified by field names)
- colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
- :return: tabular data object (hash with keys: field and data).
-
- Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
- */
- var options = {};
+ gdocsToJavascript: function(gdocsSpreadsheet) {
+
+
+
+ #
+
+ :options: (optional) optional argument dictionary: + columnsToUse: list of columns to use (specified by field names) + colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). + :return: tabular data object (hash with keys: field and data). +Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. +
+ var options = {};
if (arguments.length > 1) {
options = arguments[1];
}
var results = {
'field': [],
'data': []
- }; |
| default is no special info on type of columns | var colTypes = {};
+ };
+
+
+
+ #
+
+ default is no special info on type of columns +
+ var colTypes = {};
if (options.colTypes) {
colTypes = options.colTypes;
- } |
| either extract column headings from spreadsheet directly, or used supplied ones | if (options.columnsToUse) { |
| columns set to subset supplied | results.field = options.columnsToUse;
- } else { |
| set columns to use to be all available | if (gdocsSpreadsheet.feed.entry.length > 0) {
+ }
+
+
+
+
+
+ #
+
+ either extract column headings from spreadsheet directly, or used supplied ones +
+
+ if (options.columnsToUse) {
+
+
+
+
+
+ #
+
+ columns set to subset supplied +
+
+ results.field = options.columnsToUse;
+ } else {
+
+
+
+ #
+
+ set columns to use to be all available +
+ if (gdocsSpreadsheet.feed.entry.length > 0) {
for (var k in gdocsSpreadsheet.feed.entry[0]) {
if (k.substr(0, 3) == 'gsx') {
- var col = k.substr(4)
- results.field.push(col);
+ var col = k.substr(4);
+ results.field.push(col);
}
}
}
- } |
| converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) | var rep = /^([\d\.\-]+)\%$/;
+ }
+
+
+
+ #
+
+ converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) +
+ var rep = /^([\d\.\-]+)\%$/;
$.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
var row = [];
for (var k in results.field) {
var col = results.field[k];
var _keyname = 'gsx$' + col;
- var value = entry[_keyname]['$t']; |
| if labelled as % and value contains %, convert | if (colTypes[col] == 'percent') {
+ var value = entry[_keyname]['$t'];
+
+
+
+ #
+
+ if labelled as % and value contains %, convert +
+ if (colTypes[col] == 'percent') {
if (rep.test(value)) {
var value2 = rep.exec(value);
var value3 = parseFloat(value2);
@@ -113,4 +262,9 @@ TODO: factor this out as a common method with other backends }(jQuery, this.recline.Backend));
- |
localcsv.js | ||
|---|---|---|
this.recline = this.recline || {};
+
+
+
+
+ | ||
| TODO | reader.onload = function(e) {
+ var reader = new FileReader();
+
+
+
+ #
+
+ TODO +
+ reader.onload = function(e) {
var dataset = my.csvToDataset(e.target.result);
callback(dataset);
};
reader.onerror = function (e) {
alert('Failed to load file. Code: ' + e.target.error.code);
- }
+ };
reader.readAsText(file);
};
@@ -31,19 +65,39 @@
});
var dataset = recline.Backend.createDataset(data, fields);
return dataset;
- } | |
| Converts a Comma Separated Values string into an array of arrays. + }; + + + +
+
+
+ #
+
+ Converts a Comma Separated Values string into an array of arrays. Each line in the CSV becomes an array. -Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats. -@return The CSV parsed as an array @type Array -@param {String} s The string to convert @param {Boolean} [trm=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported -Heavily based on uselesscode's JS CSV parser (MIT Licensed): -thttp://www.uselesscode.org/javascript/csv/ | my.parseCSV= function(s, trm) { | |
| Get rid of any trailing \n | s = chomp(s);
+thttp://www.uselesscode.org/javascript/csv/
+
+
+
+
+ my.parseCSV= function(s, trm) {
+
+
+
+ #
+
+ Get rid of any trailing \n +
+ s = chomp(s);
var cur = '', // The character we are currently processing.
inQuote = false,
@@ -55,10 +109,46 @@ thttp://www.uselesscode.org/javascript/csv/ | |
| If field is empty set to null | if (field === '') {
- field = null; | |
| If the field was not quoted and we are trimming fields, trim it | } else if (trm === true) {
+ if (fieldQuoted !== true) {
+
+
+
+
+
+ #
+
+ If field is empty set to null +
+
+ if (field === '') {
+ field = null;
+
+
+
+ #
+
+ If the field was not quoted and we are trimming fields, trim it +
+ } else if (trm === true) {
field = trim(field);
- } | |
| Convert unquoted numbers to their appropriate types | if (rxIsInt.test(field)) {
+ }
+
+
+
+ #
+
+ Convert unquoted numbers to their appropriate types +
+ if (rxIsInt.test(field)) {
field = parseInt(field, 10);
} else if (rxIsFloat.test(field)) {
field = parseFloat(field, 10);
@@ -68,25 +158,145 @@ thttp://www.uselesscode.org/javascript/csv/ | |
| If we are at a EOF or EOR | if (inQuote === false && (cur === ',' || cur === "\n")) {
- field = processField(field); | |
| Add the current field to the current row | row.push(field); | |
| If this is EOR append row to output and flush row | if (cur === "\n") {
+ cur = s.charAt(i);
+
+
+
+
+
+ #
+
+ If we are at a EOF or EOR +
+
+ if (inQuote === false && (cur === ',' || cur === "\n")) {
+ field = processField(field);
+
+
+
+
+
+ #
+
+ Add the current field to the current row +
+
+ row.push(field);
+
+
+
+ #
+
+ If this is EOR append row to output and flush row +
+ if (cur === "\n") {
out.push(row);
row = [];
- } | |
| Flush the field buffer | field = '';
+ }
+
+
+
+ #
+
+ Flush the field buffer +
+ field = '';
fieldQuoted = false;
- } else { | |
| If it's not a ", add it to the field buffer | if (cur !== '"') {
+ } else {
+
+
+
+ #
+
+ If it's not a ", add it to the field buffer +
+ if (cur !== '"') {
field += cur;
} else {
- if (!inQuote) { | |
| We are not in a quote, start a quote | inQuote = true;
+ if (!inQuote) {
+
+
+
+ #
+
+ We are not in a quote, start a quote +
+ inQuote = true;
fieldQuoted = true;
- } else { | |
| Next char is ", this is an escaped " | if (s.charAt(i + 1) === '"') {
- field += '"'; | |
| Skip the next char | i += 1;
- } else { | |
| It's not escaping, so end quote | inQuote = false;
+ } else {
+
+
+
+
+
+ #
+
+ Next char is ", this is an escaped " +
+
+ if (s.charAt(i + 1) === '"') {
+ field += '"';
+
+
+
+
+
+ #
+
+ Skip the next char +
+
+ i += 1;
+ } else {
+
+
+
+ #
+
+ It's not escaping, so end quote +
+ inQuote = false;
}
}
}
}
- } | |
| Add the last field | field = processField(field);
+ }
+
+
+
+ #
+
+ Add the last field +
+ field = processField(field);
row.push(field);
out.push(row);
@@ -94,10 +304,34 @@ thttp://www.uselesscode.org/javascript/csv/ | |
| If a string has leading or trailing space, + rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/, + + + +
+
+
+ #
+
+ If a string has leading or trailing space, contains a comma double quote or a newline -it needs to be quoted in CSV output | rxNeedsQuoting = /^\s|\s$|,|"|\n/,
- trim = (function () { | |
| Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists | if (String.prototype.trim) {
+it needs to be quoted in CSV output
+
+
+
+
+ rxNeedsQuoting = /^\s|\s$|,|"|\n/,
+ trim = (function () {
+
+
+
+ #
+
+ Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists +
+ if (String.prototype.trim) {
return function (s) {
return s.trim();
};
@@ -109,12 +343,41 @@ it needs to be quoted in CSV output | }()); function chomp(s) { - if (s.charAt(s.length - 1) !== "\n") { |
| Does not end with \n, just return string | return s;
- } else { | |
| Remove the \n | return s.substring(0, s.length - 1);
+ if (s.charAt(s.length - 1) !== "\n") {
+
+
+
+
+
+ #
+
+ Does not end with \n, just return string +
+
+ return s;
+ } else {
+
+
+
+ #
+
+ Remove the \n +
+ return s.substring(0, s.length - 1);
}
}
}(jQuery, this.recline.Backend));
- |
memory.js | |
|---|---|
this.recline = this.recline || {};
+
+
+
+
+ | |
createDataset- +(function($, my) { + + + +
+
+
+ #
+
+ createDatasetConvenience function to create a simple 'in-memory' dataset in one step. -@param data: list of hashes for each document/row in the data ({key: value, key: value}) @param fields: (optional) list of field hashes (each hash defining a hash as per recline.Model.Field). If fields not specified they will be taken from the data. @param metadata: (optional) dataset metadata - see recline.Model.Dataset. -If not defined (or id not provided) id will be autogenerated. | my.createDataset = function(data, fields, metadata) {
+If not defined (or id not provided) id will be autogenerated.
+
+ my.createDataset = function(data, fields, metadata) {
if (!metadata) {
- var metadata = {};
+ metadata = {};
}
if (!metadata.id) {
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
@@ -36,17 +68,22 @@ If not defined (or id not provided) id will be autogenerated.
var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
dataset.fetch();
return dataset;
- }; |
Memory Backend - uses in-memory data- + }; + + + +
+
+
+ #
+
+ Memory Backend - uses in-memory dataTo use it you should provide in your constructor data: -
Example: -
// Backend setup
var backend = recline.Backend.Memory();
@@ -65,7 +102,10 @@ If not defined (or id not provided) id will be autogenerated.
var dataset = Dataset({id: 'my-id'}, 'memory');
dataset.fetch();
etc ...
- | my.Memory = my.Base.extend({
+
+
+ my.Memory = my.Base.extend({
initialize: function() {
this.datasets = {};
},
@@ -74,8 +114,8 @@ If not defined (or id not provided) id will be autogenerated.
},
sync: function(method, model, options) {
var self = this;
+ var dfd = $.Deferred();
if (method === "read") {
- var dfd = $.Deferred();
if (model.__type__ == 'Dataset') {
var rawDataset = this.datasets[model.id];
model.set(rawDataset.metadata);
@@ -85,7 +125,6 @@ If not defined (or id not provided) id will be autogenerated.
}
return dfd.promise();
} else if (method === 'update') {
- var dfd = $.Deferred();
if (model.__type__ == 'Document') {
_.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
if(doc.id === model.id) {
@@ -96,7 +135,6 @@ If not defined (or id not provided) id will be autogenerated.
}
return dfd.promise();
} else if (method === 'delete') {
- var dfd = $.Deferred();
if (model.__type__ == 'Document') {
var rawDataset = self.datasets[model.dataset.id];
var newdocs = _.reject(rawDataset.documents, function(doc) {
@@ -121,7 +159,19 @@ If not defined (or id not provided) id will be autogenerated.
var fieldId = _.keys(filter.term)[0];
return (doc[fieldId] == filter.term[fieldId]);
});
- }); |
| not complete sorting! | _.each(queryObj.sort, function(sortObj) {
+ });
+
+
+
+ #
+
+ not complete sorting! +
+ _.each(queryObj.sort, function(sortObj) {
var fieldName = _.keys(sortObj)[0];
results = _.sortBy(results, function(doc) {
var _out = doc[fieldName];
@@ -145,7 +195,19 @@ If not defined (or id not provided) id will be autogenerated.
_.each(queryObj.facets, function(query, facetId) {
facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
facetResults[facetId].termsall = {};
- }); |
| faceting | _.each(documents, function(doc) {
+ });
+
+
+
+ #
+
+ faceting +
+ _.each(documents, function(doc) {
_.each(queryObj.facets, function(query, facetId) {
var fieldId = query.terms.field;
var val = doc[fieldId];
@@ -162,7 +224,19 @@ If not defined (or id not provided) id will be autogenerated.
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;
+ tmp.terms = _.sortBy(terms, function(item) {
+
+
+
+ #
+
+ want descending order +
+ return -item.count;
});
tmp.terms = tmp.terms.slice(0, 10);
});
@@ -173,4 +247,9 @@ If not defined (or id not provided) id will be autogenerated.
}(jQuery, this.recline.Backend));
- |
model.js | ||
|---|---|---|
Recline Backbone Models | this.recline = this.recline || {};
+
+
+
+
+ | |
A Dataset model- +(function($, my) { + + + +
+
+
+ #
+
+ A Dataset modelA model has the following (non-Backbone) attributes: -@property {FieldList} fields: (aka columns) is a @property {DocumentList} currentDocuments: a @property {number} docCount: total number of documents in this dataset -@property {Backend} backend: the Backend (instance) for this Dataset -@property {Query} queryState: @property {FacetList} facets: FacetList object containing all current -Facets. | my.Dataset = Backbone.Model.extend({
- __type__: 'Dataset', | |
initialize- -Sets up instance properties (see above) | initialize: function(model, backend) {
+Facets.
+
+
+
+
+ my.Dataset = Backbone.Model.extend({
+ __type__: 'Dataset',
+
+
+ initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
if (backend && backend.constructor == String) {
@@ -38,15 +76,24 @@ Facets. | this.queryState = new my.Query();
this.queryState.bind('change', this.query);
this.queryState.bind('facet:add', this.query);
- }, |
query- + }, + + + +
+
+
+ #
+
+ queryAJAX method with promise API to get documents from the backend. -It will query based on current query state (given by this.queryState) updated by queryObj (if provided). -Resulting DocumentList are used to reset this.currentDocuments and are -also returned. | query: function(queryObj) {
+also returned.
+
+ query: function(queryObj) {
var self = this;
this.trigger('query:start');
var actualQuery = self._prepareQuery(queryObj);
@@ -91,16 +138,38 @@ also returned. | data.fields = this.fields.toJSON(); return data; } -}); |
A Document (aka Row)- -A single entry or row in the dataset | my.Document = Backbone.Model.extend({
+});
+
+
+ my.Document = Backbone.Model.extend({
__type__: 'Document',
initialize: function() {
_.bindAll(this, 'getFieldValue');
- }, | |
getFieldValue- + }, + + + +
+
+
+ #
+
+ getFieldValueFor the provided Field get the corresponding rendered computed data value -for this document. | getFieldValue: function(field) {
+for this document.
+
+ getFieldValue: function(field) {
var val = this.get(field.id);
if (field.deriver) {
val = field.deriver(val, field, this);
@@ -110,80 +179,150 @@ for this document. | } return val; } -}); |
A Backbone collection of Documents | my.DocumentList = Backbone.Collection.extend({
+});
+
+
+
+ #
+
+ A Backbone collection of Documents+
+ my.DocumentList = Backbone.Collection.extend({
__type__: 'DocumentList',
model: my.Document
-}); | |
A Field (aka Column) on a Dataset- +}); + + + +
+
+
+ #
+
+ A Field (aka Column) on a DatasetFollowing (Backbone) attributes as standard: -
Following additional instance properties: -@property {Function} renderer: a function to render the data for this field. Signature: function(value, field, doc) where value is the value of this cell, field is corresponding field object and document is the document object. Note that implementing functions can ignore arguments (e.g. function(value) would be a valid formatter function). -@property {Function} deriver: a function to derive/compute the value of data in this field as a function of this field's value (if any) and the current document, its signature and behaviour is the same as for renderer. Use of this function allows you to define an entirely new value for data in this field. This provides support for a) 'derived/computed' fields: i.e. fields whose data are functions of the data in other fields b) transforming the -value of this field prior to rendering. | my.Field = Backbone.Model.extend({ | |
defaults - define default values | defaults: {
+value of this field prior to rendering.
+
+
+
+
+ my.Field = Backbone.Model.extend({
+
+
+
+ #
+
+ defaults - define default values+
+ defaults: {
label: null,
type: 'string',
format: null,
is_derived: false
- }, | |
initialize- + }, + + + +
+
+
+ #
+
+ initialize@param {Object} data: standard Backbone model attributes - -@param {Object} options: renderer and/or deriver functions. | initialize: function(data, options) { | |
| if a hash not passed in the first argument throw error | if ('0' in data) {
+
+
+
+
+ initialize: function(data, options) {
+
+
+
+ #
+
+ if a hash not passed in the first argument throw error +
+ if ('0' in data) {
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
}
- if (this.attributes.label == null) {
+ if (this.attributes.label === null) {
this.set({label: this.id});
}
if (options) {
this.renderer = options.renderer;
this.deriver = options.deriver;
}
+ if (!this.renderer) {
+ this.renderer = this.defaultRenderers[this.get('type')];
+ }
+ },
+ defaultRenderers: {
+ object: function(val, field, doc) {
+ return JSON.stringify(val);
+ },
+ 'float': function(val, field, doc) {
+ var format = field.get('format');
+ if (format === 'percentage') {
+ return val + '%';
+ }
+ }
}
});
my.FieldList = Backbone.Collection.extend({
model: my.Field
-}); | |
Query- +}); + + + +
+
+
+ #
+
+ QueryQuery instances encapsulate a query to the backend (see query method on backend). Useful both for creating queries and for storing and manipulating query state - e.g. from a query editor). -Query Structure and format -Query structure should follow that of ElasticSearch query language. -NB: It is up to specific backends how to implement and support this query structure. Different backends might choose to implement things differently or not support certain features. Please check your backend for details. -Query object has the following key attributes: -
Additions: -
Examples -
{
q: 'quick brown fox',
@@ -217,40 +357,122 @@ execution.
{ term: { 'owner': 'jones' } }
]
}
- | my.Query = Backbone.Model.extend({
+
+
+ my.Query = Backbone.Model.extend({
defaults: function() {
return {
- size: 100
- , from: 0
- , facets: {} | |
| http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html -, filter: {} | , filters: []
- }
- }, | |
addTermFilter- + size: 100, + from: 0, + facets: {}, + + + +
+
+
+
+
+
+ #
+
+ http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html +, filter: {} +
+
+ filters: []
+ };
+ },
+
+
+ #
+
+ addTermFilterSet (update or add) a terms filter to filters - -See http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html | addTermFilter: function(fieldId, value) {
+
+ addTermFilter: function(fieldId, value) {
var filters = this.get('filters');
var filter = { term: {} };
filter.term[fieldId] = value;
filters.push(filter);
- this.set({filters: filters}); | |
| change does not seem to be triggered automatically | if (value) {
+ this.set({filters: filters});
+
+
+
+ #
+
+ change does not seem to be triggered automatically +
+ if (value) {
this.trigger('change');
- } else { | |
| adding a new blank filter and do not want to trigger a new query | this.trigger('change:filters:new-blank');
+ } else {
+
+
+
+ #
+
+ adding a new blank filter and do not want to trigger a new query +
+ this.trigger('change:filters:new-blank');
}
- }, | |
removeFilter- -Remove a filter from filters at index filterIndex | removeFilter: function(filterIndex) {
+ },
+
+
+ removeFilter: function(filterIndex) {
var filters = this.get('filters');
filters.splice(filterIndex, 1);
this.set({filters: filters});
this.trigger('change');
- }, | |
addFacet- + }, + + + +
+
+
+ #
+
+ addFacetAdd a Facet to this query - -See http://www.elasticsearch.org/guide/reference/api/search/facets/ | 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)) {
+
+
+
+
+ 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] = {
@@ -258,20 +480,36 @@ execution.
};
this.set({facets: facets}, {silent: true});
this.trigger('facet:add', this);
+ },
+ addHistogramFacet: function(fieldId) {
+ var facets = this.get('facets');
+ facets[fieldId] = {
+ date_histogram: {
+ field: fieldId,
+ interval: 'day'
+ }
+ };
+ this.set({facets: facets}, {silent: true});
+ this.trigger('facet:add', this);
}
-}); | |
A Facet (Result)- +}); + + + +
+
+
+ #
+
+ A Facet (Result)Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the backend. -Structure of a facet follows that of Facet results in ElasticSearch, see: http://www.elasticsearch.org/guide/reference/api/search/facets/ -Specifically the object structure of a facet looks like (there is one addition compared to ElasticSearch: the "id" field which corresponds to the key used to specify this facet in the facet query): -
{
"id": "id-of-facet",
@@ -296,7 +534,10 @@ key used to specify this facet in the facet query):
}
]
}
- | my.Facet = Backbone.Model.extend({
+
+
+ my.Facet = Backbone.Model.extend({
defaults: function() {
return {
_type: 'terms',
@@ -304,14 +545,42 @@ key used to specify this facet in the facet query):
other: 0,
missing: 0,
terms: []
- }
+ };
}
-}); | |
A Collection/List of Facets | my.FacetList = Backbone.Collection.extend({
+});
+
+
+
+ #
+
+ A Collection/List of Facets+
+ my.FacetList = Backbone.Collection.extend({
model: my.Facet
-}); | |
Backend registry- -Backends will register themselves by id into this registry | my.backends = {};
+});
+
+
+ my.backends = {};
}(jQuery, this.recline.Model));
- |
view-flot-graph.js | |
|---|---|
/*jshint multistr:true */
-
-this.recline = this.recline || {};
+
+
+
+
+ | |
Graph view for a Dataset using Flot graphing library.- +(function($, my) { + + + +
+
+
+ #
+
+ Graph view for a Dataset using Flot graphing library.Initialization arguments: -
NB: should not provide an el argument to the view but must let the view -generate the element itself (you can then append view.el to the DOM. | my.FlotGraph = Backbone.View.extend({
+generate the element itself (you can then append view.el to the DOM.
+
+ my.FlotGraph = Backbone.View.extend({
tagName: "div",
className: "data-graph-container",
@@ -79,16 +109,28 @@ generate the element itself (you can then append view.el to the DOM.
',
events: {
- 'change form select': 'onEditorSubmit'
- , 'click .editor-add': 'addSeries'
- , 'click .action-remove-series': 'removeSeries'
- , 'click .action-toggle-help': 'toggleHelp'
+ 'change form select': 'onEditorSubmit',
+ 'click .editor-add': 'addSeries',
+ 'click .action-remove-series': 'removeSeries',
+ 'click .action-toggle-help': 'toggleHelp'
},
initialize: function(options, config) {
var self = this;
this.el = $(this.el);
- _.bindAll(this, 'render', 'redraw'); |
| we need the model.fields to render properly | this.model.bind('change', this.render);
+ _.bindAll(this, 'render', 'redraw');
+
+
+
+ #
+
+ we need the model.fields to render properly +
+ this.model.bind('change', this.render);
this.model.fields.bind('reset', this.render);
this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
@@ -110,8 +152,32 @@ generate the element itself (you can then append view.el to the DOM.
render: function() {
htmls = $.mustache(this.template, this.model.toTemplateJSON());
- $(this.el).html(htmls); |
| now set a load of stuff up | this.$graph = this.el.find('.panel.graph'); |
| for use later when adding additional series -could be simpler just to have a common template! | this.$seriesClone = this.el.find('.editor-series').clone();
+ $(this.el).html(htmls);
+
+
+
+
+
+ #
+
+ now set a load of stuff up +
+
+ this.$graph = this.el.find('.panel.graph');
+
+
+
+ #
+
+ for use later when adding additional series +could be simpler just to have a common template! +
+ this.$seriesClone = this.el.find('.editor-series').clone();
this._updateSeries();
return this;
},
@@ -122,27 +188,71 @@ could be simpler just to have a common template! |
var series = this.$series.map(function () {
return $(this).val();
});
- this.chartConfig.series = $.makeArray(series)
+ this.chartConfig.series = $.makeArray(series);
this.chartConfig.group = this.el.find('.editor-group select').val();
- this.chartConfig.graphType = this.el.find('.editor-type select').val();
| update navigation | var qs = my.parseHashQueryString();
- qs['graph'] = JSON.stringify(this.chartConfig);
+ this.chartConfig.graphType = this.el.find('.editor-type select').val();
+
+
+
+ #
+
+ update navigation +
+ var qs = my.parseHashQueryString();
+ qs.graph = JSON.stringify(this.chartConfig);
my.setHashQueryString(qs);
this.redraw();
},
- redraw: function() { |
| There appear to be issues generating a Flot graph if either: | |
| var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
- if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
- return
+ redraw: function() {
+
+
+
+
+
+ #
+
+ There appear to be issues generating a Flot graph if either: +
+
+
+
+
+
+
+ #
+
+
Uncaught Invalid dimensions for plot, width = 0, height = 0 +* There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' +
+ var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
+ if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
+ return;
}
var series = this.createSeries();
var options = this.getGraphOptions(this.chartConfig.graphType);
this.plot = $.plot(this.$graph, series, options);
- this.setupTooltips(); |
| create this.plot and cache it + this.setupTooltips(); + + + +
+
+
+ #
+
+ create this.plot and cache it if (!this.plot) { this.plot = $.plot(this.$graph, series, options); } else { @@ -151,39 +261,90 @@ could be simpler just to have a common template! |
this.plot.resize();
this.plot.setupGrid();
this.plot.draw();
- } }, |
| needs to be function as can depend on state | getGraphOptions: function(typeId) {
- var self = this; |
| special tickformatter to show labels rather than numbers | var tickFormatter = function (val) {
+ }
+
+
+
+
+ },
+
+
+
+
+
+ #
+
+ needs to be function as can depend on state +
+
+ getGraphOptions: function(typeId) {
+ var self = this;
+
+
+
+ #
+
+ special tickformatter to show labels rather than numbers +
+ var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) {
- var out = self.model.currentDocuments.models[val].get(self.chartConfig.group); |
| if the value was in fact a number we want that not the | if (typeof(out) == 'number') {
+ var out = self.model.currentDocuments.models[val].get(self.chartConfig.group);
+
+
+
+ #
+
+ if the value was in fact a number we want that not the +
+ if (typeof(out) == 'number') {
return val;
} else {
return out;
}
}
return val;
- } |
| TODO: we should really use tickFormatter and 1 interval ticks if (and + }; + + + +
+
+
+ #
+
+ TODO: we should really use tickFormatter and 1 interval ticks if (and only if) x-axis values are non-numeric However, that is non-trivial to work out from a dataset (datasets may -have no field type info). Thus at present we only do this for bars. | var options = {
+have no field type info). Thus at present we only do this for bars.
+
+ var options = {
lines: {
series: {
lines: { show: true }
}
- }
- , points: {
+ },
+ points: {
series: {
points: { show: true }
},
grid: { hoverable: true, clickable: true }
- }
- , 'lines-and-points': {
+ },
+ 'lines-and-points': {
series: {
points: { show: true },
lines: { show: true }
},
grid: { hoverable: true, clickable: true }
- }
- , bars: {
+ },
+ bars: {
series: {
lines: {show: false},
bars: {
@@ -203,7 +364,7 @@ have no field type info). Thus at present we only do this for bars.
max: self.model.currentDocuments.length - 0.5
}
}
- }
+ };
return options[typeId];
},
@@ -230,7 +391,19 @@ have no field type info). Thus at present we only do this for bars.
$("#flot-tooltip").remove();
var x = item.datapoint[0];
- var y = item.datapoint[1]; |
| convert back from 'index' value on x-axis (e.g. in cases where non-number values) | if (self.model.currentDocuments.models[x]) {
+ var y = item.datapoint[1];
+
+
+
+ #
+
+ convert back from 'index' value on x-axis (e.g. in cases where non-number values) +
+ if (self.model.currentDocuments.models[x]) {
x = self.model.currentDocuments.models[x].get(self.chartConfig.group);
} else {
x = x.toFixed(2);
@@ -264,7 +437,19 @@ have no field type info). Thus at present we only do this for bars.
var y = doc.get(field);
if (typeof x === 'string') {
x = index;
- } |
| horizontal bar chart | if (self.chartConfig.graphType == 'bars') {
+ }
+
+
+
+ #
+
+ horizontal bar chart +
+ if (self.chartConfig.graphType == 'bars') {
points.push([y, x]);
} else {
points.push([x, y]);
@@ -274,12 +459,22 @@ have no field type info). Thus at present we only do this for bars.
});
}
return series;
- }, |
| Public: Adds a new empty series select box to the editor. - + }, + + + +
+
+
+ #
+
+ Public: Adds a new empty series select box to the editor. All but the first select box will have a remove button that allows them to be removed. - -Returns itself. | addSeries: function (e) {
+
+ addSeries: function (e) {
e.preventDefault();
var element = this.$seriesClone.clone(),
label = element.find('label'),
@@ -290,9 +485,20 @@ to be removed.
label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
label.find('span').text(String.fromCharCode(this.$series.length + 64));
return this;
- }, |
| Public: Removes a series list item from the editor. - -Also updates the labels of the remaining series elements. | removeSeries: function (e) {
+ },
+
+
+
+ #
+
+ Public: Removes a series list item from the editor. +Also updates the labels of the remaining series elements. +
+ removeSeries: function (e) {
e.preventDefault();
var $el = $(e.target);
$el.parent().parent().remove();
@@ -308,13 +514,29 @@ to be removed.
toggleHelp: function() {
this.el.find('.editor-info').toggleClass('editor-hide-info');
- }, |
| Private: Resets the series property to reference the select elements. - -Returns itself. | _updateSeries: function () {
+ },
+
+
+ _updateSeries: function () {
this.$series = this.el.find('.editor-series select');
}
});
})(jQuery, recline.View);
- |
view-grid.js | ||||
|---|---|---|---|---|
/*jshint multistr:true */
-
-this.recline = this.recline || {};
+
+
+
+
+ | ||||
DataGrid- +(function($, my) { + + + +
+
+
+ #
+
+ DataGridProvides a tabular view on a Dataset. - -Initialize it with a | my.DataGrid = Backbone.View.extend({
+
+ my.DataGrid = Backbone.View.extend({
tagName: "div",
className: "recline-grid-container",
@@ -23,11 +53,20 @@
},
events: {
- 'click .column-header-menu': 'onColumnHeaderClick'
- , 'click .row-header-menu': 'onRowHeaderClick'
- , 'click .root-header-menu': 'onRootHeaderClick'
- , 'click .data-table-menu li a': 'onMenuClick'
- }, | |||
| TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). + 'click .column-header-menu': 'onColumnHeaderClick', + 'click .row-header-menu': 'onRowHeaderClick', + 'click .root-header-menu': 'onRootHeaderClick', + 'click .data-table-menu li a': 'onMenuClick' + }, + + + +
+
+
+ #
+
+ TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). showDialog: function(template, data) { if (!data) data = {}; util.show('dialog'); @@ -36,8 +75,24 @@ showDialog: function(template, data) { util.hide('dialog'); }) $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); -}, | | |||
| ====================================================== -Column and row menus | onColumnHeaderClick: function(e) {
+},
+
+
+
+
+
+
+
+
+ onColumnHeaderClick: function(e) {
this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
},
@@ -58,31 +113,45 @@ Column and row menus | var self = this; e.preventDefault(); var actions = { - bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); }, facet: function() { self.model.queryState.addFacet(self.state.currentColumn); }, + facet_histogram: function() { + self.model.queryState.addHistogramFacet(self.state.currentColumn); + }, filter: function() { self.model.queryState.addTermFilter(self.state.currentColumn, ''); }, - transform: function() { self.showTransformDialog('transform') }, - sortAsc: function() { self.setColumnSort('asc') }, - sortDesc: function() { self.setColumnSort('desc') }, - hideColumn: function() { self.hideColumn() }, - showColumn: function() { self.showColumn(e) }, + transform: function() { self.showTransformDialog('transform'); }, + sortAsc: function() { self.setColumnSort('asc'); }, + sortDesc: function() { self.setColumnSort('desc'); }, + hideColumn: function() { self.hideColumn(); }, + showColumn: function() { self.showColumn(e); }, deleteRow: function() { - var doc = _.find(self.model.currentDocuments.models, function(doc) { | ||
| important this is == as the currentRow will be string (as comes -from DOM) while id may be int | return doc.id == self.state.currentRow
+ var doc = _.find(self.model.currentDocuments.models, function(doc) {
+
+
+
+ #
+
+ important this is == as the currentRow will be string (as comes +from DOM) while id may be int +
+ return doc.id == self.state.currentRow;
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
my.notify("Row deleted successfully");
- })
- .fail(function(err) {
- my.notify("Errorz! " + err)
- })
+ }).fail(function(err) {
+ my.notify("Errorz! " + err);
+ });
}
- }
+ };
actions[$(e.target).attr('data-action')]();
},
@@ -98,7 +167,7 @@ from DOM) while id may be int | $el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -112,7 +181,7 @@ from DOM) while id may be int | $el.append(view.el); util.observeExit($el, function() { util.hide('dialog'); - }) + }); $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, @@ -130,9 +199,21 @@ from DOM) while id may be int | showColumn: function(e) { this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column')); this.render(); - }, |
| ====================================================== - -Templating | template: ' \
+ },
+
+
+ template: ' \
<table class="recline-grid table-striped table-condensed" cellspacing="0"> \
<thead> \
<tr> \
@@ -151,7 +232,8 @@ from DOM) while id may be int | <div class="btn-group column-header-menu"> \ <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \ <ul class="dropdown-menu data-table-menu pull-right"> \ - <li><a data-action="facet" href="JavaScript:void(0);">Facet on this Field</a></li> \ + <li><a data-action="facet" href="JavaScript:void(0);">Term Facet</a></li> \ + <li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</a></li> \ <li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \ <li class="divider"></li> \ <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \ @@ -172,8 +254,20 @@ from DOM) while id may be int | ', toTemplateJSON: function() { - var modelData = this.model.toJSON() - modelData.notEmpty = ( this.fields.length > 0 ) | |
| TODO: move this sort of thing into a toTemplateJSON method on Dataset? | modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
+ var modelData = this.model.toJSON();
+ modelData.notEmpty = ( this.fields.length > 0 );
+
+
+
+ #
+
+ TODO: move this sort of thing into a toTemplateJSON method on Dataset? +
+ modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
return modelData;
},
render: function() {
@@ -193,24 +287,32 @@ from DOM) while id may be int | }); newView.render(); }); - this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); + this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0)); return this; } -}); | ||
DataGridRow View for rendering an individual document.- +}); + + + +
+
+
+ #
+
+ DataGridRow View for rendering an individual document.Since we want this to update in place it is up to creator to provider the element to attach to. -In addition you must pass in a FieldList in the constructor options. This should be list of fields for the DataGrid. -Example: -
var row = new DataGridRow({
model: dataset-document,
el: dom-element,
fields: mydatasets.fields // a FieldList object
});
- | my.DataGridRow = Backbone.View.extend({
+
+
+ my.DataGridRow = Backbone.View.extend({
initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
@@ -249,9 +351,9 @@ var row = new DataGridRow({
return {
field: field.id,
value: doc.getFieldValue(field)
- }
- })
- return { id: this.id, cells: cellData }
+ };
+ });
+ return { id: this.id, cells: cellData };
},
render: function() {
@@ -259,8 +361,21 @@ var row = new DataGridRow({
var html = $.mustache(this.template, this.toTemplateJSON());
$(this.el).html(html);
return this;
- }, | |||
| =================== -Cell Editor methods | onEditClick: function(e) {
+ },
+
+
+ onEditClick: function(e) {
var editing = this.el.find('.data-table-cell-editor-editor');
if (editing.length > 0) {
editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
@@ -299,4 +414,9 @@ Cell Editor methods | })(jQuery, recline.View); - |
jshint multistr:true
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {This view allows to plot gereferenced documents on a map. The location +information can be provided either via a field with +GeoJSON objects or two fields with latitude and +longitude coordinates.
+Initialization arguments:
+options: initial options. They must contain a model:
+{ + model: {recline.Model.Dataset} + }
+config: (optional) map configuration hash (not yet used)
+my.Map = Backbone.View.extend({
+
+ tagName: 'div',
+ className: 'data-map-container',
+
+ template: ' \
+ <div class="editor"> \
+ <form class="form-stacked"> \
+ <div class="clearfix"> \
+ <div class="editor-field-type"> \
+ <label class="radio"> \
+ <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
+ Latitude / Longitude fields</label> \
+ <label class="radio"> \
+ <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
+ GeoJSON field</label> \
+ </div> \
+ <div class="editor-field-type-latlon"> \
+ <label>Latitude field</label> \
+ <div class="input editor-lat-field"> \
+ <select> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ <label>Longitude field</label> \
+ <div class="input editor-lon-field"> \
+ <select> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ </div> \
+ <div class="editor-field-type-geom" style="display:none"> \
+ <label>Geometry field (GeoJSON)</label> \
+ <div class="input editor-geom-field"> \
+ <select> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ </div> \
+ </div> \
+ <div class="editor-buttons"> \
+ <button class="btn editor-update-map">Update</button> \
+ </div> \
+ <input type="hidden" class="editor-id" value="map-1" /> \
+ </div> \
+ </form> \
+ </div> \
+<div class="panel map"> \
+</div> \
+',These are the default field names that will be used if found. +If not found, the user will need to define the fields via the editor.
+ latitudeFieldNames: ['lat','latitude'],
+ longitudeFieldNames: ['lon','longitude'],
+ geometryFieldNames: ['geom','the_geom','geometry','spatial','location'],Define here events for UI elements
+ events: {
+ 'click .editor-update-map': 'onEditorSubmit',
+ 'change .editor-field-type': 'onFieldTypeChange'
+ },
+
+
+ initialize: function(options, config) {
+ var self = this;
+
+ this.el = $(this.el);Listen to changes in the fields
+ this.model.bind('change', function() {
+ self._setupGeometryField();
+ });
+ this.model.fields.bind('add', this.render);
+ this.model.fields.bind('reset', function(){
+ self._setupGeometryField()
+ self.render()
+ });Listen to changes in the documents
+ this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
+ this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
+ this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});If the div was hidden, Leaflet needs to recalculate some sizes +to display properly
+ this.bind('view:show',function(){
+ self.map.invalidateSize();
+ });
+
+ this.mapReady = false;
+
+ this.render();
+ },Public: Adds the necessary elements to the page.
+Also sets up the editor fields and the map if necessary.
+ render: function() {
+
+ var self = this;
+
+ htmls = $.mustache(this.template, this.model.toTemplateJSON());
+
+ $(this.el).html(htmls);
+ this.$map = this.el.find('.panel.map');
+
+ if (this.geomReady && this.model.fields.length){
+ if (this._geomFieldName){
+ this._selectOption('editor-geom-field',this._geomFieldName);
+ $('#editor-field-type-geom').attr('checked','checked').change();
+ } else{
+ this._selectOption('editor-lon-field',this._lonFieldName);
+ this._selectOption('editor-lat-field',this._latFieldName);
+ $('#editor-field-type-latlon').attr('checked','checked').change();
+ }
+ }
+
+ this.model.bind('query:done', function() {
+ if (!self.geomReady){
+ self._setupGeometryField();
+ }
+
+ if (!self.mapReady){
+ self._setupMap();
+ }
+ self.redraw();
+ });
+
+ return this;
+ },Public: Redraws the features on the map according to the action provided
+Actions can be:
+ redraw: function(action,doc){
+
+ var self = this;
+
+ action = action || 'refresh';
+
+ if (this.geomReady && this.mapReady){
+ if (action == 'reset'){
+ this.features.clearLayers();
+ } else if (action == 'add' && doc){
+ this._add(doc);
+ } else if (action == 'remove' && doc){
+ this._remove(doc);
+ } else if (action == 'refresh'){
+ this.features.clearLayers();
+ this._add(this.model.currentDocuments.models);
+ }
+ }
+ },UI Event handlers
+Public: Update map with user options
+Right now the only configurable option is what field(s) contains the +location information.
+ onEditorSubmit: function(e){
+ e.preventDefault();
+ if ($('#editor-field-type-geom').attr('checked')){
+ this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
+ this._latFieldName = this._lonFieldName = false;
+ } else {
+ this._geomFieldName = false;
+ this._latFieldName = $('.editor-lat-field > select > option:selected').val();
+ this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
+ }
+ this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ this.redraw();
+
+ return false;
+ },Public: Shows the relevant select lists depending on the location field +type selected.
+ onFieldTypeChange: function(e){
+ if (e.target.value == 'geom'){
+ $('.editor-field-type-geom').show();
+ $('.editor-field-type-latlon').hide();
+ } else {
+ $('.editor-field-type-geom').hide();
+ $('.editor-field-type-latlon').show();
+ }
+ },Private: Add one or n features to the map
+For each document passed, a GeoJSON geometry will be extracted and added +to the features layer. If an exception is thrown, the process will be +stopped and an error notification shown.
+Each feature will have a popup associated with all the document fields.
+ _add: function(doc){
+
+ var self = this;
+
+ if (!(doc instanceof Array)) doc = [doc];
+
+ doc.forEach(function(doc){
+ var feature = self._getGeometryFromDocument(doc);
+ if (feature instanceof Object){Build popup contents +TODO: mustache?
+ html = ''
+ for (key in doc.attributes){
+ html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
+ }
+ feature.properties = {popupContent: html};Add a reference to the model id, which will allow us to +link this Leaflet layer to a Recline doc
+ feature.properties.cid = doc.cid;
+
+ try {
+ self.features.addGeoJSON(feature);
+ } catch (except) {
+ var msg = 'Wrong geometry value';
+ if (except.message) msg += ' (' + except.message + ')';
+ my.notify(msg,{category:'error'});
+ _.breakLoop();
+ }
+ } else {
+ my.notify('Wrong geometry value',{category:'error'});
+ _.breakLoop();
+ }
+ });
+ },Private: Remove one or n features to the map
+ _remove: function(doc){
+
+ var self = this;
+
+ if (!(doc instanceof Array)) doc = [doc];
+
+ doc.forEach(function(doc){
+ for (key in self.features._layers){
+ if (self.features._layers[key].cid == doc.cid){
+ self.features.removeLayer(self.features._layers[key]);
+ }
+ }
+ });
+
+ },Private: Return a GeoJSON geomtry extracted from the document fields
+ _getGeometryFromDocument: function(doc){
+ if (this.geomReady){
+ if (this._geomFieldName){We assume that the contents of the field are a valid GeoJSON object
+ return doc.attributes[this._geomFieldName];
+ } else if (this._lonFieldName && this._latFieldName){We'll create a GeoJSON like point object from the two lat/lon fields
+ return {
+ type: 'Point',
+ coordinates: [
+ doc.attributes[this._lonFieldName],
+ doc.attributes[this._latFieldName]
+ ]
+ };
+ }
+ return null;
+ }
+ },Private: Check if there is a field with GeoJSON geometries or alternatively, +two fields with lat/lon values.
+If not found, the user can define them via the UI form.
+ _setupGeometryField: function(){
+ var geomField, latField, lonField;
+
+ this._geomFieldName = this._checkField(this.geometryFieldNames);
+ this._latFieldName = this._checkField(this.latitudeFieldNames);
+ this._lonFieldName = this._checkField(this.longitudeFieldNames);
+
+ this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ },Private: Check if a field in the current model exists in the provided +list of names.
+ _checkField: function(fieldNames){
+ var field;
+ var modelFieldNames = this.model.fields.pluck('id');
+ for (var i = 0; i < fieldNames.length; i++){
+ for (var j = 0; j < modelFieldNames.length; j++){
+ if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
+ return modelFieldNames[j];
+ }
+ }
+ return null;
+ },Private: Sets up the Leaflet map control and the features layer.
+The map uses a base layer from MapQuest based +on OpenStreetMap.
+ _setupMap: function(){
+
+ this.map = new L.Map(this.$map.get(0));
+
+ var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+ var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
+ var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
+ this.map.addLayer(bg);
+
+ this.features = new L.GeoJSON();
+ this.features.on('featureparse', function (e) {
+ if (e.properties && e.properties.popupContent){
+ e.layer.bindPopup(e.properties.popupContent);
+ }
+ if (e.properties && e.properties.cid){
+ e.layer.cid = e.properties.cid;
+ }
+
+ });
+ this.map.addLayer(this.features);
+
+ this.map.setView(new L.LatLng(0, 0), 2);
+
+ this.mapReady = true;
+ },Private: Helper function to select an option from a select list
+ _selectOption: function(id,value){
+ var options = $('.' + id + ' > select > option');
+ if (options){
+ options.each(function(opt){
+ if (this.value == value) {
+ $(this).attr('selected','selected');
+ return false;
+ }
+ });
+ }
+ }
+
+ });
+
+})(jQuery, recline.View);
+
+ view.js | ||
|---|---|---|
/*jshint multistr:true */
-this.recline = this.recline || {};
+
+
+
+
+ | ||
DataExplorer- +(function($, my) { + + + +
+
+
+ #
+
+ DataExplorerThe primary view for the entire application. Usage: -
var myExplorer = new model.recline.DataExplorer({
model: {{recline.Model.Dataset instance}}
@@ -13,18 +41,14 @@ var myExplorer = new model.recline.DataExplorer({
views: {{page views}}
config: {{config options -- see below}}
});
-
+
Parameters-model: (required) Dataset instance. -el: (required) DOM element. -views: (optional) the views (Grid, Graph etc) for DataExplorer to show. This is an array of view hashes. If not provided just initialize a DataGrid with id 'grid'. Example: -
var views = [
{
@@ -45,14 +69,15 @@ var views = [
config: Config options like: -
NB: the element already being in the DOM is important for rendering of -FlotGraph subview. | my.DataExplorer = Backbone.View.extend({
+FlotGraph subview.
+
+ my.DataExplorer = Backbone.View.extend({
template: ' \
<div class="recline-data-explorer"> \
<div class="alert-messages"></div> \
@@ -95,7 +120,19 @@ FlotGraph subview. | options.config); if (this.config.readOnly) { this.setReadOnly(); - } |
| Hash of 'page' views (i.e. those for whole page) keyed by page name | if (options.views) {
+ }
+
+
+
+ #
+
+ Hash of 'page' views (i.e. those for whole page) keyed by page name +
+ if (options.views) {
this.pageViews = options.views;
} else {
this.pageViews = [{
@@ -105,7 +142,19 @@ FlotGraph subview. | model: this.model }) }]; - } |
| this must be called after pageViews are created | this.render();
+ }
+
+
+
+ #
+
+ this must be called after pageViews are created +
+ this.render();
this.router = new Backbone.Router();
this.setupRouting();
@@ -116,8 +165,20 @@ FlotGraph subview. | this.model.bind('query:done', function() { my.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'}); |
| update navigation | var qs = my.parseHashQueryString();
- qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON());
+ my.notify('Data loaded', {category: 'success'});
+
+
+
+ #
+
+ update navigation +
+ var qs = my.parseHashQueryString();
+ qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
var out = my.getNewHashForQueryString(qs);
self.router.navigate(out);
});
@@ -137,8 +198,20 @@ FlotGraph subview. | msg = 'There was an error querying the backend'; } my.notify(msg, {category: 'error', persist: true}); - }); |
| retrieve basic data like fields etc -note this.model and dataset returned are the same | this.model.fetch()
+ });
+
+
+
+ #
+
+ retrieve basic data like fields etc +note this.model and dataset returned are the same +
+ this.model.fetch()
.done(function(dataset) {
var queryState = my.parseHashQueryString().reclineQuery;
if (queryState) {
@@ -163,7 +236,7 @@ note this.model and dataset returned are the same |
$(this.el).html(template);
var $dataViewContainer = this.el.find('.data-view-container');
_.each(this.pageViews, function(view, pageName) {
- $dataViewContainer.append(view.view.el)
+ $dataViewContainer.append(view.view.el);
});
var queryEditor = new my.QueryEditor({
model: this.model.queryState
@@ -182,7 +255,19 @@ note this.model and dataset returned are the same
},
setupRouting: function() {
- var self = this; |
| Default route | this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
+ var self = this;
+
+
+
+ #
+
+ Default route +
+ this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
self.updateNav(self.pageViews[0].id, queryString);
});
$.each(this.pageViews, function(idx, view) {
@@ -197,11 +282,25 @@ note this.model and dataset returned are the same |
this.el.find('.navigation li a').removeClass('disabled');
var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
$el.parent().addClass('active');
- $el.addClass('disabled'); |
| show the specific page | _.each(this.pageViews, function(view, idx) {
+ $el.addClass('disabled');
+
+
+
+ #
+
+ show the specific page +
+ _.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
view.view.el.show();
+ view.view.trigger('view:show');
} else {
view.view.el.hide();
+ view.view.trigger('view:hide');
}
});
},
@@ -237,8 +336,8 @@ note this.model and dataset returned are the same |
',
events: {
- 'submit form': 'onFormSubmit'
- , 'click .action-pagination-update': 'onPaginationUpdate'
+ 'submit form': 'onFormSubmit',
+ 'click .action-pagination-update': 'onPaginationUpdate'
},
initialize: function() {
@@ -257,10 +356,11 @@ note this.model and dataset returned are the same
onPaginationUpdate: function(e) {
e.preventDefault();
var $el = $(e.target);
+ var newFrom = 0;
if ($el.parent().hasClass('prev')) {
- var newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
+ newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
} else {
- var newFrom = this.model.get('from') + this.model.get('size');
+ newFrom = this.model.get('from') + this.model.get('size');
}
this.model.set({from: newFrom});
},
@@ -317,7 +417,19 @@ note this.model and dataset returned are the same
this.render();
},
render: function() {
- var tmplData = $.extend(true, {}, this.model.toJSON()); |
| we will use idx in list as there id ... | tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+ var tmplData = $.extend(true, {}, this.model.toJSON());
+
+
+
+ #
+
+ we will use idx in list as there id ... +
+ tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
filter.id = idx;
return filter;
});
@@ -331,10 +443,22 @@ note this.model and dataset returned are the same |
fieldId: fieldId,
label: fieldId,
value: filter.term[fieldId]
- }
+ };
});
var out = $.mustache(this.template, tmplData);
- this.el.html(out); |
| are there actually any facets to show? | if (this.model.get('filters').length > 0) {
+ this.el.html(out);
+
+
+
+ #
+
+ are there actually any facets to show? +
+ if (this.model.get('filters').length > 0) {
this.el.show();
} else {
this.el.hide();
@@ -382,6 +506,9 @@ note this.model and dataset returned are the same |
{{#terms}} \
<li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
{{/terms}} \
+ {{#entries}} \
+ <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
+ {{/entries}} \
</ul> \
</div> \
{{/facets}} \
@@ -404,8 +531,29 @@ note this.model and dataset returned are the same
facets: this.model.facets.toJSON(),
fields: this.model.fields.toJSON()
};
+ tmplData.facets = _.map(tmplData.facets, function(facet) {
+ if (facet._type === 'date_histogram') {
+ facet.entries = _.map(facet.entries, function(entry) {
+ entry.term = new Date(entry.time).toDateString();
+ return entry;
+ });
+ }
+ return facet;
+ });
var templated = $.mustache(this.template, tmplData);
- this.el.html(templated); |
| are there actually any facets to show? | if (this.model.facets.length > 0) {
+ this.el.html(templated);
+
+
+
+ #
+
+ are there actually any facets to show?+
+ if (this.model.facets.length > 0) {
this.el.show();
} else {
this.el.hide();
@@ -421,19 +569,53 @@ note this.model and dataset returned are the same |
var value = $target.attr('data-value');
this.model.queryState.addTermFilter(fieldId, value);
}
-});
-
-/* ========================================================== */ |
Miscellaneous Utilities | var urlPathRegex = /^([^?]+)(\?.*)?/; | |
| Parse the Hash section of a URL into path and query string | my.parseHashUrl = function(hashUrl) {
+});
+
+
+
+
+
+ #
+
+ Miscellaneous Utilities+
+
+ var urlPathRegex = /^([^?]+)(\?.*)?/;
+
+
+
+ #
+
+ Parse the Hash section of a URL into path and query string +
+ my.parseHashUrl = function(hashUrl) {
var parsed = urlPathRegex.exec(hashUrl);
- if (parsed == null) {
+ if (parsed === null) {
return {};
} else {
return {
path: parsed[1],
query: parsed[2] || ''
- }
+ };
}
-} | |
| Parse a URL query string (?xyz=abc...) into a dictionary. | my.parseQueryString = function(q) {
+};
+
+
+
+ #
+
+ Parse a URL query string (?xyz=abc...) into a dictionary. +
+ my.parseQueryString = function(q) {
if (!q) {
return {};
}
@@ -446,13 +628,49 @@ note this.model and dataset returned are the same |
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
- while (e = r.exec(q)) { |
| TODO: have values be array as query string allow repetition of keys | urlParams[d(e[1])] = d(e[2]);
+ while (e = r.exec(q)) {
+
+
+
+ #
+
+ TODO: have values be array as query string allow repetition of keys +
+ urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
-} | |
| Parse the query string out of the URL hash | my.parseHashQueryString = function() {
+};
+
+
+
+ #
+
+ Parse the query string out of the URL hash +
+ my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
-} | |
| Compse a Query String | my.composeQueryString = function(queryParams) {
+};
+
+
+
+ #
+
+ Compse a Query String +
+ my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
@@ -460,35 +678,57 @@ note this.model and dataset returned are the same |
});
queryString += items.join('&');
return queryString;
-}
+};
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
- if (window.location.hash) { |
| slice(1) to remove # at start | return window.location.hash.split('?')[0].slice(1) + queryPart;
+ if (window.location.hash) {
+
+
+
+ #
+
+ slice(1) to remove # at start +
+ return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
-}
+};
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
-} | |
notify- +}; + + + +
+
+
+ #
+
+ notifyCreate a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are: -
| my.notify = function(message, options) {
- if (!options) var options = {};
+
+
+ my.notify = function(message, options) {
+ if (!options) options = {};
var tmplData = _.extend({
msg: message,
category: 'warning'
},
options);
var _template = ' \
- <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
+ <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">Ã</a> \
{{msg}} \
{{#loader}} \
<span class="notification-loader"> </span> \
@@ -503,13 +743,29 @@ note this.model and dataset returned are the same |
});
}, 1000);
}
-} |
clearNotifications- -Clear all existing notifications | my.clearNotifications = function() {
+};
+
+
+ my.clearNotifications = function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.remove();
-}
+};
})(jQuery, recline.View);
- |
There are additional views which do not display a whole dataset but which @@ -272,6 +272,7 @@ are useful:
';
var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
this.map.addLayer(bg);
- // Layer to hold the features
this.features = new L.GeoJSON();
this.features.on('featureparse', function (e) {
if (e.properties && e.properties.popupContent){
e.layer.bindPopup(e.properties.popupContent);
}
+ if (e.properties && e.properties.cid){
+ e.layer.cid = e.properties.cid;
+ }
+
});
this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2);
this.mapReady = true;
+ },
+
+ // Private: Helper function to select an option from a select list
+ //
+ _selectOption: function(id,value){
+ var options = $('.' + id + ' > select > option');
+ if (options){
+ options.each(function(opt){
+ if (this.value == value) {
+ $(this).attr('selected','selected');
+ return false;
+ }
+ });
+ }
}
});
diff --git a/src/view.js b/src/view.js
index d9369f49..62e110ab 100644
--- a/src/view.js
+++ b/src/view.js
@@ -213,8 +213,10 @@ my.DataExplorer = Backbone.View.extend({
_.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
view.view.el.show();
+ view.view.trigger('view:show');
} else {
view.view.el.hide();
+ view.view.trigger('view:hide');
}
});
},