this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
-
-(function(my) {
- "use strict";
- my.__type__ = 'dataproxy';diff --git a/.gitignore b/.gitignore deleted file mode 100755 index fb371404..00000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -sandbox/* -.*.swp -.*.swo -_site/* -node_modules/* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 227a3756..00000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "0.10" -script: phantomjs test/qunit/runner.js test/index.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 97df3fc4..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Contributing to Recline - -We welcome patches and pull requests. There are a few guidelines. - -## General - -* Please run the tests :-) (see below) -* Please do **not** build the dist files (e.g. dist/recline.js) when submitting - patches. dist files will get built automatically and if they are part of a - patch or pull request it makes them harder to review and more likely to - conflict. -* If possible have an issue to which the commits can relate. You can reference - an issue in the commits by just including #{issue-number} somewhere in the - commit message). Note if no issue exists suggest creating one. - -## For larger changes - -* Cleanup your code and affected code parts -* Run the tests from `/test/index.html` in different browsers (at least Chrome and FF) -* Update the documentation and tutorials where necessary -* Update `/_includes/recline-deps.html` if you change required files (e.g. leaflet libraries) -* Try to build the demos in `/demos/` with jekyll and then check out the `/demos/multiview/` which utilizes most aspects of Recline - -You will also probably want to take a quick look at outline of the architecture which can be found in the [documentation online](http://okfnlabs.org/recline). - -## Running tests - -Run the tests by opening `test/index.html` in your browser. - -## Demos and Documentation - -Note that the demos and documentation utilize the [jekyll templating -system][jekyll] and to use them *locally* you will need to build them using -jekyll. Once installed, all you need to do from the command line is run jekyll: - - jekyll serve - -or if you're actively developing and want auto-reloading: - - jekyll serve --watch - -[jekyll]: https://github.com/mojombo/jekyll - diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100755 index a0993c64..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2011-2014 Max Ogden, Rufus Pollock and Contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100755 index aa062840..00000000 --- a/README.md +++ /dev/null @@ -1,148 +0,0 @@ -[](https://travis-ci.org/datopian/recline) - -A simple but powerful library for building data applications in pure Javascript and HTML. - -
' + JSON.stringify(simple, null, 2) + ''); - diff --git a/_includes/tutorial-basics-ex-2.js b/_includes/tutorial-basics-ex-2.js deleted file mode 100644 index 7e22739d..00000000 --- a/_includes/tutorial-basics-ex-2.js +++ /dev/null @@ -1,19 +0,0 @@ -// must have a div with class="ex-1" -var $el = $('.ex-2'); - -// query with text 'UK' - this will attempt to match any field for UK -// Also limit the query to return a maximum of 2 results using the size attribute - -// query function has asynchronous behaviour and returns a promise object -// (even for the case of in memory data where querying in fact happens synchronously) -// On completion the display function will be called -dataset.query({q: 'UK', size: 2}).done(function() { - $('.ex-2').append('Total found: ' + dataset.recordCount); - $('.ex-2').append(' Total returned: ' + dataset.records.length); - $('.ex-2').append( - $('').html( - JSON.stringify(dataset.records.toJSON(), null, 2) - ) - ); -}); - diff --git a/_includes/tutorial-basics-ex-events.js b/_includes/tutorial-basics-ex-events.js deleted file mode 100644 index 0f50bf15..00000000 --- a/_includes/tutorial-basics-ex-events.js +++ /dev/null @@ -1,13 +0,0 @@ -function onChange() { - $('.ex-events').append('Queried: ' + dataset.queryState.get('q') + '. Records matching: ' + dataset.recordCount); - $('.ex-events').append('
Recline.js is freely redistributable under the terms of the MIT license.
-See the integrated multiview in action on a - sample dataset - the multiview incorporates most the main views - (grid, graph, map etc) into one integrated view with search, filtering - and field summaries.
-The Data Explorer is a full - application for exploring and working with data in your browser. It - utilizes almost every feature of ReclineJS and is the app that ReclineJS - was originally developed to build.
-Create timelines quickly and easily using the Timeliner App created with ReclineJS (source code)
-See Recline's geo filter and map view capabilities put to good use - exploring crime's in San Francisco. This example shows you the Recline - Data Explorer pre-configured to show thefts near 9th and Mission.
-See how easy it is to build a responsive AJAX-based search interface - to a search backend. It includes full-text search, faceting and ability - to easily customize the display of results.
-{{description}}
\ -${{price}}
This demo shows how Recline can be used to build a search app. It includes faceting as well as search. You can find the source javascript here (plus prettified version of source for readability) – please feel free to reuse!
-The default setup uses local example data but you can also connect directly to any other backend supported by Recline, for example SOLR, ElasticSearch or even a google docs spreadsheet. Here's an example running against a GDocs spreadsheet (Oil spills in the Niger Delta).
-There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
\ -Please tell us by using the menu on the right and a graph will automatically appear.
\ -| \ - {{label}} \ - | \ - {{/fields}} \ -\ - |
|---|
-// var row = new GridRow({
-// model: dataset-record,
-// el: dom-element,
-// fields: mydatasets.fields // a FieldList object
-// });
-//
-my.GridRow = Backbone.View.extend({
- initialize: function(initData) {
- _.bindAll(this, 'render');
- this._fields = initData.fields;
- this.listenTo(this.model, 'change', this.render);
- },
-
- template: ' \
- {{#cells}} \
-
-// {
-// // geomField if specified will be used in preference to lat/lon
-// geomField: {id of field containing geometry in the dataset}
-// lonField: {id of field containing longitude in the dataset}
-// latField: {id of field containing latitude in the dataset}
-// autoZoom: true,
-// // use cluster support
-// // cluster: true = always on
-// // cluster: false = always off
-// cluster: false
-// }
-//
-//
-// Useful attributes to know about (if e.g. customizing)
-//
-// * map: the Leaflet map (L.Map)
-// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON)
-my.Map = Backbone.View.extend({
- template: ' \
-
-// var myExplorer = new recline.View.MultiView({
-// model: {{recline.Model.Dataset instance}}
-// el: {{an existing dom element}}
-// views: {{dataset views}}
-// state: {{state configuration -- see below}}
-// });
-//
-//
-// ### Parameters
-//
-// **model**: (required) recline.model.Dataset instance.
-//
-// **el**: (required) DOM element to bind to. NB: the element already
-// being in the DOM is important for rendering of some subviews (e.g.
-// Graph).
-//
-// **views**: (optional) the dataset views (Grid, Graph etc) for
-// MultiView to show. This is an array of view hashes. If not provided
-// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
-// and labels!).
-//
-//
-// var views = [
-// {
-// id: 'grid', // used for routing
-// label: 'Grid', // used for view switcher
-// view: new recline.View.Grid({
-// model: dataset
-// })
-// },
-// {
-// id: 'graph',
-// label: 'Graph',
-// view: new recline.View.Graph({
-// model: dataset
-// })
-// }
-// ];
-//
-//
-// **sidebarViews**: (optional) the sidebar views (Filters, Fields) for
-// MultiView to show. This is an array of view hashes. If not provided
-// initialize with (recline.View.)FilterEditor and Fields views (with obvious
-// id and labels!).
-//
-//
-// var sidebarViews = [
-// {
-// id: 'filterEditor', // used for routing
-// label: 'Filters', // used for view switcher
-// view: new recline.View.FilterEditor({
-// model: dataset
-// })
-// },
-// {
-// id: 'fieldsView',
-// label: 'Fields',
-// view: new recline.View.Fields({
-// model: dataset
-// })
-// }
-// ];
-//
-//
-// **state**: standard state config for this view. This state is slightly
-// special as it includes config of many of the subviews.
-//
-//
-// var state = {
-// query: {dataset query state - see dataset.queryState object}
-// 'view-{id1}': {view-state for this view}
-// 'view-{id2}': {view-state for }
-// ...
-// // Explorer
-// currentView: id of current view (defaults to first view if not specified)
-// readOnly: (default: false) run in read-only mode
-// }
-//
-//
-// Note that at present we do *not* serialize information about the actual set
-// of views in use -- e.g. those specified by the views argument -- but instead
-// expect either that the default views are fine or that the client to have
-// initialized the MultiView with the relevant views themselves.
-my.MultiView = Backbone.View.extend({
- template: ' \
- There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
Please tell us by using the menu on the right and a graph will automatically appear.
| {{label}} | {{/fields}}
|---|
Building on Backbone, Recline - supplies components and structure to data-heavy applications by providing a - set of models (Dataset, Record/Row, Field) and views (Grid, Map, Graph - etc).
- - -Models help you structure your work with data by providing some standard objects such as Dataset and Record – a Dataset being a collection of Records. More »
-Backends connect your Models to data sources (and stores) – for example Google Docs spreadsheets, local CSV files, the DataHub, ElasticSearch etc. More »
-Views are user interface components for displaying, editing or interacting with the data. For example, maps, graphs, data grids or a query editor. More »
-
-{
- q: 'quick brown fox',
- filters: [
- { term: { 'owner': 'jones' } }
- ]
-}
-
-
-
- backend.csv.js | |
|---|---|
this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.CSV = this.recline.Backend.CSV || {}; | |
| Note that provision of jQuery is optional (it is only needed if you use fetch on a remote file) | (function(my) {
- "use strict";
- my.__type__ = 'csv'; |
| use either jQuery or Underscore Deferred depending on what is available | var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; |
fetch- -fetch supports 3 options depending on the attribute provided on the dataset argument - -
All options generates similar data and use the memory store outcome, that is they return something like: - -
-{
- records: [ [...], [...], ... ],
- metadata: { may be some metadata e.g. file name }
- useMemoryStore: true
-}
- | my.fetch = function(dataset) {
- var dfd = new Deferred();
- if (dataset.file) {
- var reader = new FileReader();
- var encoding = dataset.encoding || 'UTF-8';
- reader.onload = function(e) {
- var out = my.extractFields(my.parseCSV(e.target.result, dataset), dataset);
- out.useMemoryStore = true;
- out.metadata = {
- filename: dataset.file.name
- }
- dfd.resolve(out);
- };
- reader.onerror = function (e) {
- alert('Failed to load file. Code: ' + e.target.error.code);
- };
- reader.readAsText(dataset.file, encoding);
- } else if (dataset.data) {
- var out = my.extractFields(my.parseCSV(dataset.data, dataset), dataset);
- out.useMemoryStore = true;
- dfd.resolve(out);
- } else if (dataset.url) {
- jQuery.get(dataset.url).done(function(data) {
- var out = my.extractFields(my.parseCSV(data, dataset), dataset);
- out.useMemoryStore = true;
- dfd.resolve(out);
- });
- }
- return dfd.promise();
- }; |
| Convert array of rows in { records: [ ...] , fields: [ ... ] } -@param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data. | my.extractFields = function(rows, noFields) {
- if (noFields.noHeaderRow !== true && rows.length > 0) {
- return {
- fields: rows[0],
- records: rows.slice(1)
- }
- } else {
- return {
- records: rows
- }
- }
- }; |
parseCSV- -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 {Object} options Options for loading CSV including - @param {Boolean} [trim=false] If set to True leading and trailing - whitespace is stripped off of each non-quoted field as it is imported - @param {String} [delimiter=','] A one-character string used to separate - fields. It defaults to ',' - @param {String} [quotechar='"'] A one-character string used to quote - fields containing special characters, such as the delimiter or - quotechar, or which contain new-line characters. It defaults to '"' - -@param {Integer} skipInitialRows A integer number of rows to skip (default 0) - -Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/ | my.parseCSV= function(s, options) { |
| Get rid of any trailing \n | s = chomp(s);
-
- var options = options || {};
- var trm = (options.trim === false) ? false : true;
- var delimiter = options.delimiter || ',';
- var quotechar = options.quotechar || '"';
-
- var cur = '', // The character we are currently processing.
- inQuote = false,
- fieldQuoted = false,
- field = '', // Buffer for building up the current field
- row = [],
- out = [],
- i,
- processField;
-
- processField = function (field) {
- 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)) {
- field = parseInt(field, 10);
- } else if (rxIsFloat.test(field)) {
- field = parseFloat(field, 10);
- }
- }
- return field;
- };
-
- for (i = 0; i < s.length; i += 1) {
- cur = s.charAt(i); |
| If we are at a EOF or EOR | if (inQuote === false && (cur === delimiter || 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 = '';
- fieldQuoted = false;
- } else { |
| If it's not a quotechar, add it to the field buffer | if (cur !== quotechar) {
- field += cur;
- } else {
- if (!inQuote) { |
| We are not in a quote, start a quote | inQuote = true;
- fieldQuoted = true;
- } else { |
| Next char is quotechar, this is an escaped quotechar | if (s.charAt(i + 1) === quotechar) {
- field += quotechar; |
| Skip the next char | i += 1;
- } else { |
| It's not escaping, so end quote | inQuote = false;
- }
- }
- }
- }
- } |
| Add the last field | field = processField(field);
- row.push(field);
- out.push(row); |
| Expose the ability to discard initial rows | if (options.skipInitialRows) out = out.slice(options.skipInitialRows);
-
- return out;
- }; |
serializeCSV- -Convert an Object or a simple array of arrays into a Comma -Separated Values string. - -Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers. - -@return The array serialized as a CSV -@type String - -@param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows: - -
-
-@param {object} options Options for serializing the CSV file including - delimiter and quotechar (see parseCSV options parameter above for - details on these). - -Heavily based on uselesscode's JS CSV serializer (MIT Licensed): -http://www.uselesscode.org/javascript/csv/ | my.serializeCSV= function(dataToSerialize, options) {
- var a = null;
- if (dataToSerialize instanceof Array) {
- a = dataToSerialize;
- } else {
- a = [];
- var fieldNames = _.pluck(dataToSerialize.fields, 'id');
- a.push(fieldNames);
- _.each(dataToSerialize.records, function(record, index) {
- var tmp = _.map(fieldNames, function(fn) {
- return record[fn];
- });
- a.push(tmp);
- });
- }
- var options = options || {};
- var delimiter = options.delimiter || ',';
- var quotechar = options.quotechar || '"';
-
- var cur = '', // The character we are currently processing.
- field = '', // Buffer for building up the current field
- row = '',
- out = '',
- i,
- j,
- processField;
-
- processField = function (field) {
- if (field === null) { |
| If field is null set to empty string | field = '';
- } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { |
| Convert string to delimited string | field = quotechar + field + quotechar;
- } else if (typeof field === "number") { |
| Convert number to string | field = field.toString(10);
- }
-
- return field;
- };
-
- for (i = 0; i < a.length; i += 1) {
- cur = a[i];
-
- for (j = 0; j < cur.length; j += 1) {
- field = processField(cur[j]); |
| If this is EOR append row to output and flush row | if (j === (cur.length - 1)) {
- row += field;
- out += row + "\n";
- row = '';
- } else { |
| Add the current field to the current row | row += field + delimiter;
- } |
| Flush the field buffer | field = '';
- }
- }
-
- return out;
- };
-
- var rxIsInt = /^\d+$/,
- 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) {
- return function (s) {
- return s.trim();
- };
- } else {
- return function (s) {
- return s.replace(/^\s*/, '').replace(/\s*$/, '');
- };
- }
- }());
-
- 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);
- }
- }
-
-
-}(this.recline.Backend.CSV));
-
- |
this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
-
-(function(my) {
- "use strict";
- my.__type__ = 'dataproxy';URL for the dataproxy
- - my.dataproxy_url = '//jsonpdataproxy.appspot.com';Timeout for dataproxy (after this time if no response we error) -Needed because use JSONP so do not receive e.g. 500 errors
- - my.timeout = 5000;use either jQuery or Underscore Deferred depending on what is available
- - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;Load data from a URL via the DataProxy.
-Returns array of field names and array of arrays for records
- - my.fetch = function(dataset) {
- var data = {
- url: dataset.url,
- 'max-results': dataset.size || dataset.rows || 1000,
- type: dataset.format || ''
- };
- var jqxhr = jQuery.ajax({
- url: my.dataproxy_url,
- data: data,
- dataType: 'jsonp'
- });
- var dfd = new Deferred();
- _wrapInTimeout(jqxhr).done(function(results) {
- if (results.error) {
- dfd.reject(results.error);
- }
-
- dfd.resolve({
- records: results.data,
- fields: results.fields,
- useMemoryStore: true
- });
- })
- .fail(function(args) {
- dfd.reject(args);
- });
- return dfd.promise();
- };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 -a crude way to catch those errors.
- - var _wrapInTimeout = function(ourFunction) {
- var dfd = new Deferred();
- var timer = setTimeout(function() {
- dfd.reject({
- message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
- });
- }, my.timeout);
- ourFunction.done(function(args) {
- clearTimeout(timer);
- dfd.resolve(args);
- })
- .fail(function(args) {
- clearTimeout(timer);
- dfd.reject(args);
- })
- ;
- return dfd.promise();
- };
-
-}(this.recline.Backend.DataProxy)); backend.elasticsearch.js | |
|---|---|
this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
-
-(function($, my) {
- my.__type__ = 'elasticsearch'; | |
| use either jQuery or Underscore Deferred depending on what is available | var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; |
ElasticSearch Wrapper- -A simple JS wrapper around an ElasticSearch endpoints. - -@param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running -on http://localhost:9200 with index twitter and type tweet it would be: - -http://localhost:9200/twitter/tweet- - @param {Object} options: set of options such as: - -
| my.Wrapper = function(endpoint, options) {
- var self = this;
- this.endpoint = endpoint;
- this.options = _.extend({
- dataType: 'json'
- },
- options); |
mapping- -Get ES mapping for this type/table - -@return promise compatible deferred object. | this.mapping = function() {
- var schemaUrl = self.endpoint + '/_mapping';
- var jqxhr = makeRequest({
- url: schemaUrl,
- dataType: this.options.dataType
- });
- return jqxhr;
- }; |
get- -Get record corresponding to specified id - -@return promise compatible deferred object. | this.get = function(id) {
- var base = this.endpoint + '/' + id;
- return makeRequest({
- url: base,
- dataType: 'json'
- });
- }; |
upsert- -create / update a record to ElasticSearch backend - -@param {Object} doc an object to insert to the index. -@return deferred supporting promise API | this.upsert = function(doc) {
- var data = JSON.stringify(doc);
- url = this.endpoint;
- if (doc.id) {
- url += '/' + doc.id;
- }
- return makeRequest({
- url: url,
- type: 'POST',
- data: data,
- dataType: 'json'
- });
- }; |
delete- -Delete a record from the ElasticSearch backend. - -@param {Object} id id of object to delete -@return deferred supporting promise API | this.remove = function(id) {
- url = this.endpoint;
- url += '/' + id;
- return makeRequest({
- url: url,
- type: 'DELETE',
- dataType: 'json'
- });
- };
-
- this._normalizeQuery = function(queryObj) {
- var self = this;
- var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
- var out = {
- constant_score: {
- query: {}
- }
- };
- if (!queryInfo.q) {
- out.constant_score.query = {
- match_all: {}
- };
- } else {
- out.constant_score.query = {
- query_string: {
- query: queryInfo.q
- }
- };
- }
- if (queryInfo.filters && queryInfo.filters.length) {
- out.constant_score.filter = {
- and: []
- };
- _.each(queryInfo.filters, function(filter) {
- out.constant_score.filter.and.push(self._convertFilter(filter));
- });
- }
- return out;
- }, |
| convert from Recline sort structure to ES form -http://www.elasticsearch.org/guide/reference/api/search/sort.html | this._normalizeSort = function(sort) {
- var out = _.map(sort, function(sortObj) {
- var _tmp = {};
- var _tmp2 = _.clone(sortObj);
- delete _tmp2['field'];
- _tmp[sortObj.field] = _tmp2;
- return _tmp;
- });
- return out;
- },
-
- this._convertFilter = function(filter) {
- var out = {};
- out[filter.type] = {}
- if (filter.type === 'term') {
- out.term[filter.field] = filter.term.toLowerCase();
- } else if (filter.type === 'geo_distance') {
- out.geo_distance[filter.field] = filter.point;
- out.geo_distance.distance = filter.distance;
- out.geo_distance.unit = filter.unit;
- }
- return out;
- }, |
query- -@return deferred supporting promise API | this.query = function(queryObj) {
- var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
- esQuery.query = this._normalizeQuery(queryObj);
- delete esQuery.q;
- delete esQuery.filters;
- if (esQuery.sort && esQuery.sort.length > 0) {
- esQuery.sort = this._normalizeSort(esQuery.sort);
- }
- var data = {source: JSON.stringify(esQuery)};
- var url = this.endpoint + '/_search';
- var jqxhr = makeRequest({
- url: url,
- data: data,
- dataType: this.options.dataType
- });
- return jqxhr;
- }
- }; |
Recline Connectors- -Requires URL of ElasticSearch endpoint to be specified on the dataset -via the url attribute. | |
| ES options which are passed through to | my.esOptions = {}; |
fetch | my.fetch = function(dataset) {
- var es = new my.Wrapper(dataset.url, my.esOptions);
- var dfd = new Deferred();
- es.mapping().done(function(schema) {
-
- if (!schema){
- dfd.reject({'message':'Elastic Search did not return a mapping'});
- return;
- } |
| 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;
- });
- dfd.resolve({
- fields: fieldData
- });
- })
- .fail(function(arguments) {
- dfd.reject(arguments);
- });
- return dfd.promise();
- }; |
save | my.save = function(changes, dataset) {
- var es = new my.Wrapper(dataset.url, my.esOptions);
- if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
- var dfd = new Deferred();
- msg = 'Saving more than one item at a time not yet supported';
- alert(msg);
- dfd.reject(msg);
- return dfd.promise();
- }
- if (changes.creates.length > 0) {
- return es.upsert(changes.creates[0]);
- }
- else if (changes.updates.length >0) {
- return es.upsert(changes.updates[0]);
- } else if (changes.deletes.length > 0) {
- return es.remove(changes.deletes[0].id);
- }
- }; |
query | my.query = function(queryObj, dataset) {
- var dfd = new Deferred();
- var es = new my.Wrapper(dataset.url, my.esOptions);
- var jqxhr = es.query(queryObj);
- jqxhr.done(function(results) {
- var out = {
- total: results.hits.total
- };
- out.hits = _.map(results.hits.hits, function(hit) {
- if (!('id' in hit._source) && hit._id) {
- hit._source.id = hit._id;
- }
- return hit._source;
- });
- if (results.facets) {
- out.facets = results.facets;
- }
- dfd.resolve(out);
- }).fail(function(errorObj) {
- var out = {
- title: 'Failed: ' + errorObj.status + ' code',
- message: errorObj.responseText
- };
- dfd.reject(out);
- });
- return dfd.promise();
- }; |
makeRequest- -Just $.ajax but in any headers in the 'headers' attribute of this -Backend instance. Example: - -
-var jqxhr = this._makeRequest({
- url: the-url
-});
- | var makeRequest = function(data, headers) {
- var extras = {};
- if (headers) {
- extras = {
- beforeSend: function(req) {
- _.each(headers, function(value, key) {
- req.setRequestHeader(key, value);
- });
- }
- };
- }
- var data = _.extend(extras, data);
- return $.ajax(data);
-};
-
-}(jQuery, this.recline.Backend.ElasticSearch));
-
- |
backend.gdocs.js | |
|---|---|
this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
-
-(function(my) {
- my.__type__ = 'gdocs'; | |
| use either jQuery or Underscore Deferred depending on what is available | var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; |
Google spreadsheet backend- -Fetch data from a Google Docs spreadsheet. - -Dataset must have a url attribute pointing to the Gdocs or its JSON feed e.g. - -
-var dataset = new recline.Model.Dataset({
- url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGlQVDJnbjZRSU1tUUJWOUZXRG53VkE#gid=0'
- },
- 'gdocs'
-);
-
-var dataset = new recline.Model.Dataset({
- url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
- },
- 'gdocs'
-);
-
-
-@return object with two attributes - -
| my.fetch = function(dataset) {
- var dfd = new Deferred();
- var urls = my.getGDocsAPIUrls(dataset.url); |
| TODO cover it with tests -get the spreadsheet title | (function () {
- var titleDfd = new Deferred();
-
- jQuery.getJSON(urls.spreadsheet, function (d) {
- titleDfd.resolve({
- spreadsheetTitle: d.feed.title.$t
- });
- });
-
- return titleDfd.promise();
- }()).then(function (response) { |
| get the actual worksheet data | jQuery.getJSON(urls.worksheet, function(d) {
- var result = my.parseData(d);
- var fields = _.map(result.fields, function(fieldId) {
- return {id: fieldId};
- });
-
- dfd.resolve({
- metadata: {
- title: response.spreadsheetTitle +" :: "+ result.worksheetTitle,
- spreadsheetTitle: response.spreadsheetTitle,
- worksheetTitle : result.worksheetTitle
- },
- records : result.records,
- fields : fields,
- useMemoryStore: true
- });
- });
- });
-
- return dfd.promise();
- }; |
parseData- -Parse data from Google Docs API into a reasonable form - -: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. | my.parseData = function(gdocsSpreadsheet, options) {
- var options = options || {};
- var colTypes = options.colTypes || {};
- var results = {
- fields : [],
- records: []
- };
- var entries = gdocsSpreadsheet.feed.entry || [];
- var key;
- var colName; |
| percentage values (e.g. 23.3%) | var rep = /^([\d\.\-]+)\%$/;
-
- for(key in entries[0]) { |
| it's barely possible it has inherited keys starting with 'gsx$' | if(/^gsx/.test(key)) {
- colName = key.substr(4);
- results.fields.push(colName);
- }
- } |
| converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) | results.records = _.map(entries, function(entry) {
- var row = {};
-
- _.each(results.fields, function(col) {
- var _keyname = 'gsx$' + col;
- var value = entry[_keyname].$t;
- var num;
- |
| TODO cover this part of code with test -TODO use the regexp only once -if labelled as % and value contains %, convert | if(colTypes[col] === 'percent' && rep.test(value)) {
- num = rep.exec(value)[1];
- value = parseFloat(num) / 100;
- }
-
- row[col] = value;
- });
-
- return row;
- });
-
- results.worksheetTitle = gdocsSpreadsheet.feed.title.$t;
- return results;
- }; |
| Convenience function to get GDocs JSON API Url from standard URL | my.getGDocsAPIUrls = function(url) { |
| https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY | var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+)[^#]*(#gid=([\d]+).*)?/;
- var matches = url.match(regex);
- var key;
- var worksheet;
- var urls;
-
- if(!!matches) {
- key = matches[1]; |
| the gid in url is 0-based and feed url is 1-based | worksheet = parseInt(matches[3]) + 1;
- if (isNaN(worksheet)) {
- worksheet = 1;
- }
- urls = {
- worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
- spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
- }
- }
- else { |
| we assume that it's one of the feeds urls | key = url.split('/')[5]; |
| by default then, take first worksheet | worksheet = 1;
- urls = {
- worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
- spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
- }
- }
-
- return urls;
- };
-}(this.recline.Backend.GDocs));
-
- |
this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.Memory = this.recline.Backend.Memory || {};
-
-(function(my) {
- "use strict";
- my.__type__ = 'memory';private data - use either jQuery or Underscore Deferred depending on what is available
- - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;Turn a simple array of JS objects into a mini data-store with -functionality like querying, faceting, updating (by ID) and deleting (by -ID).
-@param records list of hashes for each record/row in the data ({key: -value, key: value}) -@param fields (optional) list of field hashes (each hash defining a field -as per recline.Model.Field). If fields not specified they will be taken -from the data.
- - my.Store = function(records, fields) {
- var self = this;
- this.records = records;backwards compatability (in v0.5 records was named data)
- - this.data = this.records;
- if (fields) {
- this.fields = fields;
- } else {
- if (records) {
- this.fields = _.map(records[0], function(value, key) {
- return {id: key, type: 'string'};
- });
- }
- }
-
- this.update = function(doc) {
- _.each(self.records, function(internalDoc, idx) {
- if(doc.id === internalDoc.id) {
- self.records[idx] = doc;
- }
- });
- };
-
- this.remove = function(doc) {
- var newdocs = _.reject(self.records, function(internalDoc) {
- return (doc.id === internalDoc.id);
- });
- this.records = newdocs;
- };
-
- this.save = function(changes, dataset) {
- var self = this;
- var dfd = new Deferred();TODO _.each(changes.creates) { ⦠}
- - _.each(changes.updates, function(record) {
- self.update(record);
- });
- _.each(changes.deletes, function(record) {
- self.remove(record);
- });
- dfd.resolve();
- return dfd.promise();
- },
-
- this.query = function(queryObj) {
- var dfd = new Deferred();
- var numRows = queryObj.size || this.records.length;
- var start = queryObj.from || 0;
- var results = this.records;
-
- results = this._applyFilters(results, queryObj);
- results = this._applyFreeTextQuery(results, queryObj);TODO: this is not complete sorting! -Whatās wrong is we sort on the last entry in the sort list if there are multiple sort criteria
- - _.each(queryObj.sort, function(sortObj) {
- var fieldName = sortObj.field;
- results = _.sortBy(results, function(doc) {
- var _out = doc[fieldName];
- return _out;
- });
- if (sortObj.order == 'desc') {
- results.reverse();
- }
- });
- var facets = this.computeFacets(results, queryObj);
- var out = {
- total: results.length,
- hits: results.slice(start, start+numRows),
- facets: facets
- };
- dfd.resolve(out);
- return dfd.promise();
- };in place filtering
- - this._applyFilters = function(results, queryObj) {
- var filters = queryObj.filters;register filters
- - var filterFunctions = {
- term : term,
- terms : terms,
- range : range,
- geo_distance : geo_distance
- };
- var dataParsers = {
- integer: function (e) { return parseFloat(e, 10); },
- 'float': function (e) { return parseFloat(e, 10); },
- number: function (e) { return parseFloat(e, 10); },
- string : function (e) { return e.toString(); },
- date : function (e) { return moment(e).valueOf(); },
- datetime : function (e) { return new Date(e).valueOf(); }
- };
- var keyedFields = {};
- _.each(self.fields, function(field) {
- keyedFields[field.id] = field;
- });
- function getDataParser(filter) {
- var fieldType = keyedFields[filter.field].type || 'string';
- return dataParsers[fieldType];
- }filter records
- - return _.filter(results, function (record) {
- var passes = _.map(filters, function (filter) {
- return filterFunctions[filter.type](record, filter);
- });return only these records that pass all filters
- - return _.all(passes, _.identity);
- });filters definitions
- - function term(record, filter) {
- var parse = getDataParser(filter);
- var value = parse(record[filter.field]);
- var term = parse(filter.term);
-
- return (value === term);
- }
-
- function terms(record, filter) {
- var parse = getDataParser(filter);
- var value = parse(record[filter.field]);
- var terms = parse(filter.terms).split(",");
-
- return (_.indexOf(terms, value) >= 0);
- }
-
- function range(record, filter) {
- var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === '');
- var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === '');
- var parse = getDataParser(filter);
- var value = parse(record[filter.field]);
- var from = parse(fromnull ? '' : filter.from);
- var to = parse(tonull ? '' : filter.to);if at least one end of range is set do not allow āā to get through -note that for strings āā <= {any-character} e.g. āā <= āaā
- - if ((!fromnull || !tonull) && value === '') {
- return false;
- }
- return ((fromnull || value >= from) && (tonull || value <= to));
- }
-
- function geo_distance() {TODO code here
- - }
- };we OR across fields but AND across terms in query string
- - this._applyFreeTextQuery = function(results, queryObj) {
- if (queryObj.q) {
- var terms = queryObj.q.split(' ');
- var patterns=_.map(terms, function(term) {
- return new RegExp(term.toLowerCase());
- });
- results = _.filter(results, function(rawdoc) {
- var matches = true;
- _.each(patterns, function(pattern) {
- var foundmatch = false;
- _.each(self.fields, function(field) {
- var value = rawdoc[field.id];
- if ((value !== null) && (value !== undefined)) {
- value = value.toString();
- } else {value can be null (apparently in some cases)
- - value = '';
- }TODO regexes?
- - foundmatch = foundmatch || (pattern.test(value.toLowerCase()));TODO: early out (once we are true should break to spare unnecessary testing) -if (foundmatch) return true;
- - });
- matches = matches && foundmatch;TODO: early out (once false should break to spare unnecessary testing) -if (!matches) return false;
- - });
- return matches;
- });
- }
- return results;
- };
-
- this.computeFacets = function(records, queryObj) {
- var facetResults = {};
- if (!queryObj.facets) {
- return facetResults;
- }
- _.each(queryObj.facets, function(query, facetId) {TODO: remove dependency on recline.Model
- - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
- facetResults[facetId].termsall = {};
- });faceting
- - _.each(records, 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;
- });
- tmp.terms = tmp.terms.slice(0, 10);
- });
- return facetResults;
- };
- };
-
-}(this.recline.Backend.Memory)); demo.search.app.js | |
|---|---|
| (c) Open Knowledge Foundation 2012. Dedicated to the public domain. Please -use and reuse freely - you don't even need to credit (though a link back to -ReclineJS.com is always appreciated)! | |
Our main loop - on document ready | jQuery(function($) {
- var $el = $('.search-here'); |
Overview- -We have a slightly more complex setup than is needed to allow for using -this demo with different backends - -There are 2 alternatives: more complex and a simpler one - -If you just want to see how this work skip to the simple case ... | |
1. More complex - use data from a backend configured in query string | |
| Check for config from url query string | var config = recline.View.parseQueryString(decodeURIComponent(window.location.search));
- if (config.backend) { |
| If we had it hand off to our more complex example setup | setupMoreComplexExample(config);
- return;
- } |
2. The simple example case- -We will just set up from some example local data (at the bottom of thile file) | |
Create our Recline Dataset from sample local data | var dataset = new recline.Model.Dataset({
- records: sampleData
- }); |
Custom template- -Create a custom template for rendering the records | var template = ' \
- <div class="record"> \
- <h3> \
- {{title}} <em>by {{Author}}</em> \
- </h3> \
- <p>{{description}}</p> \
- <p><code>${{price}}</code></p> \
- </div> \
- '; |
Set up the search View (using custom template) | var searchView = new SearchView({
- el: $el,
- model: dataset,
- template: template
- });
- searchView.render(); |
Optional - we configure the initial query a bit and set up facets | dataset.queryState.set({
- size: 10
- },
- {silent: true}
- );
- dataset.queryState.addFacet('Author'); |
Finally - now do the first query- -After this point the Search View will take over handling queries! | dataset.query();
-}); |
Simple Search View- -This is a simple bespoke Backbone view for the Search. It Pulls together -various Recline UI components and the central Dataset and Query (state) -object - -It also provides simple support for customization e.g. of template for list of results - - | var SearchView = Backbone.View.extend({
- initialize: function(options) {
- this.el = $(this.el);
- _.bindAll(this, 'render');
- this.recordTemplate = options.template; |
| Every time we do a search the recline.Dataset.records Backbone -collection will get reset. We want to re-render each time! | this.model.records.bind('reset', this.render);
- this.templateResults = options.template;
- }, |
| overall template for this view | template: ' \
- <div class="controls"> \
- <div class="query-here"></div> \
- </div> \
- <div class="total"><h2><span></span> records found</h2></div> \
- <div class="body"> \
- <div class="sidebar"></div> \
- <div class="results"> \
- {{{results}}} \
- </div> \
- </div> \
- <div class="pager-here"></div> \
- ',
- |
| render the view | render: function() {
- var results = '';
- if (_.isFunction(this.templateResults)) {
- var results = _.map(this.model.records.toJSON(), this.templateResults).join('\n');
- } else { |
| templateResults is just for one result ... | var tmpl = '{{#records}}' + this.templateResults + '{{/records}}';
- var results = Mustache.render(tmpl, {
- records: this.model.records.toJSON()
- });
- }
- var html = Mustache.render(this.template, {
- results: results
- });
- this.el.html(html); |
| Set the total records found info | this.el.find('.total span').text(this.model.recordCount); |
Now setup all the extra mini-widgets- -Facets, Pager, QueryEditor etc | var view = new recline.View.FacetViewer({
- model: this.model
- });
- view.render();
- this.el.find('.sidebar').append(view.el);
-
- var pager = new recline.View.Pager({
- model: this.model.queryState
- });
- this.el.find('.pager-here').append(pager.el);
-
- var queryEditor = new recline.View.QueryEditor({
- model: this.model.queryState
- });
- this.el.find('.query-here').append(queryEditor.el);
- }
-}); |
| - - Custom code very specific to this demo | |
| e.g. to provide custom templates for the google doc and openspending examples | |
Handle case where we get data from a specific backend- -Includes providing custom templates | function setupMoreComplexExample(config) {
- var $el = $('.search-here');
- var dataset = new recline.Model.Dataset(config); |
| async as may be fetching remote | dataset.fetch().done(function() {
- var template = templates[dataset.get('url')] || templates['generic'];
- var searchView = new SearchView({
- el: $el,
- model: dataset,
- template: template
- });
- searchView.render();
-
- dataset.queryState.set({
- size: 5
- },
- {silent: true}
- );
- if (dataset.get('url') in templates) { |
| for gdocs example | dataset.queryState.addFacet('cause');
- }
- dataset.query();
- });
-};
-
-var templates = { |
| generic template function | 'generic': function(record) {
- var template = '<div class="record"> \
- <ul> \
- {{#data}} \
- <li>{{key}}: {{value}}</li> \
- {{/data}} \
- </ul> \
- </div> \
- ';
- var data = _.map(_.keys(record), function(key) {
- return { key: key, value: record[key] };
- });
- return Mustache.render(template, {
- data: data
- });
- },
- 'http://openspending.org/api/search': function(record) {
- record['time'] = record['time.label_facet']
- var template = '<div class="record"> \
- <h3> \
- <a href="http://openspending.org/{{record.dataset}}/entries/{{record.id}}">{{record.dataset}} {{record.time}}</a> \
- – <img src="http://openspending.org/static/img/icons/cd_16x16.png" /> {{amount_formatted}} \
- </h3> \
- <ul> \
- {{#data}} \
- <li>{{key}}: {{value}}</li> \
- {{/data}} \
- </ul> \
- </div> \
- ';
- var data = [];
- _.each(_.keys(record), function(key) {
- if (key !='_id' && key != 'id') {
- data.push({ key: key, value: record[key] });
- }
- });
- return Mustache.render(template, {
- record: record,
- amount_formatted: formatAmount(record['amount']),
- data: data
- });
- },
- 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdExXSTl2Y01xZEszOTBFZjVzcGtzVVE': function(record) {
- var template = '<div class="record"> \
- <h3> \
- {{record.incidentsite}} – {{record.datereported}} – {{record.estimatedspillvolumebbl}} barrels \
- </h3> \
- <ul> \
- {{#data}} \
- <li>{{key}}: {{value}}</li> \
- {{/data}} \
- </ul> \
- </div> \
- ';
- var data = [];
- _.each(_.keys(record), function(key) {
- data.push({ key: key, value: record[key] });
- });
- return Mustache.render(template, {
- record: record,
- data: data
- });
- }
-}
-
-var sampleData = [
- {
- title: 'War and Peace',
- description: 'The epic tale of love, war and history',
- Author: 'Tolstoy',
- price: 7.99
- },
- {
- title: 'Anna Karenina',
- description: 'How things go wrong in love and ultimately lead to suicide. This is why you should not have affairs, girls!',
- Author: 'Tolstoy',
- price: 8.50
- },
- {
- title: "Fathers and Sons",
- description: "Another 19th century Russian novel",
- Author: "Turgenev",
- price: 11
- }
-];
-
-var formatAmount = function (num) {
- var billion = 1000000000;
- var million = 1000000;
- var thousand = 1000;
- var numabs = Math.abs(num);
- if (numabs > billion) {
- return (num / billion).toFixed(0) + 'bn';
- }
- if (numabs > (million / 2)) {
- return (num / million).toFixed(0) + 'm';
- }
- if (numabs > thousand) {
- return (num / thousand).toFixed(0) + 'k';
- } else {
- return num.toFixed(0);
- }
-};
-
- |
This file adds in full array method support in browsers that donāt support it -see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc
- -Add ECMA262-5 Array methods if not supported natively
- -if (!('indexOf' in Array.prototype)) {
- Array.prototype.indexOf= function(find, i /*opt*/) {
- if (i===undefined) i= 0;
- if (i<0) i+= this.length;
- if (i<0) i= 0;
- for (var n= this.length; i<n; i++)
- if (i in this && this[i]===find)
- return i;
- return -1;
- };
-}
-if (!('lastIndexOf' in Array.prototype)) {
- Array.prototype.lastIndexOf= function(find, i /*opt*/) {
- if (i===undefined) i= this.length-1;
- if (i<0) i+= this.length;
- if (i>this.length-1) i= this.length-1;
- for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */
- if (i in this && this[i]===find)
- return i;
- return -1;
- };
-}
-if (!('forEach' in Array.prototype)) {
- Array.prototype.forEach= function(action, that /*opt*/) {
- for (var i= 0, n= this.length; i<n; i++)
- if (i in this)
- action.call(that, this[i], i, this);
- };
-}
-if (!('map' in Array.prototype)) {
- Array.prototype.map= function(mapper, that /*opt*/) {
- var other= new Array(this.length);
- for (var i= 0, n= this.length; i<n; i++)
- if (i in this)
- other[i]= mapper.call(that, this[i], i, this);
- return other;
- };
-}
-if (!('filter' in Array.prototype)) {
- Array.prototype.filter= function(filter, that /*opt*/) {
- var other= [], v;
- for (var i=0, n= this.length; i<n; i++)
- if (i in this && filter.call(that, v= this[i], i, this))
- other.push(v);
- return other;
- };
-}
-if (!('every' in Array.prototype)) {
- Array.prototype.every= function(tester, that /*opt*/) {
- for (var i= 0, n= this.length; i<n; i++)
- if (i in this && !tester.call(that, this[i], i, this))
- return false;
- return true;
- };
-}
-if (!('some' in Array.prototype)) {
- Array.prototype.some= function(tester, that /*opt*/) {
- for (var i= 0, n= this.length; i<n; i++)
- if (i in this && tester.call(that, this[i], i, this))
- return true;
- return false;
- };
-}this.recline = this.recline || {};
-this.recline.Model = this.recline.Model || {};
-
-(function(my) {
- "use strict";use either jQuery or Underscore Deferred depending on what is available
- -var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;my.Dataset = Backbone.Model.extend({
- constructor: function Dataset() {
- Backbone.Model.prototype.constructor.apply(this, arguments);
- }, initialize: function() {
- var self = this;
- _.bindAll(this, 'query');
- this.backend = null;
- if (this.get('backend')) {
- this.backend = this._backendFromString(this.get('backend'));
- } else { // try to guess backend ...
- if (this.get('records')) {
- this.backend = recline.Backend.Memory;
- }
- }
- this.fields = new my.FieldList();
- this.records = new my.RecordList();
- this._changes = {
- deletes: [],
- updates: [],
- creates: []
- };
- this.facets = new my.FacetList();
- this.recordCount = null;
- this.queryState = new my.Query();
- this.queryState.bind('change facet:add', function () {
- self.query(); // We want to call query() without any arguments.
- });store is what we query and save against -store will either be the backend or be a memory store if Backend fetch -tells us to use memory store
- - this._store = this.backend;if backend has a handleQueryResultFunction, use that
- - this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ?
- this.backend.handleQueryResult : this._handleQueryResult;
- if (this.backend == recline.Backend.Memory) {
- this.fetch();
- }
- },
-
- sync: function(method, model, options) {
- return this.backend.sync(method, model, options);
- }, fetch: function() {
- var self = this;
- var dfd = new Deferred();
-
- if (this.backend !== recline.Backend.Memory) {
- this.backend.fetch(this.toJSON())
- .done(handleResults)
- .fail(function(args) {
- dfd.reject(args);
- });
- } else {special case where we have been given data directly
- - handleResults({
- records: this.get('records'),
- fields: this.get('fields'),
- useMemoryStore: true
- });
- }
-
- function handleResults(results) {if explicitly given the fields -(e.g. var dataset = new Dataset({fields: fields, ā¦}) -use that field info over anything we get back by parsing the data -(results.fields)
- - var fields = self.get('fields') || results.fields;
-
- var out = self._normalizeRecordsAndFields(results.records, fields);
- if (results.useMemoryStore) {
- self._store = new recline.Backend.Memory.Store(out.records, out.fields);
- }
-
- self.set(results.metadata);
- self.fields.reset(out.fields);
- self.query()
- .done(function() {
- dfd.resolve(self);
- })
- .fail(function(args) {
- dfd.reject(args);
- });
- }
-
- return dfd.promise();
- },Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects
-e.g. fields = [āaā, ābā, ācā] and records = [ [1,2,3] ] => -fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]
- - _normalizeRecordsAndFields: function(records, fields) {if no fields get them from records
- - if (!fields && records && records.length > 0) {records is array then fields is first row of records ā¦
- - if (records[0] instanceof Array) {
- fields = records[0];
- records = records.slice(1);
- } else {
- fields = _.map(_.keys(records[0]), function(key) {
- return {id: key};
- });
- }
- }fields is an array of strings (i.e. list of field headings/ids)
- - if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) {Rename duplicate fieldIds as each field name needs to be -unique.
- - var seen = {};
- fields = _.map(fields, function(field, index) {
- if (field === null) {
- field = '';
- } else {
- field = field.toString();
- }cannot use trim as not supported by IE7
- - var fieldId = field.replace(/^\s+|\s+$/g, '');
- if (fieldId === '') {
- fieldId = '_noname_';
- field = fieldId;
- }
- while (fieldId in seen) {
- seen[field] += 1;
- fieldId = field + seen[field];
- }
- if (!(field in seen)) {
- seen[field] = 0;
- }TODO: decide whether to keep original name as label ⦠-return { id: fieldId, label: field || fieldId }
- - return { id: fieldId };
- });
- }records is provided as arrays so need to zip together with fields -NB: this requires you to have fields to match arrays
- - if (records && records.length > 0 && records[0] instanceof Array) {
- records = _.map(records, function(doc) {
- var tmp = {};
- _.each(fields, function(field, idx) {
- tmp[field.id] = doc[idx];
- });
- return tmp;
- });
- }
- return {
- fields: fields,
- records: records
- };
- },
-
- save: function() {
- var self = this;TODO: need to reset the changes ā¦
- - return this._store.save(this._changes, this.toJSON());
- },AJAX method with promise API to get records from the backend.
-It will query based on current query state (given by this.queryState) -updated by queryObj (if provided).
-Resulting RecordList are used to reset this.records and are -also returned.
- - query: function(queryObj) {
- var self = this;
- var dfd = new Deferred();
- this.trigger('query:start');
-
- if (queryObj) {
- var attributes = queryObj;
- if (queryObj instanceof my.Query) {
- attributes = queryObj.toJSON();
- }
- this.queryState.set(attributes, {silent: true});
- }
- var actualQuery = this.queryState.toJSON();
-
- this._store.query(actualQuery, this.toJSON())
- .done(function(queryResult) {
- self._handleResult(queryResult);
- self.trigger('query:done');
- dfd.resolve(self.records);
- })
- .fail(function(args) {
- self.trigger('query:fail', args);
- dfd.reject(args);
- });
- return dfd.promise();
- },
-
- _handleQueryResult: function(queryResult) {
- var self = this;
- self.recordCount = queryResult.total;
- var docs = _.map(queryResult.hits, function(hit) {
- var _doc = new my.Record(hit);
- _doc.fields = self.fields;
- _doc.bind('change', function(doc) {
- self._changes.updates.push(doc.toJSON());
- });
- _doc.bind('destroy', function(doc) {
- self._changes.deletes.push(doc.toJSON());
- });
- return _doc;
- });
- self.records.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);
- }
- },
-
- toTemplateJSON: function() {
- var data = this.toJSON();
- data.recordCount = this.recordCount;
- data.fields = this.fields.toJSON();
- return data;
- },Get a summary for each field in the form of a Facet.
@return null as this is async function. Provides deferred/promise interface.
- - getFieldsSummary: function() {
- var self = this;
- var query = new my.Query();
- query.set({size: 0});
- this.fields.each(function(field) {
- query.addFacet(field.id);
- });
- var dfd = new Deferred();
- this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
- if (queryResult.facets) {
- _.each(queryResult.facets, function(facetResult, facetId) {
- facetResult.id = facetId;
- var facet = new my.Facet(facetResult);TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
- - self.fields.get(facetId).facets.reset(facet);
- });
- }
- dfd.resolve(queryResult);
- });
- return dfd.promise();
- },Deprecated (as of v0.5) - use record.summary()
- - recordSummary: function(record) {
- return record.summary();
- },Look up a backend module from a backend string (look in recline.Backend)
- - _backendFromString: function(backendString) {
- var backend = null;
- if (recline && recline.Backend) {
- _.each(_.keys(recline.Backend), function(name) {
- if (name.toLowerCase() === backendString.toLowerCase()) {
- backend = recline.Backend[name];
- }
- });
- }
- return backend;
- }
-});my.Record = Backbone.Model.extend({
- constructor: function Record() {
- Backbone.Model.prototype.constructor.apply(this, arguments);
- },Create a Record
-You usually will not do this directly but will have records created by -Dataset e.g. in query method
-Certain methods require presence of a fields attribute (identical to that on Dataset)
- - initialize: function() {
- _.bindAll(this, 'getFieldValue');
- },For the provided Field get the corresponding rendered computed data value -for this record.
-NB: if field is undefined a default āā value will be returned
- - getFieldValue: function(field) {
- var val = this.getFieldValueUnrendered(field);
- if (field && !_.isUndefined(field.renderer)) {
- val = field.renderer(val, field, this.toJSON());
- }
- return val;
- },For the provided Field get the corresponding computed data value -for this record.
-NB: if field is undefined a default āā value will be returned
- - getFieldValueUnrendered: function(field) {
- if (!field) {
- return '';
- }
- var val = this.get(field.id);
- if (field.deriver) {
- val = field.deriver(val, field, this);
- }
- return val;
- }, summary: function(record) {
- var self = this;
- var html = '<div class="recline-record-summary">';
- this.fields.each(function(field) {
- if (field.id != 'id') {
- html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
- }
- });
- html += '</div>';
- return html;
- },Override Backbone save, fetch and destroy so they do nothing -Instead, Dataset object that created this Record should take care of -handling these changes (discovery will occur via event notifications) -WARNING: these will not persist unless you call save on Dataset
- - fetch: function() {},
- save: function() {},
- destroy: function() { this.trigger('destroy', this); }
-});my.RecordList = Backbone.Collection.extend({
- constructor: function RecordList() {
- Backbone.Collection.prototype.constructor.apply(this, arguments);
- },
- model: my.Record
-});my.Field = Backbone.Model.extend({
- constructor: function Field() {
- Backbone.Model.prototype.constructor.apply(this, arguments);
- }, defaults: {
- label: null,
- type: 'string',
- format: null,
- is_derived: false
- },@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) {
- throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
- }
- if (this.attributes.label === null) {
- this.set({label: this.id});
- }
- if (this.attributes.type.toLowerCase() in this._typeMap) {
- this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()];
- }
- if (options) {
- this.renderer = options.renderer;
- this.deriver = options.deriver;
- }
- if (!this.renderer) {
- this.renderer = this.defaultRenderers[this.get('type')];
- }
- this.facets = new my.FacetList();
- },
- _typeMap: {
- 'text': 'string',
- 'double': 'number',
- 'float': 'number',
- 'numeric': 'number',
- 'int': 'integer',
- 'datetime': 'date-time',
- 'bool': 'boolean',
- 'timestamp': 'date-time',
- 'json': 'object'
- },
- defaultRenderers: {
- object: function(val, field, doc) {
- return JSON.stringify(val);
- },
- geo_point: function(val, field, doc) {
- return JSON.stringify(val);
- },
- 'number': function(val, field, doc) {
- var format = field.get('format');
- if (format === 'percentage') {
- return val + '%';
- }
- return val;
- },
- 'string': function(val, field, doc) {
- var format = field.get('format');
- if (format === 'markdown') {
- if (typeof Showdown !== 'undefined') {
- var showdown = new Showdown.converter();
- out = showdown.makeHtml(val);
- return out;
- } else {
- return val;
- }
- } else if (format == 'plain') {
- return val;
- } else {as this is the default and default type is string may get things -here that are not actually strings
- - if (val && typeof val === 'string') {
- val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
- }
- return val;
- }
- }
- }
-});
-
-my.FieldList = Backbone.Collection.extend({
- constructor: function FieldList() {
- Backbone.Collection.prototype.constructor.apply(this, arguments);
- },
- model: my.Field
-});my.Query = Backbone.Model.extend({
- constructor: function Query() {
- Backbone.Model.prototype.constructor.apply(this, arguments);
- },
- defaults: function() {
- return {
- size: 100,
- from: 0,
- q: '',
- facets: {},
- filters: []
- };
- },
- _filterTemplates: {
- term: {
- type: 'term',TODO do we need this attribute here?
- - field: '',
- term: ''
- },
- range: {
- type: 'range',
- from: '',
- to: ''
- },
- geo_distance: {
- type: 'geo_distance',
- distance: 10,
- unit: 'km',
- point: {
- lon: 0,
- lat: 0
- }
- }
- },Add a new filter specified by the filter hash and append to the list of filters
-@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates
- - addFilter: function(filter) {crude deep copy
- - var ourfilter = JSON.parse(JSON.stringify(filter));not fully specified so use template and over-write
- - if (_.keys(filter).length <= 3) {
- ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]);
- }
- var filters = this.get('filters');
- filters.push(ourfilter);
- this.trigger('change:filters:new-blank');
- },
- replaceFilter: function(filter) {delete filter on the same field, then add
- - var filters = this.get('filters');
- var idx = -1;
- _.each(this.get('filters'), function(f, key, list) {
- if (filter.field == f.field) {
- idx = key;
- }
- });trigger just one event (change:filters:new-blank) instead of one for remove and -one for add
- - if (idx >= 0) {
- filters.splice(idx, 1);
- this.set({filters: filters});
- }
- this.addFilter(filter);
- },
- updateFilter: function(index, value) {
- }, removeFilter: function(filterIndex) {
- var filters = this.get('filters');
- filters.splice(filterIndex, 1);
- this.set({filters: filters});
- this.trigger('change');
- },Add a Facet to this query
-See http://www.elasticsearch.org/guide/reference/api/search/facets/
- - addFacet: function(fieldId, size, silent) {
- 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 }
- };
- if (!_.isUndefined(size)) {
- facets[fieldId].terms.size = size;
- }
- this.set({facets: facets}, {silent: true});
- if (!silent) {
- 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);
- },
- removeFacet: 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;
- }
- delete facets[fieldId];
- this.set({facets: facets}, {silent: true});
- this.trigger('facet:remove', this);
- },
- clearFacets: function() {
- var facets = this.get('facets');
- _.each(_.keys(facets), function(fieldId) {
- delete facets[fieldId];
- });
- this.trigger('facet:remove', this);
- },trigger a facet add; use this to trigger a single event after adding -multiple facets
- - refreshFacets: function() {
- this.trigger('facet:add', this);
- }
-
-});my.Facet = Backbone.Model.extend({
- constructor: function Facet() {
- Backbone.Model.prototype.constructor.apply(this, arguments);
- },
- defaults: function() {
- return {
- _type: 'terms',
- total: 0,
- other: 0,
- missing: 0,
- terms: []
- };
- }
-});my.FacetList = Backbone.Collection.extend({
- constructor: function FacetList() {
- Backbone.Collection.prototype.constructor.apply(this, arguments);
- },
- model: my.Facet
-});Convenience Backbone model for storing (configuration) state of objects like Views.
- -my.ObjectState = Backbone.Model.extend({
-});Override Backbone.sync to hand off to sync function in relevant backend -Backbone.sync = function(method, model, options) { - return model.backend.sync(method, model, options); -};
- -
-}(this.recline.Model));/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";Initialization arguments (in a hash in first parameter):
-state: (optional) configuration hash of form:
- {
- group: {column name for x-axis},
- series: [{column name for series A}, {column name series B}, ... ],
- // options are: lines, points, lines-and-points, bars, columns
- graphType: 'lines',
- graphOptions: {custom [flot options]}
- }
-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.Flot = Backbone.View.extend({
- template: ' \
- <div class="recline-flot"> \
- <div class="panel graph" style="display: block;"> \
- <div class="js-temp-notice alert alert-warning alert-block"> \
- <h3 class="alert-heading">Hey there!</h3> \
- <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
- <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
- </div> \
- </div> \
- </div> \
-',
-
- initialize: function(options) {
- var self = this;
- this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
-
- _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
- this.needToRedraw = false;
- this.listenTo(this.model, 'change', this.render);
- this.listenTo(this.model.fields, 'reset add', this.render);
- this.listenTo(this.model.records, 'reset add', this.redraw);
- var stateData = _.extend({
- group: null,so that at least one series chooser box shows up
- - series: [],
- graphType: 'lines-and-points'
- },
- options.state
- );
- this.state = new recline.Model.ObjectState(stateData);
- this.previousTooltipPoint = {x: null, y: null};
- this.editor = new my.FlotControls({
- model: this.model,
- state: this.state.toJSON()
- });
- this.listenTo(this.editor.state, 'change', function() {
- self.state.set(self.editor.state.toJSON());
- self.redraw();
- });
- this.elSidebar = this.editor.$el;
- },
-
- render: function() {
- var self = this;
- var tmplData = this.model.toTemplateJSON();
- var htmls = Mustache.render(this.template, tmplData);
- this.$el.html(htmls);
- this.$graph = this.$el.find('.panel.graph');
- this.$graph.on("plothover", this._toolTip);
- return this;
- },
-
- remove: function () {
- this.editor.remove();
- Backbone.View.prototype.remove.apply(this, arguments);
- },
-
- redraw: function() {There are issues generating a Flot graph if either:
- var areWeVisible = !jQuery.expr.filters.hidden(this.el);
- if ((!areWeVisible || this.model.records.length === 0)) {
- this.needToRedraw = true;
- return;
- }check we have something to plot
- - if (this.state.get('group') && this.state.get('series')) {
- var series = this.createSeries();
- var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
- this.plot = $.plot(this.$graph, series, options);
- }
- },
-
- show: function() {because we cannot redraw when hidden we may need to when becoming visible
- - if (this.needToRedraw) {
- this.redraw();
- }
- },infoboxes on mouse hover on points/bars etc
- - _toolTip: function (event, pos, item) {
- if (item) {
- if (this.previousTooltipPoint.x !== item.dataIndex ||
- this.previousTooltipPoint.y !== item.seriesIndex) {
- this.previousTooltipPoint.x = item.dataIndex;
- this.previousTooltipPoint.y = item.seriesIndex;
- $("#recline-flot-tooltip").remove();
-
- var x = item.datapoint[0].toFixed(2),
- y = item.datapoint[1].toFixed(2);
-
- if (this.state.attributes.graphType === 'bars') {
- x = item.datapoint[1].toFixed(2),
- y = item.datapoint[0].toFixed(2);
- }
-
- var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
- group: this.state.attributes.group,
- x: this._xaxisLabel(x),
- series: item.series.label,
- y: y
- });use a different tooltip location offset for bar charts
- - var xLocation, yLocation;
- if (this.state.attributes.graphType === 'bars') {
- xLocation = item.pageX + 15;
- yLocation = item.pageY - 10;
- } else if (this.state.attributes.graphType === 'columns') {
- xLocation = item.pageX + 15;
- yLocation = item.pageY;
- } else {
- xLocation = item.pageX + 10;
- yLocation = item.pageY - 20;
- }
-
- $('<div id="recline-flot-tooltip">' + content + '</div>').css({
- top: yLocation,
- left: xLocation
- }).appendTo("body").fadeIn(200);
- }
- } else {
- $("#recline-flot-tooltip").remove();
- this.previousTooltipPoint.x = null;
- this.previousTooltipPoint.y = null;
- }
- },
-
- _xaxisLabel: function (x) {
- if (this._groupFieldIsDateTime()) {oddly x comes through as milliseconds string (rather than int -or float) so we have to reparse
- - x = new Date(parseFloat(x)).toLocaleDateString();
- } else if (this.xvaluesAreIndex) {
- x = parseInt(x, 10);HACK: deal with bar graph style cases where x-axis items were strings -In this case x at this point is the index of the item in the list of -records not its actual x-axis value
- - x = this.model.records.models[x].get(this.state.attributes.group);
- }
-
- return x;
- },Get options for Flot Graph
-needs to be function as can depend on state
-@param typeId graphType id (lines, lines-and-points etc) -@param numPoints the number of points that will be plotted
- - getGraphOptions: function(typeId, numPoints) {
- var self = this;
- var groupFieldIsDateTime = self._groupFieldIsDateTime();
- var xaxis = {};
-
- if (!groupFieldIsDateTime) {
- xaxis.tickFormatter = function (x) {convert x to a string and make sure that it is not too long or the -tick labels will overlap -TODO: find a more accurate way of calculating the size of tick labels
- - var label = self._xaxisLabel(x) || "";
-
- if (typeof label !== 'string') {
- label = label.toString();
- }
- if (self.state.attributes.graphType !== 'bars' && label.length > 10) {
- label = label.slice(0, 10) + "...";
- }
-
- return label;
- };
- }for labels case we only want ticks at the label intervals -HACK: however we also get this case with Date fields. In that case we -could have a lot of values and so we limit to max 15 (we assume)
- - if (this.xvaluesAreIndex) {
- var numTicks = Math.min(this.model.records.length, 15);
- var increment = this.model.records.length / numTicks;
- var ticks = [];
- for (var i=0; i<numTicks; i++) {
- ticks.push(parseInt(i*increment, 10));
- }
- xaxis.ticks = ticks;
- } else if (groupFieldIsDateTime) {
- xaxis.mode = 'time';
- }
-
- var yaxis = {};
- yaxis.autoscale = true;
- yaxis.autoscaleMargin = 0.02;
-
- var legend = {};
- legend.position = 'ne';
-
- var grid = {};
- grid.hoverable = true;
- grid.clickable = true;
- grid.borderColor = "#aaaaaa";
- grid.borderWidth = 1;
-
- var optionsPerGraphType = {
- lines: {
- legend: legend,
- colors: this.graphColors,
- lines: { show: true },
- xaxis: xaxis,
- yaxis: yaxis,
- grid: grid
- },
- points: {
- legend: legend,
- colors: this.graphColors,
- points: { show: true, hitRadius: 5 },
- xaxis: xaxis,
- yaxis: yaxis,
- grid: grid
- },
- 'lines-and-points': {
- legend: legend,
- colors: this.graphColors,
- points: { show: true, hitRadius: 5 },
- lines: { show: true },
- xaxis: xaxis,
- yaxis: yaxis,
- grid: grid
- },
- bars: {
- legend: legend,
- colors: this.graphColors,
- lines: { show: false },
- xaxis: yaxis,
- yaxis: xaxis,
- grid: grid,
- bars: {
- show: true,
- horizontal: true,
- shadowSize: 0,
- align: 'center',
- barWidth: 0.8
- }
- },
- columns: {
- legend: legend,
- colors: this.graphColors,
- lines: { show: false },
- xaxis: xaxis,
- yaxis: yaxis,
- grid: grid,
- bars: {
- show: true,
- horizontal: false,
- shadowSize: 0,
- align: 'center',
- barWidth: 0.8
- }
- }
- };
-
- if (self.state.get('graphOptions')) {
- return _.extend(optionsPerGraphType[typeId],
- self.state.get('graphOptions'));
- } else {
- return optionsPerGraphType[typeId];
- }
- },
-
- _groupFieldIsDateTime: function() {
- var xfield = this.model.fields.get(this.state.attributes.group);
- var xtype = xfield.get('type');
- var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
- return isDateTime;
- },
-
- createSeries: function() {
- var self = this;
- self.xvaluesAreIndex = false;
- var series = [];
- var xfield = self.model.fields.get(self.state.attributes.group);
- var isDateTime = self._groupFieldIsDateTime();
-
- _.each(this.state.attributes.series, function(field) {
- var points = [];
- var fieldLabel = self.model.fields.get(field).get('label');
-
- if (isDateTime){
- var cast = function(x){
- var _date = moment(String(x));
- if (_date.isValid()) {
- x = _date.toDate().getTime();
- }
- return x
- }
- } else {
- var raw = _.map(self.model.records.models,
- function(doc, index){
- return doc.getFieldValueUnrendered(xfield)
- });
-
- if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){
- var cast = function(x){ return parseFloat(x) }
- } else {
- self.xvaluesAreIndex = true
- }
- }
-
- _.each(self.model.records.models, function(doc, index) {
- if(self.xvaluesAreIndex){
- var x = index;
- }else{
- var x = cast(doc.getFieldValueUnrendered(xfield));
- }
-
- var yfield = self.model.fields.get(field);
- var y = parseFloat(doc.getFieldValueUnrendered(yfield));
-
- if (self.state.attributes.graphType == 'bars') {
- points.push([y, x]);
- } else {
- points.push([x, y]);
- }
- });
- series.push({
- data: points,
- label: fieldLabel,
- hoverable: true
- });
- });
- return series;
- }
-});
-
-my.FlotControls = Backbone.View.extend({
- className: "editor",
- template: ' \
- <div class="editor"> \
- <form class="form-stacked"> \
- <div class="clearfix"> \
- <div class="form-group"> \
- <label>Graph Type</label> \
- <div class="input editor-type"> \
- <select class="form-control"> \
- <option value="lines-and-points">Lines and Points</option> \
- <option value="lines">Lines</option> \
- <option value="points">Points</option> \
- <option value="bars">Bars</option> \
- <option value="columns">Columns</option> \
- </select> \
- </div> \
- </div> \
- <div class="form-group"> \
- <label>Group Column (Axis 1)</label> \
- <div class="input editor-group"> \
- <select class="form-control"> \
- <option value="">Please choose ...</option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- </div> \
- <div class="editor-series-group"> \
- </div> \
- </div> \
- <div class="editor-buttons"> \
- <button class="btn btn-default editor-add">Add Series</button> \
- </div> \
- <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
- <button class="editor-save">Save</button> \
- <input type="hidden" class="editor-id" value="chart-1" /> \
- </div> \
- </form> \
- </div> \
-',
- templateSeriesEditor: ' \
- <div class="editor-series js-series-{{seriesIndex}}"> \
- <div class="form-group"> \
- <label>Series <span>{{seriesName}} (Axis 2)</span> \
- [<a href="#remove" class="action-remove-series">Remove</a>] \
- </label> \
- <div class="input"> \
- <select class="form-control"> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- </div> \
- </div> \
- ',
- events: {
- 'change form select': 'onEditorSubmit',
- 'click .editor-add': '_onAddSeries',
- 'click .action-remove-series': 'removeSeries'
- },
-
- initialize: function(options) {
- var self = this;
- _.bindAll(this, 'render');
- this.listenTo(this.model.fields, 'reset add', this.render);
- this.state = new recline.Model.ObjectState(options.state);
- this.render();
- },
-
- render: function() {
- var self = this;
- var tmplData = this.model.toTemplateJSON();
- var htmls = Mustache.render(this.template, tmplData);
- this.$el.html(htmls);set up editor from state
- - if (this.state.get('graphType')) {
- this._selectOption('.editor-type', this.state.get('graphType'));
- }
- if (this.state.get('group')) {
- this._selectOption('.editor-group', this.state.get('group'));
- }ensure at least one series box shows up
- - var tmpSeries = [""];
- if (this.state.get('series').length > 0) {
- tmpSeries = this.state.get('series');
- }
- _.each(tmpSeries, function(series, idx) {
- self.addSeries(idx);
- self._selectOption('.editor-series.js-series-' + idx, series);
- });
- return this;
- },Private: Helper function to select an option from a select list
- - _selectOption: function(id,value){
- var options = this.$el.find(id + ' select > option');
- if (options) {
- options.each(function(opt){
- if (this.value == value) {
- $(this).attr('selected','selected');
- return false;
- }
- });
- }
- },
-
- onEditorSubmit: function(e) {
- var select = this.$el.find('.editor-group select');
- var $editor = this;
- var $series = this.$el.find('.editor-series select');
- var series = $series.map(function () {
- return $(this).val();
- });
- var updatedState = {
- series: $.makeArray(series),
- group: this.$el.find('.editor-group select').val(),
- graphType: this.$el.find('.editor-type select').val()
- };
- this.state.set(updatedState);
- },Public: Adds a new empty series select box to the editor.
-@param [int] idx index of this series in the list of series
-Returns itself.
- - addSeries: function (idx) {
- var data = _.extend({
- seriesIndex: idx,
- seriesName: String.fromCharCode(idx + 64 + 1)
- }, this.model.toTemplateJSON());
-
- var htmls = Mustache.render(this.templateSeriesEditor, data);
- this.$el.find('.editor-series-group').append(htmls);
- return this;
- },
-
- _onAddSeries: function(e) {
- e.preventDefault();
- this.addSeries(this.state.get('series').length);
- },Public: Removes a series list item from the editor.
-Also updates the labels of the remaining series elements.
- - removeSeries: function (e) {
- e.preventDefault();
- var $el = $(e.target);
- $el.parent().parent().remove();
- this.onEditorSubmit();
- }
-});
-
-})(jQuery, recline.View);this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-this.recline.View.Graph = this.recline.View.Flot;
-this.recline.View.GraphControls = this.recline.View.FlotControls;/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";Provides a tabular view on a Dataset.
-Initialize it with a recline.Model.Dataset.
my.Grid = Backbone.View.extend({
- tagName: "div",
- className: "recline-grid-container",
-
- initialize: function(modelEtc) {
- var self = this;
- _.bindAll(this, 'render', 'onHorizontalScroll');
- this.listenTo(this.model.records, 'add reset remove', this.render);
- this.tempState = {};
- var state = _.extend({
- hiddenFields: []
- }, modelEtc.state
- );
- this.state = new recline.Model.ObjectState(state);
- },
-
- events: {does not work here so done at end of render function -āscroll .recline-grid tbodyā: āonHorizontalScrollā
- - },======================================================
- -Column and row menus
- -
- setColumnSort: function(order) {
- var sort = [{}];
- sort[0][this.tempState.currentColumn] = {order: order};
- this.model.query({sort: sort});
- },
-
- hideColumn: function() {
- var hiddenFields = this.state.get('hiddenFields');
- hiddenFields.push(this.tempState.currentColumn);
- this.state.set({hiddenFields: hiddenFields});change event not being triggered (because it is an array?) so trigger manually
- - this.state.trigger('change');
- this.render();
- },
-
- showColumn: function(e) {
- var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
- this.state.set({hiddenFields: hiddenFields});
- this.render();
- },
-
- onHorizontalScroll: function(e) {
- var currentScroll = $(e.target).scrollLeft();
- this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll);
- },======================================================
- - template: ' \
- <div class="table-container"> \
- <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
- <thead class="fixed-header"> \
- <tr> \
- {{#fields}} \
- <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
- <span class="column-header-name">{{label}}</span> \
- </th> \
- {{/fields}} \
- <th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
- </tr> \
- </thead> \
- <tbody class="scroll-content"></tbody> \
- </table> \
- </div> \
- ',
-
- toTemplateJSON: function() {
- var self = this;
- 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 = this.fields.map(function(field) {
- return field.toJSON();
- });last header width = scroll bar - border (2px) */
- - modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
- return modelData;
- },
- render: function() {
- var self = this;
- this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) {
- return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
- }));
-
- this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions
- var numFields = this.fields.length;compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar)
- - var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
- var width = parseInt(Math.max(50, fullWidth / numFields), 10);if columns extend outside viewport then remainder is 0
- - var remainder = Math.max(fullWidth - numFields * width,0);
- this.fields.each(function(field, idx) {add the remainder to the first field width so we make up full col
- - if (idx === 0) {
- field.set({width: width+remainder});
- } else {
- field.set({width: width});
- }
- });
- var htmls = Mustache.render(this.template, this.toTemplateJSON());
- this.$el.html(htmls);
- this.model.records.forEach(function(doc) {
- var tr = $('<tr />');
- self.$el.find('tbody').append(tr);
- var newView = new my.GridRow({
- model: doc,
- el: tr,
- fields: self.fields
- });
- newView.render();
- });hide extra header col if no scrollbar to avoid unsightly overhang
- - var $tbody = this.$el.find('tbody')[0];
- if ($tbody.scrollHeight <= $tbody.offsetHeight) {
- this.$el.find('th.last-header').hide();
- }
- this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
- this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll);
- return this;
- },Measure width of a vertical scrollbar and height of a horizontal scrollbar.
-@return: { width: pixelWidth, height: pixelHeight }
- - _scrollbarSize: function() {
- var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
- var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight };
- $c.remove();
- return dim;
- }
-});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 Grid.
-Example:
-
-var row = new GridRow({
- model: dataset-record,
- el: dom-element,
- fields: mydatasets.fields // a FieldList object
- });
-
-
- my.GridRow = Backbone.View.extend({
- initialize: function(initData) {
- _.bindAll(this, 'render');
- this._fields = initData.fields;
- this.listenTo(this.model, 'change', this.render);
- },
-
- template: ' \
- {{#cells}} \
- <td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
- <div class="data-table-cell-content"> \
- <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell"> </a> \
- <div class="data-table-cell-value">{{{value}}}</div> \
- </div> \
- </td> \
- {{/cells}} \
- ',
- events: {
- 'click .data-table-cell-edit': 'onEditClick',
- 'click .data-table-cell-editor .okButton': 'onEditorOK',
- 'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
- },
-
- toTemplateJSON: function() {
- var self = this;
- var doc = this.model;
- var cellData = this._fields.map(function(field) {
- return {
- field: field.id,
- width: field.get('width'),
- value: doc.getFieldValue(field)
- };
- });
- return { id: this.id, cells: cellData };
- },
-
- render: function() {
- this.$el.attr('data-id', this.model.id);
- var html = Mustache.render(this.template, this.toTemplateJSON());
- this.$el.html(html);
- return this;
- },===================
- -Cell Editor methods
- -
- cellEditorTemplate: ' \
- <div class="menu-container data-table-cell-editor"> \
- <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
- <div id="data-table-cell-editor-actions"> \
- <div class="data-table-cell-editor-action"> \
- <button class="okButton btn primary">Update</button> \
- <button class="cancelButton btn danger">Cancel</button> \
- </div> \
- </div> \
- </div> \
- ',
-
- onEditClick: function(e) {
- var editing = this.$el.find('.data-table-cell-editor-editor');
- if (editing.length > 0) {
- editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
- }
- $(e.target).addClass("hidden");
- var cell = $(e.target).siblings('.data-table-cell-value');
- cell.data("previousContents", cell.text());
- var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()});
- cell.html(templated);
- },
-
- onEditorOK: function(e) {
- var self = this;
- var cell = $(e.target);
- var rowId = cell.parents('tr').attr('data-id');
- var field = cell.parents('td').attr('data-field');
- var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
- var newData = {};
- newData[field] = newValue;
- this.model.set(newData);
- this.trigger('recline:flash', {message: "Updating row...", loader: true});
- this.model.save().then(function(response) {
- this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'});
- })
- .fail(function() {
- this.trigger('recline:flash', {
- message: 'Error saving row',
- category: 'error',
- persist: true
- });
- });
- },
-
- onEditorCancel: function(e) {
- var cell = $(e.target).parents('.data-table-cell-value');
- cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";This view allows to plot gereferenced records on a map. The location -information can be provided in 2 ways:
-Which fields in the data these correspond to can be configured via the state -(and are guessed if no info is provided).
-Initialization arguments are as standard for Dataset Views. State object may -have the following (optional) configuration options:
-
- {
- // geomField if specified will be used in preference to lat/lon
- geomField: {id of field containing geometry in the dataset}
- lonField: {id of field containing longitude in the dataset}
- latField: {id of field containing latitude in the dataset}
- autoZoom: true,
- // use cluster support
- // cluster: true = always on
- // cluster: false = always off
- cluster: false
- }
-
-
-Useful attributes to know about (if e.g. customizing)
-my.Map = Backbone.View.extend({
- template: ' \
- <div class="recline-map"> \
- <div class="panel map"></div> \
- </div> \
-',These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define the fields via the editor.
- - latitudeFieldNames: ['lat','latitude'],
- longitudeFieldNames: ['lon','longitude'],
- geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
-
- initialize: function(options) {
- var self = this;
- this.visible = this.$el.is(':visible');
- this.mapReady = false;this will be the Leaflet L.Map object (setup below)
- - this.map = null;
-
- var stateData = _.extend({
- geomField: null,
- lonField: null,
- latField: null,
- autoZoom: true,
- cluster: false
- },
- options.state
- );
- this.state = new recline.Model.ObjectState(stateData);
-
- this._clusterOptions = {
- zoomToBoundsOnClick: true,disableClusteringAtZoom: 10,
- - maxClusterRadius: 80,
- singleMarkerMode: false,
- skipDuplicateAddTesting: true,
- animateAddingMarkers: false
- };Listen to changes in the fields
- - this.listenTo(this.model.fields, 'change', function() {
- self._setupGeometryField();
- self.render();
- });Listen to changes in the records
- - this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);});
- this.listenTo(this.model.records, 'change', function(doc){
- self.redraw('remove',doc);
- self.redraw('add',doc);
- });
- this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);});
- this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');});
-
- this.menu = new my.MapMenu({
- model: this.model,
- state: this.state.toJSON()
- });
- this.listenTo(this.menu.state, 'change', function() {
- self.state.set(self.menu.state.toJSON());
- self.redraw();
- });
- this.listenTo(this.state, 'change', function() {
- self.redraw();
- });
- this.elSidebar = this.menu.$el;
- },The following methods are designed for overriding in order to customize -behaviour
- -Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.
-Users should override this function to customize behaviour i.e.
-view = new View({...});
-view.infobox = function(record) {
- ...
-}
-
- infobox: function(record) {
- var html = '';
- for (var key in record.attributes){
- if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
- html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
- }
- }
- return html;
- },Options to use for the Leaflet GeoJSON layer -See also http://leaflet.cloudmade.com/examples/geojson.html
-e.g.
-pointToLayer: function(feature, latLng)
-onEachFeature: function(feature, layer)
-See defaults for examples
- - geoJsonLayerOptions: {pointToLayer function to use when creating points
-Default behaviour shown here is to create a marker using the -popupContent set on the feature properties (created via infobox function -during feature generation)
-NB: inside pointToLayer this will be set to point to this map view
-instance (which allows e.g. this.markers to work in this default case)
pointToLayer: function (feature, latlng) {
- var marker = new L.Marker(latlng);
- marker.bindPopup(feature.properties.popupContent);this is for cluster case
- - this.markers.addLayer(marker);
- return marker;
- },onEachFeature default which adds popup in
- - onEachFeature: function(feature, layer) {
- if (feature.properties && feature.properties.popupContent) {
- layer.bindPopup(feature.properties.popupContent);
- }
- }
- },Also sets up the editor fields and the map if necessary.
- - render: function() {
- var self = this;
- var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
- this.$el.html(htmls);
- this.$map = this.$el.find('.panel.map');
- this.redraw();
- return this;
- },Actions can be:
- redraw: function(action, doc){
- var self = this;
- action = action || 'refresh';try to set things up if not already
- - if (!self._geomReady()){
- self._setupGeometryField();
- }
- if (!self.mapReady){
- self._setupMap();
- }
-
- if (this._geomReady() && this.mapReady){removing ad re-adding the layer enables faster bulk loading
- - this.map.removeLayer(this.features);
- this.map.removeLayer(this.markers);
-
- var countBefore = 0;
- this.features.eachLayer(function(){countBefore++;});
-
- if (action == 'refresh' || action == 'reset') {
- this.features.clearLayers();recreate cluster group because of issues with clearLayer
- - this.map.removeLayer(this.markers);
- this.markers = new L.MarkerClusterGroup(this._clusterOptions);
- this._add(this.model.records.models);
- } else if (action == 'add' && doc){
- this._add(doc);
- } else if (action == 'remove' && doc){
- this._remove(doc);
- }this must come before zooming! -if not: errors when using e.g. circle markers like -āCannot call method āprojectā of undefinedā
- - if (this.state.get('cluster')) {
- this.map.addLayer(this.markers);
- } else {
- this.map.addLayer(this.features);
- }
-
- if (this.state.get('autoZoom')){
- if (this.visible){
- this._zoomToFeatures();
- } else {
- this._zoomPending = true;
- }
- }
- }
- },
-
- show: function() {If the div was hidden, Leaflet needs to recalculate some sizes -to display properly
- - if (this.map){
- this.map.invalidateSize();
- if (this._zoomPending && this.state.get('autoZoom')) {
- this._zoomToFeatures();
- this._zoomPending = false;
- }
- }
- this.visible = true;
- },
-
- hide: function() {
- this.visible = false;
- },
-
- _geomReady: function() {
- return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
- },Private: Add one or n features to the map
-For each record 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 record fields.
- - _add: function(docs){
- var self = this;
-
- if (!(docs instanceof Array)) docs = [docs];
-
- var count = 0;
- var wrongSoFar = 0;
- _.every(docs, function(doc){
- count += 1;
- var feature = self._getGeometryFromRecord(doc);
- if (typeof feature === 'undefined' || feature === null){Empty field
- - return true;
- } else if (feature instanceof Object){
- feature.properties = {
- popupContent: self.infobox(doc),Add a reference to the model id, which will allow us to -link this Leaflet layer to a Recline doc
- - cid: doc.cid
- };
-
- try {
- self.features.addData(feature);
- } catch (except) {
- wrongSoFar += 1;
- var msg = 'Wrong geometry value';
- if (except.message) msg += ' (' + except.message + ')';
- if (wrongSoFar <= 10) {
- self.trigger('recline:flash', {message: msg, category:'error'});
- }
- }
- } else {
- wrongSoFar += 1;
- if (wrongSoFar <= 10) {
- self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
- }
- }
- return true;
- });
- },Private: Remove one or n features from the map
- - _remove: function(docs){
-
- var self = this;
-
- if (!(docs instanceof Array)) docs = [docs];
-
- _.each(docs,function(doc){
- for (var key in self.features._layers){
- if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){
- self.features.removeLayer(self.features._layers[key]);
- }
- }
- });
-
- },Private: convert DMS coordinates to decimal
-north and east are positive, south and west are negative
- - _parseCoordinateString: function(coord){
- if (typeof(coord) != 'string') {
- return(parseFloat(coord));
- }
- var dms = coord.split(/[^-?\.\d\w]+/);
- var deg = 0; var m = 0;
- var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec
- var i;
- for (i = 0; i < dms.length; ++i) {
- if (isNaN(parseFloat(dms[i]))) {
- continue;
- }
- deg += parseFloat(dms[i]) / toDeg[m];
- m += 1;
- }
- if (coord.match(/[SW]/)) {
- deg = -1*deg;
- }
- return(deg);
- },Private: Return a GeoJSON geomtry extracted from the record fields
- - _getGeometryFromRecord: function(doc){
- if (this.state.get('geomField')){
- var value = doc.get(this.state.get('geomField'));
- if (typeof(value) === 'string'){We may have a GeoJSON string representation
- - try {
- value = $.parseJSON(value);
- } catch(e) {}
- }
- if (typeof(value) === 'string') {
- value = value.replace('(', '').replace(')', '');
- var parts = value.split(',');
- var lat = this._parseCoordinateString(parts[0]);
- var lon = this._parseCoordinateString(parts[1]);
-
- if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
- return {
- "type": "Point",
- "coordinates": [lon, lat]
- };
- } else {
- return null;
- }
- } else if (value && _.isArray(value)) {[ lon, lat ]
- - return {
- "type": "Point",
- "coordinates": [value[0], value[1]]
- };
- } else if (value && value.lat) {of form { lat: ā¦, lon: ā¦}
- - return {
- "type": "Point",
- "coordinates": [value.lon || value.lng, value.lat]
- };
- }We o/w assume that contents of the field are a valid GeoJSON object
- - return value;
- } else if (this.state.get('lonField') && this.state.get('latField')){Weāll create a GeoJSON like point object from the two lat/lon fields
- - var lon = doc.get(this.state.get('lonField'));
- var lat = doc.get(this.state.get('latField'));
- lon = this._parseCoordinateString(lon);
- lat = this._parseCoordinateString(lat);
-
- if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
- return {
- type: 'Point',
- coordinates: [lon,lat]
- };
- }
- }
- 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(){should not overwrite if we have already set this (e.g. explicitly via state)
- - if (!this._geomReady()) {
- this.state.set({
- geomField: this._checkField(this.geometryFieldNames),
- latField: this._checkField(this.latitudeFieldNames),
- lonField: this._checkField(this.longitudeFieldNames)
- });
- this.menu.state.set(this.state.toJSON());
- }
- },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: Zoom to map to current features extent if any, or to the full -extent if none.
- - _zoomToFeatures: function(){
- var bounds = this.features.getBounds();
- if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
- this.map.fitBounds(bounds);
- } else {
- this.map.setView([0, 0], 2);
- }
- },Private: Sets up the Leaflet map control and the features layer.
-The map uses a base layer from OpenStreetMap based -on OpenStreetMap data.
- - _setupMap: function(){
- var self = this;
- this.map = new L.Map(this.$map.get(0));
-
- var mapUrl = "http://otile{s}-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.markers = new L.MarkerClusterGroup(this._clusterOptions);rebind this (as needed in e.g. default case above)
- - this.geoJsonLayerOptions.pointToLayer = _.bind(
- this.geoJsonLayerOptions.pointToLayer,
- this);
- this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
-
- this.map.setView([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;
- }
- });
- }
- }
-});
-
-my.MapMenu = Backbone.View.extend({
- className: 'editor',
-
- template: ' \
- <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 class="form-control"> \
- <option value=""></option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- <label>Longitude field</label> \
- <div class="input editor-lon-field"> \
- <select class="form-control"> \
- <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 class="form-control"> \
- <option value=""></option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- </div> \
- </div> \
- <div class="editor-buttons"> \
- <button class="btn btn-default editor-update-map">Update</button> \
- </div> \
- <div class="editor-options" > \
- <label class="checkbox"> \
- <input type="checkbox" id="editor-auto-zoom" value="autozoom" checked="checked" /> \
- Auto zoom to features</label> \
- <label class="checkbox"> \
- <input type="checkbox" id="editor-cluster" value="cluster"/> \
- Cluster markers</label> \
- </div> \
- <input type="hidden" class="editor-id" value="map-1" /> \
- </form> \
- ',Define here events for UI elements
- - events: {
- 'click .editor-update-map': 'onEditorSubmit',
- 'change .editor-field-type': 'onFieldTypeChange',
- 'click #editor-auto-zoom': 'onAutoZoomChange',
- 'click #editor-cluster': 'onClusteringChange'
- },
-
- initialize: function(options) {
- var self = this;
- _.bindAll(this, 'render');
- this.listenTo(this.model.fields, 'change', this.render);
- this.state = new recline.Model.ObjectState(options.state);
- this.listenTo(this.state, 'change', this.render);
- this.render();
- },Also sets up the editor fields and the map if necessary.
- - render: function() {
- var self = this;
- var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
- this.$el.html(htmls);
-
- if (this._geomReady() && this.model.fields.length){
- if (this.state.get('geomField')){
- this._selectOption('editor-geom-field',this.state.get('geomField'));
- this.$el.find('#editor-field-type-geom').attr('checked','checked').change();
- } else{
- this._selectOption('editor-lon-field',this.state.get('lonField'));
- this._selectOption('editor-lat-field',this.state.get('latField'));
- this.$el.find('#editor-field-type-latlon').attr('checked','checked').change();
- }
- }
- if (this.state.get('autoZoom')) {
- this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
- } else {
- this.$el.find('#editor-auto-zoom').removeAttr('checked');
- }
- if (this.state.get('cluster')) {
- this.$el.find('#editor-cluster').attr('checked', 'checked');
- } else {
- this.$el.find('#editor-cluster').removeAttr('checked');
- }
- return this;
- },
-
- _geomReady: function() {
- return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
- },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 (this.$el.find('#editor-field-type-geom').attr('checked')){
- this.state.set({
- geomField: this.$el.find('.editor-geom-field > select > option:selected').val(),
- lonField: null,
- latField: null
- });
- } else {
- this.state.set({
- geomField: null,
- lonField: this.$el.find('.editor-lon-field > select > option:selected').val(),
- latField: this.$el.find('.editor-lat-field > select > option:selected').val()
- });
- }
- return false;
- },Public: Shows the relevant select lists depending on the location field -type selected.
- - onFieldTypeChange: function(e){
- if (e.target.value == 'geom'){
- this.$el.find('.editor-field-type-geom').show();
- this.$el.find('.editor-field-type-latlon').hide();
- } else {
- this.$el.find('.editor-field-type-geom').hide();
- this.$el.find('.editor-field-type-latlon').show();
- }
- },
-
- onAutoZoomChange: function(e){
- this.state.set({autoZoom: !this.state.get('autoZoom')});
- },
-
- onClusteringChange: function(e){
- this.state.set({cluster: !this.state.get('cluster')});
- },Private: Helper function to select an option from a select list
- - _selectOption: function(id,value){
- var options = this.$el.find('.' + id + ' > select > option');
- if (options){
- options.each(function(opt){
- if (this.value == value) {
- $(this).attr('selected','selected');
- return false;
- }
- });
- }
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */Standard JS module setup
- -this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";Manage multiple views together along with query editor etc. Usage:
-
-var myExplorer = new recline.View.MultiView({
- model: {{recline.Model.Dataset instance}}
- el: {{an existing dom element}}
- views: {{dataset views}}
- state: {{state configuration -- see below}}
-});
-
-
-model: (required) recline.model.Dataset instance.
-el: (required) DOM element to bind to. NB: the element already -being in the DOM is important for rendering of some subviews (e.g. -Graph).
-views: (optional) the dataset views (Grid, Graph etc) for -MultiView to show. This is an array of view hashes. If not provided -initialize with (recline.View.)Grid, Graph, and Map views (with obvious id -and labels!).
-
-var views = [
- {
- id: 'grid', // used for routing
- label: 'Grid', // used for view switcher
- view: new recline.View.Grid({
- model: dataset
- })
- },
- {
- id: 'graph',
- label: 'Graph',
- view: new recline.View.Graph({
- model: dataset
- })
- }
-];
-
-
-sidebarViews: (optional) the sidebar views (Filters, Fields) for -MultiView to show. This is an array of view hashes. If not provided -initialize with (recline.View.)FilterEditor and Fields views (with obvious -id and labels!).
-
-var sidebarViews = [
- {
- id: 'filterEditor', // used for routing
- label: 'Filters', // used for view switcher
- view: new recline.View.FilterEditor({
- model: dataset
- })
- },
- {
- id: 'fieldsView',
- label: 'Fields',
- view: new recline.View.Fields({
- model: dataset
- })
- }
-];
-
-
-state: standard state config for this view. This state is slightly - special as it includes config of many of the subviews.
-
-var state = {
- query: {dataset query state - see dataset.queryState object}
- 'view-{id1}': {view-state for this view}
- 'view-{id2}': {view-state for }
- ...
- // Explorer
- currentView: id of current view (defaults to first view if not specified)
- readOnly: (default: false) run in read-only mode
-}
-
-
-Note that at present we do not serialize information about the actual set -of views in use ā e.g. those specified by the views argument ā but instead -expect either that the default views are fine or that the client to have -initialized the MultiView with the relevant views themselves.
- -my.MultiView = Backbone.View.extend({
- template: ' \
- <div class="recline-data-explorer"> \
- <div class="alert-messages"></div> \
- \
- <div class="header clearfix"> \
- <div class="navigation"> \
- <div class="btn-group" data-toggle="buttons-radio"> \
- {{#views}} \
- <button href="#{{id}}" data-view="{{id}}" class="btn btn-default">{{label}}</button> \
- {{/views}} \
- </div> \
- </div> \
- <div class="recline-results-info"> \
- <span class="doc-count">{{recordCount}}</span> records\
- </div> \
- <div class="menu-right"> \
- <div class="btn-group" data-toggle="buttons-checkbox"> \
- {{#sidebarViews}} \
- <button href="#" data-action="{{id}}" class="btn btn-default">{{label}}</button> \
- {{/sidebarViews}} \
- </div> \
- </div> \
- <div class="query-editor-here" style="display:inline;"></div> \
- </div> \
- <div class="data-view-sidebar"></div> \
- <div class="data-view-container"></div> \
- </div> \
- ',
- events: {
- 'click .menu-right button': '_onMenuClick',
- 'click .navigation button': '_onSwitchView'
- },
-
- initialize: function(options) {
- var self = this;
- this._setupState(options.state);Hash of āpageā views (i.e. those for whole page) keyed by page name
- - if (options.views) {
- this.pageViews = options.views;
- } else {
- this.pageViews = [{
- id: 'grid',
- label: 'Grid',
- view: new my.SlickGrid({
- model: this.model,
- state: this.state.get('view-grid')
- })
- }, {
- id: 'graph',
- label: 'Graph',
- view: new my.Graph({
- model: this.model,
- state: this.state.get('view-graph')
- })
- }, {
- id: 'map',
- label: 'Map',
- view: new my.Map({
- model: this.model,
- state: this.state.get('view-map')
- })
- }, {
- id: 'timeline',
- label: 'Timeline',
- view: new my.Timeline({
- model: this.model,
- state: this.state.get('view-timeline')
- })
- }];
- }Hashes of sidebar elements
- - if(options.sidebarViews) {
- this.sidebarViews = options.sidebarViews;
- } else {
- this.sidebarViews = [{
- id: 'filterEditor',
- label: 'Filters',
- view: new my.FilterEditor({
- model: this.model
- })
- }, {
- id: 'fieldsView',
- label: 'Fields',
- view: new my.Fields({
- model: this.model
- })
- }];
- }these must be called after pageViews are created
- - this.render();
- this._bindStateChanges();
- this._bindFlashNotifications();now do updates based on state (need to come after render)
- - if (this.state.get('readOnly')) {
- this.setReadOnly();
- }
- if (this.state.get('currentView')) {
- this.updateNav(this.state.get('currentView'));
- } else {
- this.updateNav(this.pageViews[0].id);
- }
- this._showHideSidebar();
-
- this.listenTo(this.model, 'query:start', function() {
- self.notify({loader: true, persist: true});
- });
- this.listenTo(this.model, 'query:done', function() {
- self.clearNotifications();
- self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown');
- });
- this.listenTo(this.model, 'query:fail', function(error) {
- self.clearNotifications();
- var msg = '';
- if (typeof(error) == 'string') {
- msg = error;
- } else if (typeof(error) == 'object') {
- if (error.title) {
- msg = error.title + ': ';
- }
- if (error.message) {
- msg += error.message;
- }
- } else {
- msg = 'There was an error querying the backend';
- }
- self.notify({message: msg, category: 'error', persist: true});
- });retrieve basic data like fields etc -note this.model and dataset returned are the same -TODO: set query state �
- - this.model.queryState.set(self.state.get('query'), {silent: true});
- },
-
- setReadOnly: function() {
- this.$el.addClass('recline-read-only');
- },
-
- render: function() {
- var tmplData = this.model.toTemplateJSON();
- tmplData.views = this.pageViews;
- tmplData.sidebarViews = this.sidebarViews;
- var template = Mustache.render(this.template, tmplData);
- this.$el.html(template);now create and append other views
- - var $dataViewContainer = this.$el.find('.data-view-container');
- var $dataSidebar = this.$el.find('.data-view-sidebar');the main views
- - _.each(this.pageViews, function(view, pageName) {
- view.view.render();
- if (view.view.redraw) {
- view.view.redraw();
- }
- $dataViewContainer.append(view.view.el);
- if (view.view.elSidebar) {
- $dataSidebar.append(view.view.elSidebar);
- }
- });
-
- _.each(this.sidebarViews, function(view) {
- this['$'+view.id] = view.view.$el;
- $dataSidebar.append(view.view.el);
- }, this);
-
- this.pager = new recline.View.Pager({
- model: this.model
- });
- this.$el.find('.recline-results-info').after(this.pager.el);
-
- this.queryEditor = new recline.View.QueryEditor({
- model: this.model.queryState
- });
- this.$el.find('.query-editor-here').append(this.queryEditor.el);
-
- },
-
- remove: function () {
- _.each(this.pageViews, function (view) {
- view.view.remove();
- });
- _.each(this.sidebarViews, function (view) {
- view.view.remove();
- });
- this.pager.remove();
- this.queryEditor.remove();
- Backbone.View.prototype.remove.apply(this, arguments);
- },hide the sidebar if empty
- - _showHideSidebar: function() {
- var $dataSidebar = this.$el.find('.data-view-sidebar');
- var visibleChildren = $dataSidebar.children().filter(function() {
- return $(this).css("display") != "none";
- }).length;
-
- if (visibleChildren > 0) {
- $dataSidebar.show();
- } else {
- $dataSidebar.hide();
- }
- },
-
- updateNav: function(pageName) {
- this.$el.find('.navigation button').removeClass('active');
- var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]');
- $el.addClass('active');add/remove sidebars and hide inactive views
- - _.each(this.pageViews, function(view, idx) {
- if (view.id === pageName) {
- view.view.$el.show();
- if (view.view.elSidebar) {
- view.view.elSidebar.show();
- }
- } else {
- view.view.$el.hide();
- if (view.view.elSidebar) {
- view.view.elSidebar.hide();
- }
- if (view.view.hide) {
- view.view.hide();
- }
- }
- });
-
- this._showHideSidebar();call view.view.show after sidebar visibility has been determined so -that views can correctly calculate their maximum width
- - _.each(this.pageViews, function(view, idx) {
- if (view.id === pageName) {
- if (view.view.show) {
- view.view.show();
- }
- }
- });
- },
-
- _onMenuClick: function(e) {
- e.preventDefault();
- var action = $(e.target).attr('data-action');
- this['$'+action].toggle();
- this._showHideSidebar();
- },
-
- _onSwitchView: function(e) {
- e.preventDefault();
- var viewName = $(e.target).attr('data-view');
- this.updateNav(viewName);
- this.state.set({currentView: viewName});
- },create a state object for this view and do the job of
-a) initializing it from both data passed in and other sources (e.g. hash url)
-b) ensure the state object is updated in responese to changes in subviews, query etc.
- - _setupState: function(initialState) {
- var self = this;get data from the query string / hash url plus some defaults
- - var qs = my.parseHashQueryString();
- var query = qs.reclineQuery;
- query = query ? JSON.parse(query) : self.model.queryState.toJSON();backwards compatability (now named view-graph but was named graph)
- - var graphState = qs['view-graph'] || qs.graph;
- graphState = graphState ? JSON.parse(graphState) : {};now get default data + hash url plus initial state and initial our state object with it
- - var stateData = _.extend({
- query: query,
- 'view-graph': graphState,
- backend: this.model.backend.__type__,
- url: this.model.get('url'),
- dataset: this.model.toJSON(),
- currentView: null,
- readOnly: false
- },
- initialState);
- this.state = new recline.Model.ObjectState(stateData);
- },
-
- _bindStateChanges: function() {
- var self = this;finally ensure we update our state object when state of sub-object changes so that state is always up to date
- - this.listenTo(this.model.queryState, 'change', function() {
- self.state.set({query: self.model.queryState.toJSON()});
- });
- _.each(this.pageViews, function(pageView) {
- if (pageView.view.state && pageView.view.state.bind) {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();
- self.state.set(update);
- self.listenTo(pageView.view.state, 'change', function() {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();had problems where change not being triggered for e.g. grid view so letās do it explicitly
- - self.state.set(update, {silent: true});
- self.state.trigger('change');
- });
- }
- });
- },
-
- _bindFlashNotifications: function() {
- var self = this;
- _.each(this.pageViews, function(pageView) {
- self.listenTo(pageView.view, 'recline:flash', function(flash) {
- self.notify(flash);
- });
- });
- },Create a notification (a div.alert in div.alert-messsages) using provided -flash object. Flash attributes (all are optional):
- notify: function(flash) {
- var tmplData = _.extend({
- message: 'Loading',
- category: 'warning',
- loader: false
- },
- flash
- );
- var _template;
- if (tmplData.loader) {
- _template = ' \
- <div class="alert alert-info alert-loader"> \
- {{message}} \
- <span class="notification-loader"> </span> \
- </div>';
- } else {
- _template = ' \
- <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">Ć</a> \
- {{message}} \
- </div>';
- }
- var _templated = $(Mustache.render(_template, tmplData));
- _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
- if (!flash.persist) {
- setTimeout(function() {
- $(_templated).fadeOut(1000, function() {
- $(this).remove();
- });
- }, 1000);
- }
- }, clearNotifications: function() {
- var $notifications = $('.recline-data-explorer .alert-messages .alert');
- $notifications.fadeOut(1500, function() {
- $(this).remove();
- });
- }
-});Restore a MultiView instance from a serialized state including the associated dataset
-This inverts the state serialization process in Multiview
- -my.MultiView.restore = function(state) {hack-y - restoring a memory dataset does not mean much ⦠(but useful for testing!)
- - var datasetInfo;
- if (state.backend === 'memory') {
- datasetInfo = {
- backend: 'memory',
- records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
- };
- } else {
- datasetInfo = _.extend({
- url: state.url,
- backend: state.backend
- },
- state.dataset
- );
- }
- var dataset = new recline.Model.Dataset(datasetInfo);
- var explorer = new my.MultiView({
- model: dataset,
- state: state
- });
- return explorer;
-};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) {
- return {};
- } else {
- return {
- path: parsed[1],
- query: parsed[2] || ''
- };
- }
-};Parse a URL query string (?xyz=abcā¦) into a dictionary.
- -my.parseQueryString = function(q) {
- if (!q) {
- return {};
- }
- var urlParams = {},
- e, d = function (s) {
- return unescape(s.replace(/\+/g, " "));
- },
- r = /([^&=]+)=?([^&]*)/g;
-
- 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]);
- }
- return urlParams;
-};Parse the query string out of the URL hash
- -my.parseHashQueryString = function() {
- var q = my.parseHashUrl(window.location.hash).query;
- return my.parseQueryString(q);
-};Compse a Query String
- -my.composeQueryString = function(queryParams) {
- var queryString = '?';
- var items = [];
- $.each(queryParams, function(key, value) {
- if (typeof(value) === 'object') {
- value = JSON.stringify(value);
- }
- items.push(key + '=' + encodeURIComponent(value));
- });
- 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;
- } else {
- return queryPart;
- }
-};
-
-my.setHashQueryString = function(queryParams) {
- window.location.hash = my.getNewHashForQueryString(queryParams);
-};
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";Provides a tabular view on a Dataset, based on SlickGrid.
-https://github.com/mleibman/SlickGrid
-Initialize it with a recline.Model.Dataset.
Additional options to drive SlickGrid grid can be given through state. -The following keys allow for customization:
-For example: - var grid = new recline.View.SlickGrid({ - model: dataset, - el: $el, - state: { - gridOptions: { - editable: true, - enableAddRow: true - // Enable support for row delete - enabledDelRow: true, - // Enable support for row Reorder - enableReOrderRow:true, - ⦠- }, - columnsEditor: [ - {column: ādateā, editor: Slick.Editors.Date }, - {column: ātitleā, editor: Slick.Editors.Text} - ] - } - }); -// NB: you need an explicit height on the element for slickgrid to work
- -my.SlickGrid = Backbone.View.extend({
- initialize: function(modelEtc) {
- var self = this;
- this.$el.addClass('recline-slickgrid');Template for row delete menu , change it if you donāt love
- - this.templates = {
- "deleterow" : '<button href="#" class="recline-row-delete btn btn-default" title="Delete row">X</button>'
- };
-
- _.bindAll(this, 'render', 'onRecordChanged');
- this.listenTo(this.model.records, 'add remove reset', this.render);
- this.listenTo(this.model.records, 'change', this.onRecordChanged);
- var state = _.extend({
- hiddenColumns: [],
- columnsOrder: [],
- columnsSort: {},
- columnsWidth: [],
- columnsEditor: [],
- options: {},
- fitColumns: false
- }, modelEtc.state
-
- );
- this.state = new recline.Model.ObjectState(state);
- this._slickHandler = new Slick.EventHandler();add menu for new row , check if enableAddRow is set to true or not set
- - if(this.state.get("gridOptions")
- && this.state.get("gridOptions").enabledAddRow != undefined
- && this.state.get("gridOptions").enabledAddRow == true ){
- this.editor = new my.GridControl()
- this.elSidebar = this.editor.$el
- this.listenTo(this.editor.state, 'change', function(){
- this.model.records.add(new recline.Model.Record())
- });
- }
- },
-
- onRecordChanged: function(record) {Ignore if the grid is not yet drawn
- - if (!this.grid) {
- return;
- }Letās find the row corresponding to the index
- - var row_index = this.grid.getData().getModelRow( record );
- this.grid.invalidateRow(row_index);
- this.grid.getData().updateItem(record, row_index);
- this.grid.render();
- },
-
- render: function() {
- var self = this;
- var options = _.extend({
- enableCellNavigation: true,
- enableColumnReorder: true,
- explicitInitialization: true,
- syncColumnCellResize: true,
- forceFitColumns: this.state.get('fitColumns')
- }, self.state.get('gridOptions'));We need all columns, even the hidden ones, to show on the column picker
- - var columns = [];custom formatter as default one escapes html -plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ā¦) -row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values
- - var formatter = function(row, cell, value, columnDef, dataContext) {
- if(columnDef.id == "del"){
- return self.templates.deleterow
- }
- var field = self.model.fields.get(columnDef.id);
- if (field.renderer) {
- return field.renderer(value, field, dataContext);
- } else {
- return value
- }
- };we need to be sure that user is entering a valid input , for exemple if -field is date type and field.format =āYY-MM-DDā, we should be sure that -user enter a correct value
- - var validator = function(field) {
- return function(value){
- if (field.type == "date" && isNaN(Date.parse(value))){
- return {
- valid: false,
- msg: "A date is required, check field field-date-format"
- };
- } else {
- return {valid: true, msg :null }
- }
- }
- };Add column for row reorder support
- - if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) {
- columns.push({
- id: "#",
- name: "",
- width: 22,
- behavior: "selectAndMove",
- selectable: false,
- resizable: false,
- cssClass: "recline-cell-reorder"
- })
- }Add column for row delete support
- - if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) {
- columns.push({
- id: 'del',
- name: '',
- field: 'del',
- sortable: true,
- width: 38,
- formatter: formatter,
- validator:validator
- })
- }
-
- function sanitizeFieldName(name) {
- var sanitized = $(name).text();
- return (name !== sanitized && sanitized !== '') ? sanitized : name;
- }
-
- _.each(this.model.fields.toJSON(),function(field){
- var column = {
- id: field.id,
- name: sanitizeFieldName(field.label),
- field: field.id,
- sortable: true,
- minWidth: 80,
- formatter: formatter,
- validator:validator(field)
- };
- var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
- if (widthInfo){
- column.width = widthInfo.width;
- }
- var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
- if (editInfo){
- column.editor = editInfo.editor;
- } else {guess editor type
- - var typeToEditorMap = {
- 'string': Slick.Editors.LongText,
- 'integer': Slick.Editors.IntegerEditor,
- 'number': Slick.Editors.Text,TODO: need a way to ensure we format date in the right way -Plus what if dates are in distant past or future ⦠(?) -ādateā: Slick.Editors.DateEditor,
- - 'date': Slick.Editors.Text,
- 'boolean': Slick.Editors.YesNoSelectEditorTODO: (?) percent ā¦
- - };
- if (field.type in typeToEditorMap) {
- column.editor = typeToEditorMap[field.type]
- } else {
- column.editor = Slick.Editors.LongText;
- }
- }
- columns.push(column);
- });Restrict the visible columns
- - var visibleColumns = _.filter(columns, function(column) {
- return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
- });Order them if there is ordering info on the state
- - if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
- visibleColumns = visibleColumns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
- });
- columns = columns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
- });
- }Move hidden columns to the end, so they appear at the bottom of the -column picker
- - var tempHiddenColumns = [];
- for (var i = columns.length -1; i >= 0; i--){
- if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
- tempHiddenColumns.push(columns.splice(i,1)[0]);
- }
- }
- columns = columns.concat(tempHiddenColumns);Transform a model object into a row
- - function toRow(m) {
- var row = {};
- self.model.fields.each(function(field) {
- var render = "";when adding row from slickgrid the field value is undefined
- - if(!_.isUndefined(m.getFieldValueUnrendered(field))){
- render =m.getFieldValueUnrendered(field)
- }
- row[field.id] = render
- });
- return row;
- }
-
- function RowSet() {
- var models = [];
- var rows = [];
-
- this.push = function(model, row) {
- models.push(model);
- rows.push(row);
- };
-
- this.getLength = function() {return rows.length; };
- this.getItem = function(index) {return rows[index];};
- this.getItemMetadata = function(index) {return {};};
- this.getModel = function(index) {return models[index];};
- this.getModelRow = function(m) {return _.indexOf(models, m);};
- this.updateItem = function(m,i) {
- rows[i] = toRow(m);
- models[i] = m;
- };
- }
-
- var data = new RowSet();
-
- this.model.records.each(function(doc){
- data.push(doc, toRow(doc));
- });
-
- this.grid = new Slick.Grid(this.el, data, visibleColumns, options);Column sorting
- - var sortInfo = this.model.queryState.get('sort');
- if (sortInfo){
- var column = sortInfo[0].field;
- var sortAsc = sortInfo[0].order !== 'desc';
- this.grid.setSortColumn(column, sortAsc);
- }
-
- if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
- this._setupRowReordering();
- }
-
- this._slickHandler.subscribe(this.grid.onSort, function(e, args){
- var order = (args.sortAsc) ? 'asc':'desc';
- var sort = [{
- field: args.sortCol.field,
- order: order
- }];
- self.model.query({sort: sort});
- });
-
- this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
- self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
- });
-
- this.grid.onColumnsResized.subscribe(function(e, args){
- var columns = args.grid.getColumns();
- var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
- var columnsWidth = [];
- _.each(columns,function(column){
- if (column.width != defaultColumnWidth){
- columnsWidth.push({column:column.id,width:column.width});
- }
- });
- self.state.set({columnsWidth:columnsWidth});
- });
-
- this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) {We need to change the model associated value
- - var grid = args.grid;
- var model = data.getModel(args.row);
- var field = grid.getColumns()[args.cell].id;
- var v = {};
- v[field] = args.item[field];
- model.set(v);
- });
- this._slickHandler.subscribe(this.grid.onClick,function(e, args){try catch , because this fail in qunit , but no -error on browser.
- - try{e.preventDefault()}catch(e){}The cell of grid that handle row delete is The first cell (0) if -The grid ReOrder is not present ie enableReOrderRow == false -else it is The the second cell (1) , because The 0 is now cell -that handle row Reoder.
- - var cell =0
- if(self.state.get("gridOptions")
- && self.state.get("gridOptions").enableReOrderRow != undefined
- && self.state.get("gridOptions").enableReOrderRow == true ){
- cell =1
- }
- if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){We need to delete the associated model
- - var model = data.getModel(args.row);
- model.destroy()
- }
- }) ;
- var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
- _.extend(options,{state:this.state}));
- if (self.visible){
- self.grid.init();
- self.rendered = true;
- } else {Defer rendering until the view is visible
- - self.rendered = false;
- }
- return this;
- },Row reordering support based on -https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example9-row-reordering.html
- - _setupRowReordering: function() {
- var self = this;
- self.grid.setSelectionModel(new Slick.RowSelectionModel());
-
- var moveRowsPlugin = new Slick.RowMoveManager({
- cancelEditOnDrag: true
- });
-
- moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) {
- for (var i = 0; i < data.rows.length; i++) {no point in moving before or after itself
- - if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) {
- e.stopPropagation();
- return false;
- }
- }
- return true;
- });
-
- moveRowsPlugin.onMoveRows.subscribe(function (e, args) {
- var extractedRows = [], left, right;
- var rows = args.rows;
- var insertBefore = args.insertBefore;
-
- var data = self.model.records.toJSON()
- left = data.slice(0, insertBefore);
- right= data.slice(insertBefore, data.length);
-
- rows.sort(function(a,b) { return a-b; });
-
- for (var i = 0; i < rows.length; i++) {
- extractedRows.push(data[rows[i]]);
- }
-
- rows.reverse();
-
- for (var i = 0; i < rows.length; i++) {
- var row = rows[i];
- if (row < insertBefore) {
- left.splice(row, 1);
- } else {
- right.splice(row - insertBefore, 1);
- }
- }
-
- data = left.concat(extractedRows.concat(right));
- var selectedRows = [];
- for (var i = 0; i < rows.length; i++)
- selectedRows.push(left.length + i);
-
- self.model.records.reset(data)
-
- });register The plugin to handle row Reorder
- - if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
- self.grid.registerPlugin(moveRowsPlugin);
- }
- },
-
- remove: function () {
- this._slickHandler.unsubscribeAll();
- Backbone.View.prototype.remove.apply(this, arguments);
- },
-
- show: function() {If the div is hidden, SlickGrid will calculate wrongly some -sizes so we must render it explicitly when the view is visible
- - if (!this.rendered){
- if (!this.grid){
- this.render();
- }
- this.grid.init();
- this.rendered = true;
- }
- this.visible = true;
- },
-
- hide: function() {
- this.visible = false;
- }
-});Add new grid Control to display a new row add menu bouton -It display a simple side-bar menu ,for user to add new -row to grid
- -my.GridControl= Backbone.View.extend({
- className: "recline-row-add",Template for row edit menu , change it if you donāt love
- - template: '<h1><button href="#" class="recline-row-add btn btn-default">Add row</button></h1>',
-
- initialize: function(options){
- var self = this;
- _.bindAll(this, 'render');
- this.state = new recline.Model.ObjectState();
- this.render();
- },
-
- render: function() {
- var self = this;
- this.$el.html(this.template)
- },
-
- events : {
- "click .recline-row-add" : "addNewRow"
- },
-
- addNewRow : function(e){
- e.preventDefault()
- this.state.trigger("change")
- }
-});
-
-})(jQuery, recline.View);
-
-/*
-* Context menu for the column picker, adapted from
-* http://mleibman.github.com/SlickGrid/examples/example-grouping
-*
-*/
-(function ($) {
- function SlickColumnPicker(columns, grid, options) {
- var $menu;
- var columnCheckboxes;
-
- var defaults = {
- fadeSpeed:250
- };
-
- function init() {
- grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
- options = $.extend({}, defaults, options);
-
- $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
-
- $menu.bind('mouseleave', function (e) {
- $(this).fadeOut(options.fadeSpeed);
- });
- $menu.bind('click', updateColumn);
-
- }
-
- function handleHeaderContextMenu(e, args) {
- e.preventDefault();
- $menu.empty();
- columnCheckboxes = [];
-
- var $li, $input;
- for (var i = 0; i < columns.length; i++) {
- $li = $('<li />').appendTo($menu);
- $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
- columnCheckboxes.push($input);
-
- if (grid.getColumnIndex(columns[i].id) !== null) {
- $input.attr('checked', 'checked');
- }
- $input.appendTo($li);
- $('<label />')
- .text(columns[i].name)
- .attr('for','slick-column-vis-'+columns[i].id)
- .appendTo($li);
- }
- $('<li/>').addClass('divider').appendTo($menu);
- $li = $('<li />').data('option', 'autoresize').appendTo($menu);
- $input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
- $input.appendTo($li);
- $('<label />')
- .text('Force fit columns')
- .attr('for','slick-option-autoresize')
- .appendTo($li);
- if (grid.getOptions().forceFitColumns) {
- $input.attr('checked', 'checked');
- }
-
- $menu.css('top', e.pageY - 10)
- .css('left', e.pageX - 10)
- .fadeIn(options.fadeSpeed);
- }
-
- function updateColumn(e) {
- var checkbox;
-
- if ($(e.target).data('option') === 'autoresize') {
- var checked;
- if ($(e.target).is('li')){
- checkbox = $(e.target).find('input').first();
- checked = !checkbox.is(':checked');
- checkbox.attr('checked',checked);
- } else {
- checked = e.target.checked;
- }
-
- if (checked) {
- grid.setOptions({forceFitColumns:true});
- grid.autosizeColumns();
- } else {
- grid.setOptions({forceFitColumns:false});
- }
- options.state.set({fitColumns:checked});
- return;
- }
-
- if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
- $(e.target).is('input')) {
- if ($(e.target).is('li')){
- checkbox = $(e.target).find('input').first();
- checkbox.attr('checked',!checkbox.is(':checked'));
- }
- var visibleColumns = [];
- var hiddenColumnsIds = [];
- $.each(columnCheckboxes, function (i, e) {
- if ($(this).is(':checked')) {
- visibleColumns.push(columns[i]);
- } else {
- hiddenColumnsIds.push(columns[i].id);
- }
- });
-
- if (!visibleColumns.length) {
- $(e.target).attr('checked', 'checked');
- return;
- }
-
- grid.setColumns(visibleColumns);
- options.state.set({hiddenColumns:hiddenColumnsIds});
- }
- }
- init();
- }Slick.Controls.ColumnPicker
- - $.extend(true, window, {
- Slick: {
- Controls: {
- ColumnPicker: SlickColumnPicker
- }
- }
- });
-
-})(jQuery);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";turn off unnecessary logging from VMM Timeline
- -if (typeof VMM !== 'undefined') {
- VMM.debug = false;
-}my.Timeline = Backbone.View.extend({
- template: ' \
- <div class="recline-timeline"> \
- <div id="vmm-timeline-id"></div> \
- </div> \
- ',These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define these fields on initialization
- - startFieldNames: ['date','startdate', 'start', 'start-date'],
- endFieldNames: ['end','endDate'],
- elementId: '#vmm-timeline-id',
-
- initialize: function(options) {
- var self = this;
- this.timeline = new VMM.Timeline(this.elementId);
- this._timelineIsInitialized = false;
- this.listenTo(this.model.fields, 'reset', function() {
- self._setupTemporalField();
- });
- this.listenTo(this.model.records, 'all', function() {
- self.reloadData();
- });
- var stateData = _.extend({
- startField: null,
- endField: null,by default timelinejs (and browsers) will parse ambiguous dates in US format (mm/dd/yyyy) -set to true to interpret dd/dd/dddd as dd/mm/yyyy
- - nonUSDates: false,
- timelineJSOptions: {}
- },
- options.state
- );
- this.state = new recline.Model.ObjectState(stateData);
- this._setupTemporalField();
- },
-
- render: function() {
- var tmplData = {};
- var htmls = Mustache.render(this.template, tmplData);
- this.$el.html(htmls);can only call _initTimeline once view in DOM as Timeline uses $ -internally to look up element
- - if ($(this.elementId).length > 0) {
- this._initTimeline();
- }
- },
-
- show: function() {only call _initTimeline once view in DOM as Timeline uses $ internally to look up element
- - if (this._timelineIsInitialized === false) {
- this._initTimeline();
- }
- },
-
- _initTimeline: function() {
- var data = this._timelineJSON();
- var config = this.state.get("timelineJSOptions");
- config.id = this.elementId;
- this.timeline.init(config, data);
- this._timelineIsInitialized = true
- },
-
- reloadData: function() {
- if (this._timelineIsInitialized) {
- var data = this._timelineJSON();
- this.timeline.reload(data);
- }
- }, convertRecord: function(record, fields) {
- return this._convertRecord(record, fields);
- },Internal method to generate a Timeline formatted entry
- - _convertRecord: function(record, fields) {
- var start = this._parseDate(record.get(this.state.get('startField')));
- var end = this._parseDate(record.get(this.state.get('endField')));
- if (start) {
- var tlEntry = {
- "startDate": start,
- "endDate": end,
- "headline": String(record.get('title') || ''),
- "text": record.get('description') || record.summary(),
- "tag": record.get('tags')
- };
- return tlEntry;
- } else {
- return null;
- }
- },
-
- _timelineJSON: function() {
- var self = this;
- var out = {
- 'timeline': {
- 'type': 'default',
- 'headline': '',
- 'date': [
- ]
- }
- };
- this.model.records.each(function(record) {
- var newEntry = self.convertRecord(record, self.fields);
- if (newEntry) {
- out.timeline.date.push(newEntry);
- }
- });if no entries create a placeholder entry to prevent Timeline crashing with error
- - if (out.timeline.date.length === 0) {
- var tlEntry = {
- "startDate": '2000,1,1',
- "headline": 'No data to show!'
- };
- out.timeline.date.push(tlEntry);
- }
- return out;
- },convert dates into a format TimelineJS will handle -TimelineJS does not document this at all so combo of read the code + -trial and error -Summary (AFAICt): -Preferred: [-]yyyy[,mm,dd,hh,mm,ss] -Supported: mm/dd/yyyy
- - _parseDate: function(date) {
- if (!date) {
- return null;
- }
- var out = $.trim(date);
- out = out.replace(/(\d)th/g, '$1');
- out = out.replace(/(\d)st/g, '$1');
- out = $.trim(out);
- if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
- out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
- }
- if (out.match(/\d\d-\d\d-\d\d.*/)) {
- out = out.replace(/-/g, '/');
- }
- if (this.state.get('nonUSDates')) {
- var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
- if (parts) {
- out = [parts[2], parts[1], parts[3]].join('/');
- }
- }
- return out;
- },
-
- _setupTemporalField: function() {
- this.state.set({
- startField: this._checkField(this.startFieldNames),
- endField: this._checkField(this.endFieldNames)
- });
- },
-
- _checkField: function(possibleFieldNames) {
- var modelFieldNames = this.model.fields.pluck('id');
- for (var i = 0; i < possibleFieldNames.length; i++){
- for (var j = 0; j < modelFieldNames.length; j++){
- if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
- return modelFieldNames[j];
- }
- }
- return null;
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";Widget for displaying facets
-Usage:
- var viewer = new FacetViewer({
- model: dataset
- });
-
- my.FacetViewer = Backbone.View.extend({
- className: 'recline-facet-viewer',
- template: ' \
- <div class="facets"> \
- {{#facets}} \
- <div class="facet-summary" data-facet="{{id}}"> \
- <h3> \
- {{id}} \
- </h3> \
- <ul class="facet-items"> \
- {{#terms}} \
- <li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{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}} \
- </div> \
- ',
-
- events: {
- 'click .js-facet-filter': 'onFacetFilter'
- },
- initialize: function(model) {
- _.bindAll(this, 'render');
- this.listenTo(this.model.facets, 'all', this.render);
- this.listenTo(this.model.fields, 'all', this.render);
- this.render();
- },
- render: function() {
- var tmplData = {
- fields: this.model.fields.toJSON()
- };
- tmplData.facets = _.map(this.model.facets.toJSON(), 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.render(this.template, tmplData);
- this.$el.html(templated);are there actually any facets to show?
- - if (this.model.facets.length > 0) {
- this.$el.show();
- } else {
- this.$el.hide();
- }
- },
- onHide: function(e) {
- e.preventDefault();
- this.$el.hide();
- },
- onFacetFilter: function(e) {
- e.preventDefault();
- var $target= $(e.target);
- var fieldId = $target.closest('.facet-summary').attr('data-facet');
- var value = $target.attr('data-value');
- this.model.queryState.addFilter({type: 'term', field: fieldId, term: value});have to trigger explicitly for some reason
- - this.model.query();
- }
-});
-
-
-})(jQuery, recline.View);/*jshint multistr:true */Editor ā to change type (and possibly format) -Editor for show/hide ā¦
- -Box to boot transform editor ā¦
- -
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";
-
-my.Fields = Backbone.View.extend({
- className: 'recline-fields-view',
- template: ' \
- <div class="panel-group fields-list well"> \
- <h3>Fields <a href="#" class="js-show-hide">+</a></h3> \
- {{#fields}} \
- <div class="panel panel-default field"> \
- <div class="panel-heading"> \
- <i class="glyphicon glyphicon-file"></i> \
- <h4> \
- {{label}} \
- <small> \
- {{type}} \
- <a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> » </a> \
- </small> \
- </h4> \
- </div> \
- <div id="collapse{{id}}" class="panel-collapse collapse"> \
- <div class="panel-body"> \
- {{#facets}} \
- <div class="facet-summary" data-facet="{{id}}"> \
- <ul class="facet-items"> \
- {{#terms}} \
- <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
- {{/terms}} \
- </ul> \
- </div> \
- {{/facets}} \
- <div class="clear"></div> \
- </div> \
- </div> \
- </div> \
- {{/fields}} \
- </div> \
- ',
-
- initialize: function(model) {
- var self = this;
- _.bindAll(this, 'render');TODO: this is quite restrictive in terms of when it is re-run -e.g. a change in type will not trigger a re-run atm. -being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width)
- - this.listenTo(this.model.fields, 'reset', function(action) {
- self.model.fields.each(function(field) {
- field.facets.unbind('all', self.render);
- field.facets.bind('all', self.render);
- });fields can get reset or changed in which case we need to recalculate
- - self.model.getFieldsSummary();
- self.render();
- });
- this.$el.find('.collapse').collapse();
- this.render();
- },
- render: function() {
- var self = this;
- var tmplData = {
- fields: []
- };
- this.model.fields.each(function(field) {
- var out = field.toJSON();
- out.facets = field.facets.toJSON();
- tmplData.fields.push(out);
- });
- var templated = Mustache.render(this.template, tmplData);
- this.$el.html(templated);
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";
-
-my.FilterEditor = Backbone.View.extend({
- className: 'recline-filter-editor well',
- template: ' \
- <div class="filters"> \
- <h3>Filters</h3> \
- <a href="#" class="js-add-filter">Add filter</a> \
- <form class="form-stacked js-add" style="display: none;"> \
- <div class="form-group"> \
- <label>Field</label> \
- <select class="fields form-control"> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- <div class="form-group"> \
- <label>Filter type</label> \
- <select class="filterType form-control"> \
- <option value="term">Value</option> \
- <option value="range">Range</option> \
- <option value="geo_distance">Geo distance</option> \
- </select> \
- </div> \
- <button type="submit" class="btn btn-default">Add</button> \
- </form> \
- <form class="form-stacked js-edit"> \
- {{#filters}} \
- {{{filterRender}}} \
- {{/filters}} \
- {{#filters.length}} \
- <button type="submit" class="btn btn-default">Update</button> \
- {{/filters.length}} \
- </form> \
- </div> \
- ',
- filterTemplates: {
- term: ' \
- <div class="filter-{{type}} filter"> \
- <fieldset> \
- <legend> \
- {{field}} <small>{{type}}</small> \
- <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
- </legend> \
- <input class="input-sm" type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </fieldset> \
- </div> \
- ',
- range: ' \
- <div class="filter-{{type}} filter"> \
- <fieldset> \
- <legend> \
- {{field}} <small>{{type}}</small> \
- <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
- </legend> \
- <div class="form-group"> \
- <label class="control-label" for="">From</label> \
- <input class="input-sm" type="text" value="{{from}}" name="from" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </div> \
- <div class="form-group"> \
- <label class="control-label" for="">To</label> \
- <input class="input-sm" type="text" value="{{to}}" name="to" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </div> \
- </fieldset> \
- </div> \
- ',
- geo_distance: ' \
- <div class="filter-{{type}} filter"> \
- <fieldset> \
- <legend> \
- {{field}} <small>{{type}}</small> \
- <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
- </legend> \
- <div class="form-group"> \
- <label class="control-label" for="">Longitude</label> \
- <input class="input-sm" type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </div> \
- <div class="form-group"> \
- <label class="control-label" for="">Latitude</label> \
- <input class="input-sm" type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </div> \
- <div class="form-group"> \
- <label class="control-label" for="">Distance (km)</label> \
- <input class="input-sm" type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </div> \
- </fieldset> \
- </div> \
- '
- },
- events: {
- 'click .js-remove-filter': 'onRemoveFilter',
- 'click .js-add-filter': 'onAddFilterShow',
- 'submit form.js-edit': 'onTermFiltersUpdate',
- 'submit form.js-add': 'onAddFilter'
- },
- initialize: function() {
- _.bindAll(this, 'render');
- this.listenTo(this.model.fields, 'all', this.render);
- this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
- this.render();
- },
- render: function() {
- var self = this;
- var tmplData = $.extend(true, {}, this.model.queryState.toJSON());we will use idx in list as there id ā¦
- - tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
- filter.id = idx;
- return filter;
- });
- tmplData.fields = this.model.fields.toJSON();
- tmplData.filterRender = function() {
- return Mustache.render(self.filterTemplates[this.type], this);
- };
- var out = Mustache.render(this.template, tmplData);
- this.$el.html(out);
- },
- onAddFilterShow: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- this.$el.find('form.js-add').show();
- },
- onAddFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- var filterType = $target.find('select.filterType').val();
- var field = $target.find('select.fields').val();
- this.model.queryState.addFilter({type: filterType, field: field});
- },
- onRemoveFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- var filterId = $target.attr('data-filter-id');
- this.model.queryState.removeFilter(filterId);
- },
- onTermFiltersUpdate: function(e) {
- var self = this;
- e.preventDefault();
- var filters = self.model.queryState.get('filters');
- var $form = $(e.target);
- _.each($form.find('input'), function(input) {
- var $input = $(input);
- var filterType = $input.attr('data-filter-type');
- var fieldId = $input.attr('data-filter-field');
- var filterIndex = parseInt($input.attr('data-filter-id'), 10);
- var name = $input.attr('name');
- var value = $input.val();
-
- switch (filterType) {
- case 'term':
- filters[filterIndex].term = value;
- break;
- case 'range':
- filters[filterIndex][name] = value;
- break;
- case 'geo_distance':
- if(name === 'distance') {
- filters[filterIndex].distance = parseFloat(value);
- }
- else {
- filters[filterIndex].point[name] = parseFloat(value);
- }
- break;
- }
- });
- self.model.queryState.set({filters: filters, from: 0});
- self.model.queryState.trigger('change');
- }
-});
-
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";
-
-my.Pager = Backbone.View.extend({
- className: 'recline-pager',
- template: ' \
- <div class="pagination"> \
- <ul class="pagination"> \
- <li class="prev action-pagination-update"><a href="" class="btn btn-default">«</a></li> \
- <li class="page-range"><a><label for="from">From</label><input id="from" name="from" type="text" value="{{from}}" /> – <label for="to">To</label><input id="to" name="to" type="text" value="{{to}}" /> </a></li> \
- <li class="next action-pagination-update"><a href="" class="btn btn-default">»</a></li> \
- </ul> \
- </div> \
- ',
-
- events: {
- 'click .action-pagination-update': 'onPaginationUpdate',
- 'change input': 'onFormSubmit'
- },
-
- initialize: function() {
- _.bindAll(this, 'render');
- this.listenTo(this.model.queryState, 'change', this.render);
- this.render();
- },
- onFormSubmit: function(e) {
- e.preventDefault();filter is 0-based; form is 1-based
- - var formFrom = parseInt(this.$el.find('input[name="from"]').val())-1;
- var formTo = parseInt(this.$el.find('input[name="to"]').val())-1;
- var maxRecord = this.model.recordCount-1;
- if (this.model.queryState.get('from') != formFrom) { // changed from; update from
- this.model.queryState.set({from: Math.min(maxRecord, Math.max(formFrom, 0))});
- } else if (this.model.queryState.get('to') != formTo) { // change to; update size
- var to = Math.min(maxRecord, Math.max(formTo, 0));
- this.model.queryState.set({size: Math.min(maxRecord+1, Math.max(to-formFrom+1, 1))});
- }
- },
- onPaginationUpdate: function(e) {
- e.preventDefault();
- var $el = $(e.target);
- var newFrom = 0;
- var currFrom = this.model.queryState.get('from');
- var size = this.model.queryState.get('size');
- var updateQuery = false;
- if ($el.parent().hasClass('prev')) {
- newFrom = Math.max(currFrom - Math.max(0, size), 0);
- updateQuery = newFrom != currFrom;
- } else {
- newFrom = Math.max(currFrom + size, 0);
- updateQuery = (newFrom < this.model.recordCount);
- }
- if (updateQuery) {
- this.model.queryState.set({from: newFrom});
- }
- },
- render: function() {
- var tmplData = this.model.toJSON();
- var from = parseInt(this.model.queryState.get('from'));
- tmplData.from = from+1;
- tmplData.to = Math.min(from+this.model.queryState.get('size'), this.model.recordCount);
- var templated = Mustache.render(this.template, tmplData);
- this.$el.html(templated);
- return this;
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";
-
-my.QueryEditor = Backbone.View.extend({
- className: 'recline-query-editor',
- template: ' \
- <form action="" method="GET" class="form-inline" role="form"> \
- <div class="form-group"> \
- <div class="input-group text-query"> \
- <div class="input-group-addon"> \
- <i class="glyphicon glyphicon-search"></i> \
- </div> \
- <label for="q">Search</label> \
- <input class="form-control search-query" type="text" id="q" name="q" value="{{q}}" placeholder="Search data ..."> \
- </div> \
- </div> \
- <button type="submit" class="btn btn-default">Go »</button> \
- </form> \
- ',
-
- events: {
- 'submit form': 'onFormSubmit'
- },
-
- initialize: function() {
- _.bindAll(this, 'render');
- this.listenTo(this.model, 'change', this.render);
- this.render();
- },
- onFormSubmit: function(e) {
- e.preventDefault();
- var query = this.$el.find('.search-query').val();
- this.model.set({q: query});
- },
- render: function() {
- var tmplData = this.model.toJSON();
- var templated = Mustache.render(this.template, tmplData);
- this.$el.html(templated);
- }
-});
-
-})(jQuery, recline.View);/*jshint multistr:true */
-
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict";
-
-my.ValueFilter = Backbone.View.extend({
- className: 'recline-filter-editor well',
- template: ' \
- <div class="filters"> \
- <h3>Filters</h3> \
- <button class="btn js-add-filter add-filter">Add filter</button> \
- <form class="form-stacked js-add" style="display: none;"> \
- <fieldset> \
- <label>Field</label> \
- <select class="fields form-control"> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- <button type="submit" class="btn">Add</button> \
- </fieldset> \
- </form> \
- <form class="form-stacked js-edit"> \
- {{#filters}} \
- {{{filterRender}}} \
- {{/filters}} \
- {{#filters.length}} \
- <button type="submit" class="btn update-filter">Update</button> \
- {{/filters.length}} \
- </form> \
- </div> \
- ',
- filterTemplates: {
- term: ' \
- <div class="filter-{{type}} filter"> \
- <fieldset> \
- {{field}} \
- <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
- <input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
- </fieldset> \
- </div> \
- '
- },
- events: {
- 'click .js-remove-filter': 'onRemoveFilter',
- 'click .js-add-filter': 'onAddFilterShow',
- 'submit form.js-edit': 'onTermFiltersUpdate',
- 'submit form.js-add': 'onAddFilter'
- },
- initialize: function() {
- _.bindAll(this, 'render');
- this.listenTo(this.model.fields, 'all', this.render);
- this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
- this.render();
- },
- render: function() {
- var self = this;
- var tmplData = $.extend(true, {}, this.model.queryState.toJSON());we will use idx in list as the id ā¦
- - tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
- filter.id = idx;
- return filter;
- });
- tmplData.fields = this.model.fields.toJSON();
- tmplData.filterRender = function() {
- return Mustache.render(self.filterTemplates.term, this);
- };
- var out = Mustache.render(this.template, tmplData);
- this.$el.html(out);
- },
- updateFilter: function(input) {
- var self = this;
- var filters = self.model.queryState.get('filters');
- var $input = $(input);
- var filterIndex = parseInt($input.attr('data-filter-id'), 10);
- var value = $input.val();
- filters[filterIndex].term = value;
- },
- onAddFilterShow: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- this.$el.find('form.js-add').show();
- },
- onAddFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- var field = $target.find('select.fields').val();
- this.model.queryState.addFilter({type: 'term', field: field});
- },
- onRemoveFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- var filterId = $target.attr('data-filter-id');
- this.model.queryState.removeFilter(filterId);
- },
- onTermFiltersUpdate: function(e) {
- var self = this;
- e.preventDefault();
- var filters = self.model.queryState.get('filters');
- var $form = $(e.target);
- _.each($form.find('input'), function(input) {
- self.updateFilter(input);
- });
- self.model.queryState.set({filters: filters, from: 0});
- self.model.queryState.trigger('change');
- }
-});
-
-})(jQuery, recline.View);Backend identifiers -How do you know the backend identifier for a given Backend? It's just the name -of the 'class' in recline.Backend module (but case-insensitive). E.g. -recline.Backend.ElasticSearch can be identified as 'ElasticSearch' or -'elasticsearch'.
-records attribute. If you did want all records loaded into records at the start just requery after fetch has completed:
-
-{% highlight javascript %}
-// for the async case need to put inside of done
-dataset.fetch().done(function() {
- dataset.query({size: dataset.recordCount});
-});
-{% endhighlight %}
-
- initialize: {
- model: {a recline.Model.Dataset instance}
- // el: {do not specify - instead view should create}
- state: {(optional) Object / Hash specifying initial state}
- ...
- }
-
-
-Note: Dataset Views in core Recline have a common layout on disk as follows,
-where ViewName is the named of View class:
-
-
-src/view-{lower-case-ViewName}.js
-css/{lower-case-ViewName}.css
-test/view-{lower-case-ViewName}.js
-
-
-### State
-
-State information exists in order to support state serialization into the url
-or elsewhere and reloading of application from a stored state.
-
-State is available not only for individual views (as described above) but for
-the dataset (e.g. the current query). For an example of pulling together state
-from across multiple components see `recline.View.DataExplorer`.
-
-### Flash Messages / Notifications
-
-To send 'flash messages' or notifications the convention is that views should
-fire an event named `recline:flash` with a payload that is a flash object with
-the following attributes (all optional):
-
-* message: message to show.
-* category: warning (default), success, error
-* persist: if true alert is persistent, o/w hidden after 3s (default=false)
-* loader: if true show a loading message
-
-Objects or views wishing to bind to flash messages may then subscribe to these
-events and take some action such as displaying them to the user. For an example
-of such behaviour see the DataExplorer view.
-
-### Writing your own Views
-
-See the existing Views.
-
-## Internationalization
-
-### Adding translations to templates
-
-Internationalization is implemented with help of [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) library and supports [ICU Message syntax](http://userguide.icu-project.org/formatparse/messages).
-
-Translation keys are using Mustache tags prefixed by `t.`. You can specify translated strings in two ways:
-
-1. Simple text with no variables is rendered using Mustache variable-tag
-```javascript
-$template = '{{ "{{t.Add_row"}}}}'; // will show "Add row" in defaultLocale (English)
-```
-
-2. Text with variables or special characters is rendered using Mustache section-tag
-
-```javascript
-$template = '{{ "{{#t.desc"}}}}Add first_row field{{ "{{/t.desc"}}}}';
-// using special chars; will show "Add first_row field" in defaultLocale
-
-$template = '{{ "{{#t.num_records"}}}}{recordCount, plural, =0 {no records} =1{# record} other {# records}}{{ "{{/t.num_records"}}}}';
-// will show "no records", "1 record" or "x records" in defaultLocale (English)
-```
-
-When using section-tags in existing templates be sure to remove a bracket from variables inside sections:
-```javascript
-#template___notranslation = '{{ "{{recordCount"}}}} records';
-#template_withtranslation = '{{ "{{#t.num_records"}}}} {recordCount} records {{ "{{/t.num_records"}}}}';
-```
-
-
-Then setup Mustache to use translation by injecting tranlation tags in `render` function:
-
-```javascript
-// ============== BEFORE ===================
-
-my.MultiView = Backbone.View.extend({
- render: function() {
- var tmplData = this.model.toTemplateJSON();
- var output = Mustache.render(this.template, tmplData);
- ...
- }
-});
-
-// ============== AFTER ====================
-
-my.MultiView = Backbone.View.extend({
- render: function() {
- var tmplData = this.model.toTemplateJSON();
- tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); // inject Moustache formatter
- var output = Mustache.render(this.template, tmplData);
- ...
- }
-});
-```
-
-### Language resolution
-
-By default the language is detected from the root `lang` attributes - ` and ``.
-
-If you want to override this functionality then override `I18nMessages.languageResolver` with your implementation.
-
-```html
-
-
-```
-
-Libraries can also ask for specific language: `I18nMessages('recline', recline.View.translations, 'pl')`. Language resolver is not used in that case.
-
-If you're creating templates using default language other than English (why?) set appropriately appHardcodedLocale: `I18nMessages('recline', recline.View.translations, undefined, 'pl')`. Then missing strings won't be reported in console and underscores in simple translations will be converted to spaces.
-
-### Adding new language
-
-Create a copy of `src/i18n/pl.js` and translate all the keys. Long English messages can be found in `en.js`.
-
-### Overriding defined messages
-
-If you want to override translations from provided locale do it in a script tag after including recline:
-
-```html
-
-
-```
\ No newline at end of file
diff --git a/download.markdown b/download.markdown
deleted file mode 100644
index 049e2e6d..00000000
--- a/download.markdown
+++ /dev/null
@@ -1,112 +0,0 @@
----
-layout: container
-title: Download
----
-
-The tutorials on this website will usually be based on the latest (master) codebase. It should also be very stable.
-After downloading recline you'll want to use it in your project -- see below or tutorials for details.
- -### Changelog - -[View Changelog on Github](https://github.com/okfn/recline#changelog) - -### Dependencies - -Recline has dependencies on some third-party libraries. Specifically, recline.dataset.js depends on: - -* [Underscore](http://documentcloud.github.com/underscore/) >= 1.0 -* [Underscore Deferred](https://github.com/wookiehangover/underscore.deferred) v0.4.0 -* [Backbone](http://backbonejs.org/) >= 0.5.1 - -Those backends which utilize jquery's ajax method depend on jQuery: - -* [JQuery](http://jquery.com/) >= 1.6 - -All the views require, in addition to those needed for recline.dataset.js: - -* [JQuery](http://jquery.com/) >= 1.6 -* [Mustache.js](https://github.com/janl/mustache.js/) >= 0.5.0-dev (required for all views) - -Individual views have additional dependencies such as: - -* [JQuery Flot](http://www.flotcharts.org/) >= 0.7 (required for for graph view) -* [Leaflet](http://leaflet.cloudmade.com/) >= 0.4.4 (required for map view) -* [Leaflet.markercluster](https://github.com/danzel/Leaflet.markercluster) as of 2012-09-12 (required for marker clustering) -* [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02 (required for the timeline view) -* [Bootstrap](http://twitter.github.com/bootstrap/) >= v3 (default option for CSS and UI JS but you can use your own) - -If you grab the full zipball for Recline this will include all of the relevant -dependencies in the vendor directory and you can also find them at in the -[github repo here](https://github.com/okfn/recline/tree/master/vendor). - -### Example - -Here is an example of the page setup for an app using every Recline component: - -{% highlight html %} - - - - - -{% include recline-deps.html %} -{% endhighlight %} - diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index a1f8bfae..00000000 Binary files a/favicon.ico and /dev/null differ diff --git a/images/drag-handle.png b/images/drag-handle.png deleted file mode 100644 index ad7531cf..00000000 Binary files a/images/drag-handle.png and /dev/null differ diff --git a/images/logo.png b/images/logo.png deleted file mode 100644 index 423f84ed..00000000 Binary files a/images/logo.png and /dev/null differ diff --git a/images/screenshot-1.jpg b/images/screenshot-1.jpg deleted file mode 100644 index eddf1cea..00000000 Binary files a/images/screenshot-1.jpg and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index aea40126..00000000 --- a/index.html +++ /dev/null @@ -1,54 +0,0 @@ ---- -layout: default -title: Home ---- - -
- A simple but powerful library for building data applications in - pure Javascript and HTML.
-- Recline re-uses best-of-breed presentation libraries like - SlickGrid, Leaflet, Flot and D3 to create data 'Views' - and allows you to connect them with your data in seconds. -
-- Documentation » - Tutorials » - Demos » -
- -// Load some data
-var dataset = recline.Model.Dataset({
- records: [
- { value: 1, date: '2012-08-07' },
- { value: 5, b: '2013-09-07' }
- ]
- // Load CSV data instead
- // (And Recline has support for many more data source types)
- // url: 'my-local-csv-file.csv',
- // backend: 'csv'
-});
-
-// get an element from your HTML for the viewer
-var $el = $('#data-viewer');
-
-var allInOneDataViewer = new recline.View.MultiView({
- model: dataset,
- el: $el
-});
-// Your new Data Viewer will be live!
-
- Jakie kolumny powinny zostaÄ narysowane na wykresie?
\ -Wybierz je używajÄ c menu po prawej, a wykres pojawi siÄ automatycznie.
', - Graph_Type: 'Typ wykresu', - Lines_and_Points: 'Linie z punktami', - Lines: 'Linie', - Points: 'Punkty', - Bars: 'SÅupki poziome', - Columns: 'SÅupki', - flot_Group_Column: 'Kolumna (OÅ X)', - Please_choose: 'ProszÄ wybraÄ', - Remove: 'UsuÅ', - Series: 'Seria', - Axis_2: 'OÅ Y', - Add_Series: 'Dodaj seriÄ danych', - Save: 'Zapisz', - - map_mapping: 'ŹródÅo koordynatów', - map_mapping_lat_lon: 'SzerokoÅÄ i dÅugoÅÄ geo.', - map_mapping_geojson: 'Jedna kolumna typu GeoJSON', - Latitude_field: 'Kolumna szerokoÅci geo. (WGS84)', - Longitude_field: 'Kolumna dÅugoÅci geo. (WGS84)', - Auto_zoom_to_features: 'Kadruj, aby pokazaÄ wszytkie punkty', - Cluster_markers: 'ÅÄ cz pobliskie punkty w grupy', - - num_records: '{recordCount} rekordów' -}; \ No newline at end of file diff --git a/src/model.js b/src/model.js deleted file mode 100644 index 1c383b61..00000000 --- a/src/model.js +++ /dev/null @@ -1,651 +0,0 @@ -// # Recline Backbone Models -this.recline = this.recline || {}; -this.recline.Model = this.recline.Model || {}; - -(function(my) { - "use strict"; - -// use either jQuery or Underscore Deferred depending on what is available -var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - -// ## Dataset -my.Dataset = Backbone.Model.extend({ - constructor: function Dataset() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - initialize: function() { - var self = this; - _.bindAll(this, 'query'); - this.backend = null; - if (this.get('backend')) { - this.backend = this._backendFromString(this.get('backend')); - } else { // try to guess backend ... - if (this.get('records')) { - this.backend = recline.Backend.Memory; - } - } - this.fields = new my.FieldList(); - this.records = new my.RecordList(); - this._changes = { - deletes: [], - updates: [], - creates: [] - }; - this.facets = new my.FacetList(); - this.recordCount = null; - this.queryState = new my.Query(); - this.queryState.bind('change facet:add', function () { - self.query(); // We want to call query() without any arguments. - }); - // store is what we query and save against - // store will either be the backend or be a memory store if Backend fetch - // tells us to use memory store - this._store = this.backend; - - // if backend has a handleQueryResultFunction, use that - this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? - this.backend.handleQueryResult : this._handleQueryResult; - if (this.backend == recline.Backend.Memory) { - this.fetch(); - } - }, - - sync: function(method, model, options) { - return this.backend.sync(method, model, options); - }, - - // ### fetch - // - // Retrieve dataset and (some) records from the backend. - fetch: function() { - var self = this; - var dfd = new Deferred(); - - if (this.backend !== recline.Backend.Memory) { - this.backend.fetch(this.toJSON()) - .done(handleResults) - .fail(function(args) { - dfd.reject(args); - }); - } else { - // special case where we have been given data directly - handleResults({ - records: this.get('records'), - fields: this.get('fields'), - useMemoryStore: true - }); - } - - function handleResults(results) { - // if explicitly given the fields - // (e.g. var dataset = new Dataset({fields: fields, ...}) - // use that field info over anything we get back by parsing the data - // (results.fields) - var fields = self.get('fields') || results.fields; - - var out = self._normalizeRecordsAndFields(results.records, fields); - if (results.useMemoryStore) { - self._store = new recline.Backend.Memory.Store(out.records, out.fields); - } - - self.set(results.metadata); - self.fields.reset(out.fields); - self.query() - .done(function() { - dfd.resolve(self); - }) - .fail(function(args) { - dfd.reject(args); - }); - } - - return dfd.promise(); - }, - - // ### _normalizeRecordsAndFields - // - // Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects - // - // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] => - // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}] - _normalizeRecordsAndFields: function(records, fields) { - // if no fields get them from records - if (!fields && records && records.length > 0) { - // records is array then fields is first row of records ... - if (records[0] instanceof Array) { - fields = records[0]; - records = records.slice(1); - } else { - fields = _.map(_.keys(records[0]), function(key) { - return {id: key}; - }); - } - } - - // fields is an array of strings (i.e. list of field headings/ids) - if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) { - // Rename duplicate fieldIds as each field name needs to be - // unique. - var seen = {}; - fields = _.map(fields, function(field, index) { - if (field === null) { - field = ''; - } else { - field = field.toString(); - } - // cannot use trim as not supported by IE7 - var fieldId = field.replace(/^\s+|\s+$/g, ''); - if (fieldId === '') { - fieldId = '_noname_'; - field = fieldId; - } - while (fieldId in seen) { - seen[field] += 1; - fieldId = field + seen[field]; - } - if (!(field in seen)) { - seen[field] = 0; - } - // TODO: decide whether to keep original name as label ... - // return { id: fieldId, label: field || fieldId } - return { id: fieldId }; - }); - } - // records is provided as arrays so need to zip together with fields - // NB: this requires you to have fields to match arrays - if (records && records.length > 0 && records[0] instanceof Array) { - records = _.map(records, function(doc) { - var tmp = {}; - _.each(fields, function(field, idx) { - tmp[field.id] = doc[idx]; - }); - return tmp; - }); - } - return { - fields: fields, - records: records - }; - }, - - save: function() { - var self = this; - // TODO: need to reset the changes ... - return this._store.save(this._changes, this.toJSON()); - }, - - // ### query - // - // AJAX method with promise API to get records from the backend. - // - // It will query based on current query state (given by this.queryState) - // updated by queryObj (if provided). - // - // Resulting RecordList are used to reset this.records and are - // also returned. - query: function(queryObj) { - var self = this; - var dfd = new Deferred(); - this.trigger('query:start'); - - if (queryObj) { - var attributes = queryObj; - if (queryObj instanceof my.Query) { - attributes = queryObj.toJSON(); - } - this.queryState.set(attributes, {silent: true}); - } - var actualQuery = this.queryState.toJSON(); - - this._store.query(actualQuery, this.toJSON()) - .done(function(queryResult) { - self._handleResult(queryResult); - self.trigger('query:done'); - dfd.resolve(self.records); - }) - .fail(function(args) { - self.trigger('query:fail', args); - dfd.reject(args); - }); - return dfd.promise(); - }, - - _handleQueryResult: function(queryResult) { - var self = this; - self.recordCount = queryResult.total; - var docs = _.map(queryResult.hits, function(hit) { - var _doc = new my.Record(hit); - _doc.fields = self.fields; - _doc.bind('change', function(doc) { - self._changes.updates.push(doc.toJSON()); - }); - _doc.bind('destroy', function(doc) { - self._changes.deletes.push(doc.toJSON()); - }); - return _doc; - }); - self.records.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); - } - }, - - toTemplateJSON: function() { - var data = this.toJSON(); - data.recordCount = this.recordCount; - data.fields = this.fields.toJSON(); - return data; - }, - - // ### getFieldsSummary - // - // Get a summary for each field in the form of a `Facet`. - // - // @return null as this is async function. Provides deferred/promise interface. - getFieldsSummary: function() { - var self = this; - var query = new my.Query(); - query.set({size: 0}); - this.fields.each(function(field) { - query.addFacet(field.id); - }); - var dfd = new Deferred(); - this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { - if (queryResult.facets) { - _.each(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - var facet = new my.Facet(facetResult); - // TODO: probably want replace rather than reset (i.e. just replace the facet with this id) - self.fields.get(facetId).facets.reset(facet); - }); - } - dfd.resolve(queryResult); - }); - return dfd.promise(); - }, - - // Deprecated (as of v0.5) - use record.summary() - recordSummary: function(record) { - return record.summary(); - }, - - // ### _backendFromString(backendString) - // - // Look up a backend module from a backend string (look in recline.Backend) - _backendFromString: function(backendString) { - var backend = null; - if (recline && recline.Backend) { - _.each(_.keys(recline.Backend), function(name) { - if (name.toLowerCase() === backendString.toLowerCase()) { - backend = recline.Backend[name]; - } - }); - } - return backend; - } -}); - - -// ## A Record -// -// A single record (or row) in the dataset -my.Record = Backbone.Model.extend({ - constructor: function Record() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - // - // Create a Record - // - // You usually will not do this directly but will have records created by - // Dataset e.g. in query method - // - // Certain methods require presence of a fields attribute (identical to that on Dataset) - initialize: function() { - _.bindAll(this, 'getFieldValue'); - }, - - // ### getFieldValue - // - // For the provided Field get the corresponding rendered computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValue: function(field) { - var val = this.getFieldValueUnrendered(field); - if (field && !_.isUndefined(field.renderer)) { - val = field.renderer(val, field, this.toJSON()); - } - return val; - }, - - // ### getFieldValueUnrendered - // - // For the provided Field get the corresponding computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValueUnrendered: function(field) { - if (!field) { - return ''; - } - var val = this.get(field.id); - if (field.deriver) { - val = field.deriver(val, field, this); - } - return val; - }, - - // ### summary - // - // Get a simple html summary of this record in form of key/value list - summary: function(record) { - var self = this; - var html = 'There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
\ -Please tell us by using the menu on the right and a graph will automatically appear.
{{/t.flot_info}} \ -| \ - {{label}} \ - | \ - {{/fields}} \ -\ - |
|---|
-// var row = new GridRow({
-// model: dataset-record,
-// el: dom-element,
-// fields: mydatasets.fields // a FieldList object
-// });
-//
-my.GridRow = Backbone.View.extend({
- initialize: function(initData) {
- _.bindAll(this, 'render');
- this._fields = initData.fields;
- this.listenTo(this.model, 'change', this.render);
- },
-
- template: ' \
- {{#cells}} \
-
-// {
-// // geomField if specified will be used in preference to lat/lon
-// geomField: {id of field containing geometry in the dataset}
-// lonField: {id of field containing longitude in the dataset}
-// latField: {id of field containing latitude in the dataset}
-// autoZoom: true,
-// // use cluster support
-// // cluster: true = always on
-// // cluster: false = always off
-// cluster: false
-// }
-//
-//
-// Useful attributes to know about (if e.g. customizing)
-//
-// * map: the Leaflet map (L.Map)
-// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON)
-my.Map = Backbone.View.extend({
- template: ' \
-
-// var myExplorer = new recline.View.MultiView({
-// model: {{recline.Model.Dataset instance}}
-// el: {{an existing dom element}}
-// views: {{dataset views}}
-// state: {{state configuration -- see below}}
-// });
-//
-//
-// ### Parameters
-//
-// **model**: (required) recline.model.Dataset instance.
-//
-// **el**: (required) DOM element to bind to. NB: the element already
-// being in the DOM is important for rendering of some subviews (e.g.
-// Graph).
-//
-// **views**: (optional) the dataset views (Grid, Graph etc) for
-// MultiView to show. This is an array of view hashes. If not provided
-// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
-// and labels!).
-//
-//
-// var views = [
-// {
-// id: 'grid', // used for routing
-// label: 'Grid', // used for view switcher
-// view: new recline.View.Grid({
-// model: dataset
-// })
-// },
-// {
-// id: 'graph',
-// label: 'Graph',
-// view: new recline.View.Graph({
-// model: dataset
-// })
-// }
-// ];
-//
-//
-// **sidebarViews**: (optional) the sidebar views (Filters, Fields) for
-// MultiView to show. This is an array of view hashes. If not provided
-// initialize with (recline.View.)FilterEditor and Fields views (with obvious
-// id and labels!).
-//
-//
-// var sidebarViews = [
-// {
-// id: 'filterEditor', // used for routing
-// label: 'Filters', // used for view switcher
-// view: new recline.View.FilterEditor({
-// model: dataset
-// })
-// },
-// {
-// id: 'fieldsView',
-// label: 'Fields',
-// view: new recline.View.Fields({
-// model: dataset
-// })
-// }
-// ];
-//
-//
-// **state**: standard state config for this view. This state is slightly
-// special as it includes config of many of the subviews.
-//
-//
-// var state = {
-// query: {dataset query state - see dataset.queryState object}
-// 'view-{id1}': {view-state for this view}
-// 'view-{id2}': {view-state for }
-// ...
-// // Explorer
-// currentView: id of current view (defaults to first view if not specified)
-// readOnly: (default: false) run in read-only mode
-// }
-//
-//
-// Note that at present we do *not* serialize information about the actual set
-// of views in use -- e.g. those specified by the views argument -- but instead
-// expect either that the default views are fine or that the client to have
-// initialized the MultiView with the relevant views themselves.
-my.MultiView = Backbone.View.extend({
- template: ' \
- | Source: | " + escapeText( source ) + " |
|---|
| Expected: | " + expected + " |
|---|---|
| Result: | " + actual + " |
| Diff: | " + QUnit.diff( expected, actual ) + " |
| Source: | " + escapeText( source ) + " |
| Result: | " + escapeText( actual ) + " |
|---|---|
| Source: | " + escapeText( source ) + " |