diff --git a/README.md b/README.md index 27bd9b63..dd13e2f7 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,22 @@ A simple but powerful library for building data applications in pure Javascript Running the tests by opening `test/index.html` in your browser. +### Contributing + +We welcome patches and pull requests and have a few guidelines. + +For small bugfixes or enhancements: + +* Please run the tests + +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 _layouts/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 + ## Changelog diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index ee0d4750..ad926765 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -1,6 +1,6 @@ - + @@ -21,7 +21,7 @@ - + diff --git a/demos/couchdb/app.js b/demos/couchdb/app.js new file mode 100755 index 00000000..c02fe9e7 --- /dev/null +++ b/demos/couchdb/app.js @@ -0,0 +1,73 @@ +jQuery(function($) { + window.dataExplorer = null; + window.explorerDiv = $('.data-explorer-here'); + + var queryParameters = recline.View.parseQueryString(decodeURIComponent(window.location.search)); + + var dataset = new recline.Model.Dataset({ + db_url: queryParameters['url'] || '/couchdb/yourcouchdb', + view_url: queryParameters['view_url'] || '/couchdb/yourcouchdb/_design/yourdesigndoc/_view/yourview', + backend: 'couchdb', + query_options: { + 'key': '_id' + } + }); + + dataset.fetch().done(function(dataset) { + console.log('records: ' + dataset.records); + }); + + createExplorer(dataset); +}); + +// make Explorer creation / initialization in a function so we can call it +// again and again +var createExplorer = function(dataset, state) { + // remove existing data explorer view + var reload = false; + if (window.dataExplorer) { + window.dataExplorer.remove(); + reload = true; + } + window.dataExplorer = null; + var $el = $('
'); + $el.appendTo(window.explorerDiv); + + var views = [ + { + id: 'grid', + label: 'Grid', + view: new recline.View.SlickGrid({ + model: dataset + }), + }, + { + id: 'graph', + label: 'Graph', + view: new recline.View.Graph({ + model: dataset + }), + }, + { + id: 'map', + label: 'Map', + view: new recline.View.Map({ + model: dataset + }), + }, + { + id: 'transform', + label: 'Transform', + view: new recline.View.Transform({ + model: dataset + }) + } + ]; + + window.dataExplorer = new recline.View.MultiView({ + model: dataset, + el: $el, + state: state, + views: views + }); +} diff --git a/demos/couchdb/index.html b/demos/couchdb/index.html new file mode 100755 index 00000000..953ef283 --- /dev/null +++ b/demos/couchdb/index.html @@ -0,0 +1,40 @@ +--- +layout: container +title: CouchDB Multiview - Demos +recline-deps: true +root: ../../ +--- + + + +
+

Instructions

+

To use this demo you will need a CouchDB instance running and accessible over HTTP. You should then pass the following 2 query parameters to this page:

+ +
url: url-to-your-couchdb-instance
+view_url: url-to-your-couchdb-view
+
+Example:
+http://path-to-this-page/?url=/mycouchdb/&view_url=/mycouchdb/_design/yourdesigndoc/_view/yourview
+
+ +

Note that if the CouchDB database is not running on the same domain as this page then the host it is on must support CORS – the simplest approach here is probably to set up a reverse proxy or proxy so your CouchDB database appears on the local domain at e.g. /mycouchdb/.

+
+ +

Demo

+ +
+ +
+ + + + diff --git a/demos/index.html b/demos/index.html index 35e95ada..aeca66c9 100644 --- a/demos/index.html +++ b/demos/index.html @@ -73,3 +73,18 @@ root: ../
+ +
+
+
+

CouchDB Demo

+

Using CouchDB with Recline Multiview to provide an elegant powerful browser for a CouchDB database and view.

+
+
+
+ +
+
+ +
+ diff --git a/dist/recline.js b/dist/recline.js index 11910563..f3d4605a 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -159,8 +159,14 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // // @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} [separator=','] Separator for CSV file + // @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 '"' + // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parseCSV= function(s, options) { @@ -169,8 +175,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var options = options || {}; var trm = (options.trim === false) ? false : true; - var separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. inQuote = false, @@ -205,7 +211,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; cur = s.charAt(i); // If we are at a EOF or EOR - if (inQuote === false && (cur === separator || cur === "\n")) { + if (inQuote === false && (cur === delimiter || cur === "\n")) { field = processField(field); // Add the current field to the current row row.push(field); @@ -218,8 +224,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; fieldQuoted = false; } else { - // If it's not a delimiter, add it to the field buffer - if (cur !== delimiter) { + // If it's not a quotechar, add it to the field buffer + if (cur !== quotechar) { field += cur; } else { if (!inQuote) { @@ -227,9 +233,9 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; inQuote = true; fieldQuoted = true; } else { - // Next char is delimiter, this is an escaped delimiter - if (s.charAt(i + 1) === delimiter) { - field += delimiter; + // Next char is quotechar, this is an escaped quotechar + if (s.charAt(i + 1) === quotechar) { + field += quotechar; // Skip the next char i += 1; } else { @@ -249,23 +255,48 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; return out; }; - // Converts an array of arrays into a Comma Separated Values string. - // Each array becomes a line in the CSV. + // ### 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 {Array} a The array of arrays to convert - // @param {Object} options Options for loading CSV including - // @param {String} [separator=','] Separator for CSV file - // Heavily based on uselesscode's JS CSV parser (MIT Licensed): + // @param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows: + // + // { + // fields: [ {id: .., ...}, {id: ..., + // records: [ { record }, { record }, ... ] + // ... // more attributes we do not care about + // } + // + // @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(a, options) { + 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 separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. field = '', // Buffer for building up the current field @@ -281,7 +312,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { // Convert string to delimited string - field = delimiter + field + delimiter; + field = quotechar + field + quotechar; } else if (typeof field === "number") { // Convert number to string field = field.toString(10); @@ -302,7 +333,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; row = ''; } else { // Add the current field to the current row - row += field + separator; + row += field + delimiter; } // Flush the field buffer field = ''; @@ -1848,26 +1879,27 @@ my.Graph = Backbone.View.extend({ return getFormattedX(x); }; + // infoboxes on mouse hover on points/bars etc var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y; - // it's horizontal so we have to flip - if (self.state.attributes.graphType === 'bars') { - var _tmp = x; - x = y; - y = _tmp; - } - - x = getFormattedX(x); + var x = obj.x; + var y = obj.y; + // it's horizontal so we have to flip + if (self.state.attributes.graphType === 'bars') { + var _tmp = x; + x = y; + y = _tmp; + } + + x = getFormattedX(x); - var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { - group: self.state.attributes.group, - x: x, - series: obj.series.label, - y: y - }); - - return content; + var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { + group: self.state.attributes.group, + x: x, + series: obj.series.label, + y: y + }); + + return content; }; var getFormattedX = function (x) { @@ -1938,18 +1970,18 @@ my.Graph = Backbone.View.extend({ xaxis: yaxis, yaxis: xaxis, mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' + track: true, + relative: true, + trackFormatter: trackFormatter, + fillColor: '#FFFFFF', + fillOpacity: 0.3, + position: 'e' }, bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 + show: true, + horizontal: true, + shadowSize: 0, + barWidth: 0.8 }, }, columns: { @@ -2470,6 +2502,11 @@ this.recline.View = this.recline.View || {}; // latField: {id of field containing latitude in the dataset} // } // +// +// 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: ' \
\ @@ -2488,6 +2525,8 @@ my.Map = Backbone.View.extend({ this.el = $(this.el); this.visible = true; this.mapReady = false; + // this will be the Leaflet L.Map object (setup below) + this.map = null; var stateData = _.extend({ geomField: null, @@ -2525,6 +2564,34 @@ my.Map = Backbone.View.extend({ this.elSidebar = this.menu.el; }, + // ## Customization Functions + // + // The following methods are designed for overriding in order to customize + // behaviour + + // ### infobox + // + // 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 (key in record.attributes){ + if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ + html += '
' + key + ': '+ record.attributes[key] + '
'; + } + } + return html; + }, + + // END: Customization section + // ---- + // ### Public: Adds the necessary elements to the page. // // Also sets up the editor fields and the map if necessary. @@ -2619,19 +2686,12 @@ my.Map = Backbone.View.extend({ // Empty field return true; } else if (feature instanceof Object){ - // Build popup contents - // TODO: mustache? - html = ''; - for (key in doc.attributes){ - if (!(self.state.get('geomField') && key == self.state.get('geomField'))){ - html += '
' + key + ': '+ doc.attributes[key] + '
'; - } - } - feature.properties = {popupContent: html}; - - // Add a reference to the model id, which will allow us to - // link this Leaflet layer to a Recline doc - feature.properties.cid = doc.cid; + 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); diff --git a/docs/src/backend.ckan.html b/docs/src/backend.ckan.html new file mode 100644 index 00000000..0727be5b --- /dev/null +++ b/docs/src/backend.ckan.html @@ -0,0 +1,87 @@ + backend.ckan.js

backend.ckan.js

this.recline = this.recline || {};
+this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
+
+(function($, my) {

CKAN Backend

+ +

This provides connection to the CKAN DataStore (v2)

+ +

General notes

+ +
    +
  • Every dataset must have an id equal to its resource id on the CKAN instance
  • +
  • You should set the CKAN API endpoint for requests by setting APIENDPOINT value on this module (recline.Backend.Ckan.APIENDPOINT)
  • +
  my.__type__ = 'ckan';

Default CKAN API endpoint used for requests (you can change this but it will affect every request!)

  my.API_ENDPOINT = 'http://datahub.io/api';

fetch

  my.fetch = function(dataset) {
+    var wrapper = my.DataStore();
+    var dfd = $.Deferred();
+    var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
+    jqxhr.done(function(results) {

map ckan types to our usual types ...

      var fields = _.map(results.result.fields, function(field) {
+        field.type = field.type in CKAN_TYPES_MAP ? CKAN_TYPES_MAP[field.type] : field.type;
+        return field;
+      });
+      var out = {
+        fields: fields,
+        useMemoryStore: false
+      };
+      dfd.resolve(out);  
+    });
+    return dfd.promise();
+  };

only put in the module namespace so we can access for tests!

  my._normalizeQuery = function(queryObj, dataset) {
+    var actualQuery = {
+      resource_id: dataset.id,
+      q: queryObj.q,
+      limit: queryObj.size || 10,
+      offset: queryObj.from || 0
+    };
+    if (queryObj.sort && queryObj.sort.length > 0) {
+      var _tmp = _.map(queryObj.sort, function(sortObj) {
+        return sortObj.field + ' ' + (sortObj.order || '');
+      });
+      actualQuery.sort = _tmp.join(',');
+    }
+    return actualQuery;
+  }
+
+  my.query = function(queryObj, dataset) {
+    var actualQuery = my._normalizeQuery(queryObj, dataset);
+    var wrapper = my.DataStore();
+    var dfd = $.Deferred();
+    var jqxhr = wrapper.search(actualQuery);
+    jqxhr.done(function(results) {
+      var out = {
+        total: results.result.total,
+        hits: results.result.records,
+      };
+      dfd.resolve(out);  
+    });
+    return dfd.promise();
+  };

DataStore

+ +

Simple wrapper around the CKAN DataStore API

+ +

@param endpoint: CKAN api endpoint (e.g. http://datahub.io/api)

  my.DataStore = function(endpoint) { 
+    var that = {
+      endpoint: endpoint || my.API_ENDPOINT
+    };
+    that.search = function(data) {
+      var searchUrl = that.endpoint + '/3/action/datastore_search';
+      var jqxhr = $.ajax({
+        url: searchUrl,
+        data: data,
+        dataType: 'json'
+      });
+      return jqxhr;
+    }
+
+    return that;
+  }
+
+  var CKAN_TYPES_MAP = {
+    'int4': 'integer',
+    'float8': 'float',
+    'text': 'string'
+  };
+
+}(jQuery, this.recline.Backend.Ckan));
+
+
\ No newline at end of file diff --git a/docs/src/backend.couchdb.html b/docs/src/backend.couchdb.html index c2602fdf..f126f2d1 100644 --- a/docs/src/backend.couchdb.html +++ b/docs/src/backend.couchdb.html @@ -1,17 +1,17 @@ - backend.couchdb.js

backend.couchdb.js

this.recline = this.recline || {};
+      backend.couchdb.js           

Backbone connector for a CouchDB backend.

-

Usage:

- -

var backend = new recline.Backend.CouchDB(); -var dataset = new recline.Model.Dataset({ - dburl: '/couchdb/mydb', - viewurl: '/couchdb/mydb/design/design1/views/view1', - queryoptions: { - 'key': 'somedocument_key' - } +

var dataset = new recline.Model.Dataset({
+  db_url: path-to-couchdb-database e.g. '/couchdb/mydb',        
+  view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1',
+  backend: 'couchdb',
+  query_options: {
+    'key': '_id'
+  }
 });
-backend.fetch(dataset.toJSON());
-backend.query(query, dataset.toJSON()).done(function () { ... });

-

Alternatively: -var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); +backend.query(query, dataset.toJSON()).done(function () { ... }); +

+ +

Alternatively:

+ +
var dataset = new recline.Model.Dataset({ ... }, 'couchdb');
 dataset.fetch();
-var results = dataset.query(query_obj);

+var results = dataset.query(query_obj); +
-

Additionally, the Dataset instance may define three methods: - function recordupdate (record, document) { ... } +

Additionally, the Dataset instance may define three methods:

+ +

function recordupdate (record, document) { ... } function recorddelete (record, document) { ... } - function recordcreate (record, document) { ... } -Where record is the JSON representation of the Record/Document instance + function record_create (record, document) { ... }

+ +

Where record is the JSON representation of the Record/Document instance and document is the JSON document stored in couchdb. -When _alldocs view is used (default), a record is the same as a document +When alldocs view is used (default), a record is the same as a document so these methods need not be defined. They are most useful when using a custom view that performs a map-reduce operation on each document to yield a record. Hence, when the record is @@ -422,8 +425,8 @@ we should remove it before sending it to the server.

dfd.reject(args); }); return dfd.promise(); + } }; - }(jQuery, this.recline.Backend.CouchDB));

backend.couchdb.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
 
-(function($, my) {
-  my.__type__ = 'couchdb';

CouchDB Wrapper

+(function($, my) { +my.__type__ = 'couchdb';

CouchDB Wrapper

Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints. @param {String} endpoint: url for CouchDB database, e.g. for Couchdb running on localhost:5984 with database // ckan-std it would be:

-
http://localhost:5984/ckan-std
+

TODO Add user/password arguments for couchdb authentication support.

-

TODO Add user/password arguments for couchdb authentication support.

  my.CouchDBWrapper = function(db_url, view_url, options) { 
+

See the example how to use this in: "demos/couchdb/"

  my.CouchDBWrapper = function(db_url, view_url, options) { 
     var self = this;
     self.endpoint = db_url;
     self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs';
@@ -124,31 +124,34 @@ See: http://wiki.apache.org/couchdb/HTTPviewAPI

\ No newline at end of file diff --git a/docs/src/backend.csv.html b/docs/src/backend.csv.html index c0092c84..33c222f2 100644 --- a/docs/src/backend.csv.html +++ b/docs/src/backend.csv.html @@ -1,4 +1,4 @@ - backend.csv.js

backend.csv.js

this.recline = this.recline || {};
+      backend.csv.js           

backend.csv.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.CSV = this.recline.Backend.CSV || {};
 
@@ -57,15 +57,21 @@ Each line in the CSV becomes an 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} [separator=','] Separator for CSV file -Heavily based on uselesscode's JS CSV parser (MIT Licensed): + @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 '"'

+ +

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 separator = options.separator || ',';
-    var delimiter = options.delimiter || '"';
+    var delimiter = options.delimiter || ',';
+    var quotechar = options.quotechar || '"';
 
     var cur = '', // The character we are currently processing.
       inQuote = false,
@@ -90,19 +96,19 @@ http://www.uselesscode.org/javascript/csv/

}; for (i = 0; i < s.length; i += 1) { - cur = s.charAt(i);

If we are at a EOF or EOR

      if (inQuote === false && (cur === separator || cur === "\n")) {
+      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 delimiter, add it to the field buffer

        if (cur !== delimiter) {
+      } 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 delimiter, this is an escaped delimiter

            if (s.charAt(i + 1) === delimiter) {
-              field += delimiter;

Skip the next char

              i += 1;
+          } 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;
             }
           }
@@ -113,22 +119,48 @@ http://www.uselesscode.org/javascript/csv/

out.push(row); return out; - };

Converts an array of arrays into a Comma Separated Values string. -Each array becomes a line in the CSV.

+ };

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 {Array} a The array of arrays to convert -@param {Object} options Options for loading CSV including -@param {String} [separator=','] Separator for CSV file -Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

  my.serializeCSV= function(a, options) {
+

@param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows:

+ +
{
+  fields: [ {id: .., ...}, {id: ..., 
+  records: [ { record }, { record }, ... ]
+  ... // more attributes we do not care about
+}
+
+ +

@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 separator = options.separator || ',';
-    var delimiter = options.delimiter || '"';
+    var delimiter = options.delimiter || ',';
+    var quotechar = options.quotechar || '"';
 
     var cur = '', // The character we are currently processing.
       field = '', // Buffer for building up the current field
@@ -140,7 +172,7 @@ http://www.uselesscode.org/javascript/csv/

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 = delimiter + field + delimiter;
+      } 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);
       }
 
@@ -155,7 +187,7 @@ http://www.uselesscode.org/javascript/csv/

row += field; out += row + "\n"; row = ''; - } else {

Add the current field to the current row

          row += field + separator;
+        } else {

Add the current field to the current row

          row += field + delimiter;
         }

Flush the field buffer

        field = '';
       }
     }
diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html
index f0232bf4..80c1bc89 100644
--- a/docs/src/backend.dataproxy.html
+++ b/docs/src/backend.dataproxy.html
@@ -1,4 +1,4 @@
-      backend.dataproxy.js           

backend.dataproxy.js

this.recline = this.recline || {};
+      backend.dataproxy.js           

backend.dataproxy.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
 
diff --git a/docs/src/backend.elasticsearch.html b/docs/src/backend.elasticsearch.html
index ea0e1344..e8f2cedf 100644
--- a/docs/src/backend.elasticsearch.html
+++ b/docs/src/backend.elasticsearch.html
@@ -1,4 +1,4 @@
-      backend.elasticsearch.js           

backend.elasticsearch.js

this.recline = this.recline || {};
+      backend.elasticsearch.js           

backend.elasticsearch.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
 
@@ -104,6 +104,16 @@ on http://localhost:9200 with index twitter and type tweet it would be:

}); } 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) {
@@ -117,14 +127,16 @@ on http://localhost:9200 with index twitter and type tweet it would be:

out.geo_distance.unit = filter.unit; } return out; - },

query

+ },

query

@return deferred supporting promise API

    this.query = function(queryObj) {
       var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
-      var queryNormalized = this._normalizeQuery(queryObj);
+      esQuery.query = this._normalizeQuery(queryObj);
       delete esQuery.q;
       delete esQuery.filters;
-      esQuery.query = queryNormalized;
+      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({
@@ -134,10 +146,10 @@ on http://localhost:9200 with index twitter and type tweet it would be:

}); return jqxhr; } - };

Recline Connectors

+ };

Recline Connectors

Requires URL of ElasticSearch endpoint to be specified on the dataset -via the url attribute.

ES options which are passed through to options on Wrapper (see Wrapper for details)

  my.esOptions = {};

fetch

  my.fetch = function(dataset) {
+via the url attribute.

ES options which are passed through to options on Wrapper (see Wrapper for details)

  my.esOptions = {};

fetch

  my.fetch = function(dataset) {
     var es = new my.Wrapper(dataset.url, my.esOptions);
     var dfd = $.Deferred();
     es.mapping().done(function(schema) {
@@ -145,7 +157,7 @@ via the url attribute.

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];
+      }

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;
@@ -158,7 +170,7 @@ via the url attribute.

dfd.reject(arguments); }); return dfd.promise(); - };

save

  my.save = function(changes, dataset) {
+  };

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 = $.Deferred();
@@ -175,7 +187,7 @@ via the url attribute.

} else if (changes.deletes.length > 0) { return es.delete(changes.deletes[0].id); } - };

query

  my.query = function(queryObj, dataset) {
+  };

query

  my.query = function(queryObj, dataset) {
     var dfd = $.Deferred();
     var es = new my.Wrapper(dataset.url, my.esOptions);
     var jqxhr = es.query(queryObj);
@@ -201,7 +213,7 @@ via the url attribute.

dfd.reject(out); }); return dfd.promise(); - };

makeRequest

+ };

makeRequest

Just $.ajax but in any headers in the 'headers' attribute of this Backend instance. Example:

diff --git a/docs/src/backend.gdocs.html b/docs/src/backend.gdocs.html index 9425ca1d..e05f0b2f 100644 --- a/docs/src/backend.gdocs.html +++ b/docs/src/backend.gdocs.html @@ -1,4 +1,4 @@ - backend.gdocs.js

backend.gdocs.js

this.recline = this.recline || {};
+      backend.gdocs.js           

backend.gdocs.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
 
diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html
index 2ea8beb5..248eca99 100644
--- a/docs/src/backend.memory.html
+++ b/docs/src/backend.memory.html
@@ -1,4 +1,4 @@
-      backend.memory.js           

backend.memory.js

this.recline = this.recline || {};
+      backend.memory.js           

backend.memory.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.Memory = this.recline.Backend.Memory || {};
 
@@ -60,13 +60,14 @@ from the data.

var results = this.data; results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj);

not complete sorting!

      _.each(queryObj.sort, function(sortObj) {
-        var fieldName = _.keys(sortObj)[0];
+      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[fieldName].order == 'desc') {
+        if (sortObj.order == 'desc') {
           results.reverse();
         }
       });
diff --git a/docs/src/data.transform.html b/docs/src/data.transform.html
index ef709101..62a7fa8e 100644
--- a/docs/src/data.transform.html
+++ b/docs/src/data.transform.html
@@ -1,4 +1,4 @@
-      data.transform.js           

data.transform.js

this.recline = this.recline || {};
+      data.transform.js           

data.transform.js

this.recline = this.recline || {};
 this.recline.Data = this.recline.Data || {};
 
 (function(my) {

adapted from https://github.com/harthur/costco. heather rules

my.Transform = {};
diff --git a/docs/src/model.html b/docs/src/model.html
index 26572c1a..47cd7aab 100644
--- a/docs/src/model.html
+++ b/docs/src/model.html
@@ -1,4 +1,4 @@
-      model.js           

model.js

Recline Backbone Models

this.recline = this.recline || {};
+      model.js           

model.js

Recline Backbone Models

this.recline = this.recline || {};
 this.recline.Model = this.recline.Model || {};
 
 (function($, my) {

Dataset

my.Dataset = Backbone.Model.extend({
@@ -233,37 +233,12 @@ also returned.

} return backend; } -});

Dataset.restore

- -

Restore a Dataset instance from a serialized state. Serialized state for a -Dataset is an Object like:

- -

-{
-  backend: {backend type - i.e. value of dataset.backend.type}
-  dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
-  // convenience - if url provided and dataste not this be used as dataset url
-  url: {dataset url}
-  ...
-}

my.Dataset.restore = function(state) {
-  var dataset = null;

hack-y - restoring a memory dataset does not mean much ...

  if (state.backend === 'memory') {
-    var datasetInfo = {
-      records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
-    };
-  } else {
-    var datasetInfo = {
-      url: state.url,
-      backend: state.backend
-    };
-  }
-  dataset = new recline.Model.Dataset(datasetInfo);
-  return dataset;
-};

A Record

+});

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

+ },

initialize

Create a Record

@@ -272,7 +247,7 @@ 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

+ },

getFieldValue

For the provided Field get the corresponding rendered computed data value for this record.

  getFieldValue: function(field) {
@@ -281,7 +256,7 @@ for this record.

val = field.renderer(val, field, this.toJSON()); } return val; - },

getFieldValueUnrendered

+ },

getFieldValueUnrendered

For the provided Field get the corresponding computed data value for this record.

  getFieldValueUnrendered: function(field) {
@@ -290,7 +265,7 @@ for this record.

val = field.deriver(val, field, this); } return val; - },

summary

+ },

summary

Get a simple html summary of this record in form of key/value list

  summary: function(record) {
     var self = this;
@@ -302,30 +277,30 @@ for this record.

}); html += '</div>'; return html; - },

Override Backbone save, fetch and destroy so they do nothing + },

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); }
-});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
+});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
   constructor: function RecordList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Record
-});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({
+});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({
   constructor: function Field() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
-  },

defaults - define default values

  defaults: {
+  },

defaults - define default values

  defaults: {
     label: null,
     type: 'string',
     format: null,
     is_derived: false
-  },

initialize

+ },

initialize

@param {Object} data: standard Backbone model attributes

-

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
+

@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) {
@@ -366,7 +341,7 @@ WARNING: these will not persist unless you call save on Dataset

} } else if (format == 'plain') { return val; - } else {

as this is the default and default type is string may get things + } 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>');
         }
@@ -381,7 +356,7 @@ here that are not actually strings

Backbone.Collection.prototype.constructor.apply(this, arguments); }, model: my.Field -});

Query

my.Query = Backbone.Model.extend({
+});

Query

my.Query = Backbone.Model.extend({
   constructor: function Query() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -396,7 +371,7 @@ here that are not actually strings

}, _filterTemplates: { term: { - type: 'term',

TODO do we need this attribute here?

      field: '',
+      type: 'term',

TODO do we need this attribute here?

      field: '',
       term: ''
     },
     range: {
@@ -413,11 +388,11 @@ here that are not actually strings

lat: 0 } } - },

addFilter

+ },

addFilter

Add a new filter (appended 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 full specified so use template and over-write +

@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 full specified so use template and over-write 3 as for 'type', 'field' and 'fieldType'

    if (_.keys(filter).length <= 3) {
       ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
     }
@@ -426,19 +401,19 @@ here that are not actually strings

this.trigger('change:filters:new-blank'); }, updateFilter: function(index, value) { - },

removeFilter

+ },

removeFilter

Remove a filter from filters at index filterIndex

  removeFilter: function(filterIndex) {
     var filters = this.get('filters');
     filters.splice(filterIndex, 1);
     this.set({filters: filters});
     this.trigger('change');
-  },

addFacet

+ },

addFacet

Add a Facet to this query

See http://www.elasticsearch.org/guide/reference/api/search/facets/

  addFacet: function(fieldId) {
-    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
+    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] = {
@@ -458,7 +433,7 @@ here that are not actually strings

this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); } -});

A Facet (Result)

my.Facet = Backbone.Model.extend({
+});

A Facet (Result)

my.Facet = Backbone.Model.extend({
   constructor: function Facet() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -471,15 +446,15 @@ here that are not actually strings

terms: [] }; } -});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
+});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
   constructor: function FacetList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Facet
-});

Object State

+});

Object State

Convenience Backbone model for storing (configuration) state of objects like Views.

my.ObjectState = Backbone.Model.extend({
-});

Backbone.sync

+});

Backbone.sync

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);
diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html
index 62c65a23..8fd0d659 100644
--- a/docs/src/view.graph.html
+++ b/docs/src/view.graph.html
@@ -1,4 +1,4 @@
-      view.graph.js           

view.graph.js

/*jshint multistr:true */
+      view.graph.js           

view.graph.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
@@ -102,29 +102,28 @@ generate the element itself (you can then append view.el to the DOM.

var tickFormatter = function (x) { return getFormattedX(x); }; - - var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y;

it's horizontal so we have to flip

          if (self.state.attributes.graphType === 'bars') {
-            var _tmp = x;
-            x = y;
-            y = _tmp;
-          }
-          
-          x = getFormattedX(x);
+    

infoboxes on mouse hover on points/bars etc

    var trackFormatter = function (obj) {
+      var x = obj.x;
+      var y = obj.y;

it's horizontal so we have to flip

      if (self.state.attributes.graphType === 'bars') {
+        var _tmp = x;
+        x = y;
+        y = _tmp;
+      }
+      
+      x = getFormattedX(x);
 
-          var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
-            group: self.state.attributes.group,
-            x: x,
-            series: obj.series.label,
-            y: y
-          });
-        
-        return content;
+      var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
+        group: self.state.attributes.group,
+        x: x,
+        series: obj.series.label,
+        y: y
+      });
+      
+      return content;
     };
     
     var getFormattedX = function (x) {
-      var xfield = self.model.fields.get(self.state.attributes.group);

time series

      var isDateTime = xfield.get('type') === 'date';
+      var xfield = self.model.fields.get(self.state.attributes.group);

time series

      var isDateTime = xfield.get('type') === 'date';
 
       if (self.model.records.models[parseInt(x)]) {
         x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
@@ -151,7 +150,7 @@ generate the element itself (you can then append view.el to the DOM.

var legend = {}; legend.position = 'ne'; -

mouse.lineColor is set in createSeries

    var optionsPerGraphType = { 
+    

mouse.lineColor is set in createSeries

    var optionsPerGraphType = { 
       lines: {
         legend: legend,
         colors: this.graphColors,
@@ -186,18 +185,18 @@ generate the element itself (you can then append view.el to the DOM.

xaxis: yaxis, yaxis: xaxis, mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' + track: true, + relative: true, + trackFormatter: trackFormatter, + fillColor: '#FFFFFF', + fillOpacity: 0.3, + position: 'e' }, bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 + show: true, + horizontal: true, + shadowSize: 0, + barWidth: 0.8 }, }, columns: { @@ -233,12 +232,12 @@ generate the element itself (you can then append view.el to the DOM.

var points = []; _.each(self.model.records.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); - var x = doc.getFieldValue(xfield);

time series

        var isDateTime = xfield.get('type') === 'date';
+        var x = doc.getFieldValue(xfield);

time series

        var isDateTime = xfield.get('type') === 'date';
         
-        if (isDateTime) {

datetime

          if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') {

not bar or column

            x = new Date(x).getTime();
-          } else {

bar or column

            x = index;
+        if (isDateTime) {

datetime

          if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') {

not bar or column

            x = new Date(x).getTime();
+          } else {

bar or column

            x = index;
           }
-        } else if (typeof x === 'string') {

string

          x = parseFloat(x);
+        } else if (typeof x === 'string') {

string

          x = parseFloat(x);
           if (isNaN(x)) {
             x = index;
           }
@@ -246,7 +245,7 @@ generate the element itself (you can then append view.el to the DOM.

var yfield = self.model.fields.get(field); var y = doc.getFieldValue(yfield); -

horizontal bar chart

        if (self.state.attributes.graphType == 'bars') {
+        

horizontal bar chart

        if (self.state.attributes.graphType == 'bars') {
           points.push([y, x]);
         } else {
           points.push([x, y]);
@@ -330,12 +329,12 @@ generate the element itself (you can then append view.el to the DOM.

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.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 = [""];
+    }

ensure at least one series box shows up

    var tmpSeries = [""];
     if (this.state.get('series').length > 0) {
       tmpSeries = this.state.get('series');
     }
@@ -344,7 +343,7 @@ generate the element itself (you can then append view.el to the DOM.

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){
+  },

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){
@@ -369,7 +368,7 @@ generate the element itself (you can then append view.el to the DOM.

graphType: this.el.find('.editor-type select').val() }; this.state.set(updatedState); - },

Public: Adds a new empty series select box to the editor.

+ },

Public: Adds a new empty series select box to the editor.

@param [int] idx index of this series in the list of series

@@ -387,7 +386,7 @@ generate the element itself (you can then append view.el to the DOM.

_onAddSeries: function(e) { e.preventDefault(); this.addSeries(this.state.get('series').length); - },

Public: Removes a series list item from the editor.

+ },

Public: Removes a series list item from the editor.

Also updates the labels of the remaining series elements.

  removeSeries: function (e) {
     e.preventDefault();
diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html
index 7e613802..02c7230a 100644
--- a/docs/src/view.grid.html
+++ b/docs/src/view.grid.html
@@ -1,4 +1,4 @@
-      view.grid.js           

view.grid.js

/*jshint multistr:true */
+      view.grid.js           

view.grid.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
diff --git a/docs/src/view.map.html b/docs/src/view.map.html
index b30e2692..9dc80dde 100644
--- a/docs/src/view.map.html
+++ b/docs/src/view.map.html
@@ -1,4 +1,4 @@
-      view.map.js           

view.map.js

/*jshint multistr:true */
+      view.map.js           
this.map.addLayer(bg);this.features=newL.GeoJSON(); - this.features.on('featureparse',function(e){ - if(e.properties&&e.properties.popupContent){ - e.layer.bindPopup(e.properties.popupContent); - } - if(e.properties&&e.properties.cid){ - e.layer.cid=e.properties.cid; - } - - }); <input type="hidden" class="editor-id" value="map-1" /> \ </div> \ </form> \ -',this.state=newrecline.Model.ObjectState(options.state);this.state.bind('change',this.render);this.render(); - },_geomReady:function(){returnBoolean(this.state.get('geomField')||(this.state.get('latField')&&this.state.get('lonField'))); - },

view.map.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
@@ -20,7 +20,14 @@ have the following (optional) configuration options:

lonField: {id of field containing longitude in the dataset} latField: {id of field containing latitude in the dataset} } -
my.Map = Backbone.View.extend({
+
+ +

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: ' \
     <div class="recline-map"> \
       <div class="panel map"></div> \
@@ -34,7 +41,7 @@ If not found, the user will need to define the fields via the editor.

var self = this; this.el = $(this.el); this.visible = true; - this.mapReady = false; + this.mapReady = false;

this will be the Leaflet L.Map object (setup below)

    this.map = null;
 
     var stateData = _.extend({
         geomField: null,
@@ -44,16 +51,16 @@ If not found, the user will need to define the fields via the editor.

}, options.state ); - this.state = new recline.Model.ObjectState(stateData);

Listen to changes in the fields

    this.model.fields.bind('change', function() {
-      self._setupGeometryField()
-      self.render()
-    });

Listen to changes in the records

    this.model.records.bind('add', function(doc){self.redraw('add',doc)});
+    this.state = new recline.Model.ObjectState(stateData);

Listen to changes in the fields

    this.model.fields.bind('change', function() {
+      self._setupGeometryField();
+      self.render();
+    });

Listen to changes in the records

    this.model.records.bind('add', function(doc){self.redraw('add',doc);});
     this.model.records.bind('change', function(doc){
         self.redraw('remove',doc);
         self.redraw('add',doc);
     });
-    this.model.records.bind('remove', function(doc){self.redraw('remove',doc)});
-    this.model.records.bind('reset', function(){self.redraw('reset')});
+    this.model.records.bind('remove', function(doc){self.redraw('remove',doc);});
+    this.model.records.bind('reset', function(){self.redraw('reset');});
 
     this.menu = new my.MapMenu({
       model: this.model,
@@ -64,7 +71,28 @@ If not found, the user will need to define the fields via the editor.

self.redraw(); }); this.elSidebar = this.menu.el; - },

Public: Adds the necessary elements to the page.

+ },

Customization Functions

+ +

The following methods are designed for overriding in order to customize +behaviour

infobox

+ +

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 (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;
+  },

END: Customization section

Public: Adds the necessary elements to the page.

Also sets up the editor fields and the map if necessary.

  render: function() {
     var self = this;
@@ -74,7 +102,7 @@ If not found, the user will need to define the fields via the editor.

this.$map = this.el.find('.panel.map'); this.redraw(); return this; - },

Public: Redraws the features on the map according to the action provided

+ },

Public: Redraws the features on the map according to the action provided

Actions can be:

@@ -85,7 +113,7 @@ If not found, the user will need to define the fields via the editor.

  • refresh: Clear existing features and add all current records
  •   redraw: function(action, doc){
         var self = this;
    -    action = action || 'refresh';

    try to set things up if not already

        if (!self._geomReady()){
    +    action = action || 'refresh';

    try to set things up if not already

        if (!self._geomReady()){
           self._setupGeometryField();
         }
         if (!self.mapReady){
    @@ -111,7 +139,7 @@ If not found, the user will need to define the fields via the editor.

    } }, - show: function() {

    If the div was hidden, Leaflet needs to recalculate some sizes + 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')) {
    @@ -128,7 +156,7 @@ to display properly

    _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

    + },

    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 @@ -141,22 +169,22 @@ stopped and an error notification shown.

    var count = 0; var wrongSoFar = 0; - _.every(docs,function(doc){ + _.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){

    Build popup contents -TODO: mustache?

            html = ''
    -        for (key in doc.attributes){
    -          if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
    -            html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
    -          }
    -        }
    -        feature.properties = {popupContent: html};

    Add a reference to the model id, which will allow us to -link this Leaflet layer to a Recline doc

            feature.properties.cid = doc.cid;
    +      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.addGeoJSON(feature);
    +          self.features.addData(feature);
    +
    +          if (feature.properties && feature.properties.popupContent) {
    +            self.features.bindPopup(feature.properties.popupContent);
    +          }
             } catch (except) {
               wrongSoFar += 1;
               var msg = 'Wrong geometry value';
    @@ -166,14 +194,14 @@ link this Leaflet layer to a Recline doc

    } } } else { - wrongSoFar += 1 + wrongSoFar += 1; if (wrongSoFar <= 10) { self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } } return true; }); - },

    Private: Remove one or n features to the map

      _remove: function(docs){
    +  },

    Private: Remove one or n features to the map

      _remove: function(docs){
     
         var self = this;
     
    @@ -181,16 +209,16 @@ link this Leaflet layer to a Recline doc

    _.each(docs,function(doc){ for (key in self.features._layers){ - if (self.features._layers[key].cid == doc.cid){ + if (self.features._layers[key].feature.properties.cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); } } }); - },

    Private: Return a GeoJSON geomtry extracted from the record fields

      _getGeometryFromRecord: function(doc){
    +  },

    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 {
    +      if (typeof(value) === 'string'){

    We may have a GeoJSON string representation

            try {
               value = $.parseJSON(value);
             } catch(e) {}
           }
    @@ -208,16 +236,16 @@ link this Leaflet layer to a Recline doc

    } else { return null; } - } else if (value && value.slice) {

    [ lon, lat ]

            return {
    +      } else if (value && value.slice) {

    [ lon, lat ]

            return {
               "type": "Point",
               "coordinates": [value[0], value[1]]
             };
    -      } else if (value && value.lat) {

    of form { lat: ..., lon: ...}

            return {
    +      } 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'));
    +      }

    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'));
           if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
             return {
    @@ -227,10 +255,10 @@ link this Leaflet layer to a Recline doc

    } } return null; - },

    Private: Check if there is a field with GeoJSON geometries or alternatively, + },

    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()) {
    +

    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),
    @@ -238,7 +266,7 @@ two fields with lat/lon values.

    }); this.menu.state.set(this.state.toJSON()); } - },

    Private: Check if a field in the current model exists in the provided + },

    Private: Check if a field in the current model exists in the provided list of names.

      _checkField: function(fieldNames){
         var field;
         var modelFieldNames = this.model.fields.pluck('id');
    @@ -249,15 +277,15 @@ list of names.

    } } return null; - },

    Private: Zoom to map to current features extent if any, or to the full + },

    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){
    +    if (bounds.getNorthEast()){
           this.map.fitBounds(bounds);
         } else {
    -      this.map.setView(new L.LatLng(0, 0), 2);
    +      this.map.setView([0, 0], 2);
         }
    -  },

    Private: Sets up the Leaflet map control and the features layer.

    + },

    Private: Sets up the Leaflet map control and the features layer.

    The map uses a base layer from MapQuest based on OpenStreetMap.

      _setupMap: function(){
    @@ -269,36 +297,12 @@ on OpenStreetMap.

    This will be available in the next Leaflet stable release. -In the meantime we add it manually to our layer.

        this.features.getBounds = function(){
    -      var bounds = new L.LatLngBounds();
    -      this._iterateLayers(function (layer) {
    -        if (layer instanceof L.Marker){
    -          bounds.extend(layer.getLatLng());
    -        } else {
    -          if (layer.getBounds){
    -            bounds.extend(layer.getBounds().getNorthEast());
    -            bounds.extend(layer.getBounds().getSouthWest());
    -          }
    -        }
    -      }, this);
    -      return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
    -    }
    -
         this.map.addLayer(this.features);
     
    -    this.map.setView(new L.LatLng(0, 0), 2);
    +    this.map.setView([0, 0], 2);
     
         this.mapReady = true;
    -  },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    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){
    @@ -368,7 +372,7 @@ In the meantime we add it manually to our layer.

    Define here events for UI elements

      events: {
    +',

    Define here events for UI elements

      events: {
         'click .editor-update-map': 'onEditorSubmit',
         'change .editor-field-type': 'onFieldTypeChange',
         'click #editor-auto-zoom': 'onAutoZoomChange'
    @@ -382,7 +386,7 @@ In the meantime we add it manually to our layer.

    Public: Adds the necessary elements to the page.

    + },

    Public: Adds the necessary elements to the page.

    Also sets up the editor fields and the map if necessary.

      render: function() {
         var self = this;
    @@ -410,7 +414,7 @@ In the meantime we add it manually to our layer.

    UI Event handlers

    Public: Update map with user options

    + },

    UI Event handlers

    Public: Update map with user options

    Right now the only configurable option is what field(s) contains the location information.

      onEditorSubmit: function(e){
    @@ -429,7 +433,7 @@ location information.

    }); } return false; - },

    Public: Shows the relevant select lists depending on the location field + },

    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();
    @@ -442,7 +446,7 @@ type selected.

    onAutoZoomChange: function(e){ this.state.set({autoZoom: !this.state.get('autoZoom')}); - },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    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){
    diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
    index 902aff81..20ce53b2 100644
    --- a/docs/src/view.multiview.html
    +++ b/docs/src/view.multiview.html
    @@ -1,4 +1,4 @@
    -      view.multiview.js           

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

    this.recline = this.recline || {};
    +      view.multiview.js           

    view.multiview.js

    /*jshint multistr:true */

    Standard JS module setup

    this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
     
     (function($, my) {

    MultiView

    @@ -106,8 +106,9 @@ initialized the MultiView with the relevant views themselves.

    </div> \ <div class="menu-right"> \ <div class="btn-group" data-toggle="buttons-checkbox"> \ - <a href="#" class="btn active" data-action="filters">Filters</a> \ - <a href="#" class="btn active" data-action="fields">Fields</a> \ + {{#sidebarViews}} \ + <a href="#" data-action="{{id}}" class="btn active">{{label}}</a> \ + {{/sidebarViews}} \ </div> \ </div> \ <div class="query-editor-here" style="display:inline;"></div> \ @@ -134,28 +135,28 @@ initialized the MultiView with the relevant views themselves.

    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') - }), + }) }, { id: 'transform', label: 'Transform', @@ -229,6 +230,7 @@ TODO: set query state ...?

    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) {
    @@ -242,7 +244,7 @@ TODO: set query state ...?

    _.each(this.sidebarViews, function(view) { this['$'+view.id] = view.view.el; $dataSidebar.append(view.view.el); - }); + }, this); var pager = new recline.View.Pager({ model: this.model.queryState @@ -283,13 +285,7 @@ TODO: set query state ...?

    _onMenuClick: function(e) { e.preventDefault(); var action = $(e.target).attr('data-action'); - if (action === 'filters') { - this.$filterEditor.toggle(); - } else if (action === 'fields') { - this.$fieldsView.toggle(); - } else if (action === 'transform') { - this.transformView.el.toggle(); - } + this['$'+action].toggle(); }, _onSwitchView: function(e) { @@ -310,6 +306,7 @@ TODO: set query state ...?

    'view-graph': graphState, backend: this.model.backend.__type__, url: this.model.get('url'), + dataset: this.model.toJSON(), currentView: null, readOnly: false }, @@ -339,7 +336,7 @@ TODO: set query state ...?

    var self = this; _.each(this.pageViews, function(pageView) { pageView.view.bind('recline:flash', function(flash) { - self.notify(flash); + self.notify(flash); }); }); },

    notify

    @@ -360,14 +357,15 @@ flash object. Flash attributes (all are optional):

    }, flash ); + var _template; if (tmplData.loader) { - var _template = ' \ + _template = ' \ <div class="alert alert-info alert-loader"> \ {{message}} \ <span class="notification-loader">&nbsp;</span> \ </div>'; } else { - var _template = ' \ + _template = ' \ <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \ {{message}} \ </div>'; @@ -391,14 +389,28 @@ flash object. Flash attributes (all are optional):

    } });

    MultiView.restore

    -

    Restore a MultiView instance from a serialized state including the associated dataset

    my.MultiView.restore = function(state) {
    -  var dataset = recline.Model.Dataset.restore(state);
    +

    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!)

      if (state.backend === 'memory') {
    +    var datasetInfo = {
    +      backend: 'memory',
    +      records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
    +    };
    +  } else {
    +    var 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;
    -}

    Miscellaneous Utilities

    var urlPathRegex = /^([^?]+)(\?.*)?/;

    Parse the Hash section of a URL into path and query string

    my.parseHashUrl = function(hashUrl) {
    +}

    Miscellaneous Utilities

    var urlPathRegex = /^([^?]+)(\?.*)?/;

    Parse the Hash section of a URL into path and query string

    my.parseHashUrl = function(hashUrl) {
       var parsed = urlPathRegex.exec(hashUrl);
       if (parsed === null) {
         return {};
    @@ -408,7 +420,7 @@ flash object. Flash attributes (all are optional):

    query: parsed[2] || '' }; } -};

    Parse a URL query string (?xyz=abc...) into a dictionary.

    my.parseQueryString = function(q) {
    +};

    Parse a URL query string (?xyz=abc...) into a dictionary.

    my.parseQueryString = function(q) {
       if (!q) {
         return {};
       }
    @@ -421,13 +433,13 @@ flash object. Flash attributes (all are optional):

    if (q && q.length && q[0] === '?') { q = q.slice(1); } - while (e = r.exec(q)) {

    TODO: have values be array as query string allow repetition of keys

        urlParams[d(e[1])] = d(e[2]);
    +  while (e = r.exec(q)) {

    TODO: have values be array as query string allow repetition of keys

        urlParams[d(e[1])] = d(e[2]);
       }
       return urlParams;
    -};

    Parse the query string out of the URL hash

    my.parseHashQueryString = function() {
    +};

    Parse the query string out of the URL hash

    my.parseHashQueryString = function() {
       q = my.parseHashUrl(window.location.hash).query;
       return my.parseQueryString(q);
    -};

    Compse a Query String

    my.composeQueryString = function(queryParams) {
    +};

    Compse a Query String

    my.composeQueryString = function(queryParams) {
       var queryString = '?';
       var items = [];
       $.each(queryParams, function(key, value) {
    @@ -442,7 +454,7 @@ flash object. Flash attributes (all are optional):

    my.getNewHashForQueryString = function(queryParams) { var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) {

    slice(1) to remove # at start

        return window.location.hash.split('?')[0].slice(1) + queryPart;
    +  if (window.location.hash) {

    slice(1) to remove # at start

        return window.location.hash.split('?')[0].slice(1) + queryPart;
       } else {
         return queryPart;
       }
    diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html
    index 72be29fd..41b5cafd 100644
    --- a/docs/src/view.slickgrid.html
    +++ b/docs/src/view.slickgrid.html
    @@ -1,4 +1,4 @@
    -      view.slickgrid.js           

    view.slickgrid.js

    /*jshint multistr:true */
    +      view.slickgrid.js           

    view.slickgrid.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    @@ -100,15 +100,17 @@ column picker

    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

    Column sorting

        var sortInfo = this.model.queryState.get('sort');
         if (sortInfo){
    -      var column = _.keys(sortInfo[0])[0];
    -      var sortAsc = !(sortInfo[0][column].order == 'desc');
    +      var column = sortInfo[0].field;
    +      var sortAsc = !(sortInfo[0].order == 'desc');
           this.grid.setSortColumn(column, sortAsc);
         }
     
         this.grid.onSort.subscribe(function(e, args){
           var order = (args.sortAsc) ? 'asc':'desc';
    -      var sort = [{}];
    -      sort[0][args.sortCol.field] = {order: order};
    +      var sort = [{
    +        field: args.sortCol.field,
    +        order: order
    +      }];
           self.model.query({sort: sort});
         });
     
    diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html
    index 295900ce..04956d6d 100644
    --- a/docs/src/view.timeline.html
    +++ b/docs/src/view.timeline.html
    @@ -1,4 +1,4 @@
    -      view.timeline.js           

    view.timeline.js

    /*jshint multistr:true */
    +      view.timeline.js           

    view.timeline.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/view.transform.html b/docs/src/view.transform.html
    index fdca17c9..e311eced 100644
    --- a/docs/src/view.transform.html
    +++ b/docs/src/view.transform.html
    @@ -1,4 +1,4 @@
    -      view.transform.js           

    view.transform.js

    /*jshint multistr:true */
    +      view.transform.js           

    view.transform.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};

    Views module following classic module pattern

    (function($, my) {

    ColumnTransform

    diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html index 62af60b6..e3144459 100644 --- a/docs/src/widget.facetviewer.html +++ b/docs/src/widget.facetviewer.html @@ -1,4 +1,4 @@ - widget.facetviewer.js

    widget.facetviewer.js

    /*jshint multistr:true */
    +      widget.facetviewer.js           

    widget.facetviewer.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html
    index 2821e218..ced40695 100644
    --- a/docs/src/widget.fields.html
    +++ b/docs/src/widget.fields.html
    @@ -1,4 +1,4 @@
    -      widget.fields.js           

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    + widget.fields.js

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    For each field

    diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html index 30085297..2b02a8c7 100644 --- a/docs/src/widget.filtereditor.html +++ b/docs/src/widget.filtereditor.html @@ -1,4 +1,4 @@ - widget.filtereditor.js

    widget.filtereditor.js

    /*jshint multistr:true */
    +      widget.filtereditor.js           

    widget.filtereditor.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
    index 9ecfadc9..101da7a1 100644
    --- a/docs/src/widget.pager.html
    +++ b/docs/src/widget.pager.html
    @@ -1,4 +1,4 @@
    -      widget.pager.js           

    widget.pager.js

    /*jshint multistr:true */
    +      widget.pager.js           

    widget.pager.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html
    index 846d8ee5..3ae1ed06 100644
    --- a/docs/src/widget.queryeditor.html
    +++ b/docs/src/widget.queryeditor.html
    @@ -1,4 +1,4 @@
    -      widget.queryeditor.js           

    widget.queryeditor.js

    /*jshint multistr:true */
    +      widget.queryeditor.js           
    '); - fragments.push(''); - rowStarted = true; - } - - if (lf) - label = lf(label, s); - - fragments.push( - '' + - ''); - } - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '

    widget.queryeditor.js

    /*jshint multistr:true */
     
     this.recline = this.recline || {};
     this.recline.View = this.recline.View || {};
    diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown
    index 2dc86f56..32865631 100644
    --- a/docs/tutorial-views.markdown
    +++ b/docs/tutorial-views.markdown
    @@ -189,14 +189,14 @@ library and the Recline Map view:
     
     {% highlight html %}
     
    -
    +
     
     
     
     
    -
    +
     
     {% endhighlight %}
     
    diff --git a/download.markdown b/download.markdown
    index 468a6302..efcabc9e 100644
    --- a/download.markdown
    +++ b/download.markdown
    @@ -78,7 +78,7 @@ Optional dependencies:
     
     * [Mustache.js](https://github.com/janl/mustache.js/) >= 0.5.0-dev (required for all views)
     * [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view)
    -* [Leaflet](http://leaflet.cloudmade.com/) >= 0.3.1 (required for map view)
    +* [Leaflet](http://leaflet.cloudmade.com/) >= 0.4.4 (required for map view)
     * [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02 (required for the timeline view)
     * [Bootstrap](http://twitter.github.com/bootstrap/) >= v2.0 (default option for CSS and UI JS but you can use your own)
     
    diff --git a/src/backend.couchdb.js b/src/backend.couchdb.js
    old mode 100644
    new mode 100755
    index 3fa930f2..54f3f88a
    --- a/src/backend.couchdb.js
    +++ b/src/backend.couchdb.js
    @@ -2,8 +2,8 @@ this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
     this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
     
    -(function($, my) {
    -  my.__type__ = 'couchdb';
    +(function($, my) {  
    +my.__type__ = 'couchdb';
     
       // ## CouchDB Wrapper
       //
    @@ -11,9 +11,10 @@ this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
       // @param {String} endpoint: url for CouchDB database, e.g. for Couchdb running
       // on localhost:5984 with database // ckan-std it would be:
       // 
    -  // 
    http://localhost:5984/ckan-std
    // // TODO Add user/password arguments for couchdb authentication support. + // + // See the example how to use this in: "demos/couchdb/" my.CouchDBWrapper = function(db_url, view_url, options) { var self = this; self.endpoint = db_url; @@ -149,29 +150,30 @@ this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {}; // ## CouchDB Backend // // Backbone connector for a CouchDB backend. + // + // var dataset = new recline.Model.Dataset({ + // db_url: path-to-couchdb-database e.g. '/couchdb/mydb', + // view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1', + // backend: 'couchdb', + // query_options: { + // 'key': '_id' + // } + // }); // - // Usage: - // - // var backend = new recline.Backend.CouchDB(); - // var dataset = new recline.Model.Dataset({ - // db_url: '/couchdb/mydb', - // view_url: '/couchdb/mydb/_design/design1/_views/view1', - // query_options: { - // 'key': 'some_document_key' - // } - // }); - // backend.fetch(dataset.toJSON()); - // backend.query(query, dataset.toJSON()).done(function () { ... }); + // backend.query(query, dataset.toJSON()).done(function () { ... }); // // Alternatively: - // var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); - // dataset.fetch(); - // var results = dataset.query(query_obj); + // + // var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); + // dataset.fetch(); + // var results = dataset.query(query_obj); // // Additionally, the Dataset instance may define three methods: + // // function record_update (record, document) { ... } // function record_delete (record, document) { ... } // function record_create (record, document) { ... } + // // Where `record` is the JSON representation of the Record/Document instance // and `document` is the JSON document stored in couchdb. // When _all_docs view is used (default), a record is the same as a document @@ -494,6 +496,6 @@ _deleteDocument = function (del_doc, dataset) { dfd.reject(args); }); return dfd.promise(); + } }; - }(jQuery, this.recline.Backend.CouchDB)); diff --git a/src/backend.csv.js b/src/backend.csv.js index 5776eb71..81e69749 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -59,8 +59,14 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // // @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} [separator=','] Separator for CSV file + // @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 '"' + // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parseCSV= function(s, options) { @@ -69,8 +75,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var options = options || {}; var trm = (options.trim === false) ? false : true; - var separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. inQuote = false, @@ -105,7 +111,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; cur = s.charAt(i); // If we are at a EOF or EOR - if (inQuote === false && (cur === separator || cur === "\n")) { + if (inQuote === false && (cur === delimiter || cur === "\n")) { field = processField(field); // Add the current field to the current row row.push(field); @@ -118,8 +124,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; fieldQuoted = false; } else { - // If it's not a delimiter, add it to the field buffer - if (cur !== delimiter) { + // If it's not a quotechar, add it to the field buffer + if (cur !== quotechar) { field += cur; } else { if (!inQuote) { @@ -127,9 +133,9 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; inQuote = true; fieldQuoted = true; } else { - // Next char is delimiter, this is an escaped delimiter - if (s.charAt(i + 1) === delimiter) { - field += delimiter; + // Next char is quotechar, this is an escaped quotechar + if (s.charAt(i + 1) === quotechar) { + field += quotechar; // Skip the next char i += 1; } else { @@ -149,23 +155,48 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; return out; }; - // Converts an array of arrays into a Comma Separated Values string. - // Each array becomes a line in the CSV. + // ### 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 {Array} a The array of arrays to convert - // @param {Object} options Options for loading CSV including - // @param {String} [separator=','] Separator for CSV file - // Heavily based on uselesscode's JS CSV parser (MIT Licensed): + // @param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows: + // + // { + // fields: [ {id: .., ...}, {id: ..., + // records: [ { record }, { record }, ... ] + // ... // more attributes we do not care about + // } + // + // @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(a, options) { + 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 separator = options.separator || ','; - var delimiter = options.delimiter || '"'; + var delimiter = options.delimiter || ','; + var quotechar = options.quotechar || '"'; var cur = '', // The character we are currently processing. field = '', // Buffer for building up the current field @@ -181,7 +212,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; field = ''; } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { // Convert string to delimited string - field = delimiter + field + delimiter; + field = quotechar + field + quotechar; } else if (typeof field === "number") { // Convert number to string field = field.toString(10); @@ -202,7 +233,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; row = ''; } else { // Add the current field to the current row - row += field + separator; + row += field + delimiter; } // Flush the field buffer field = ''; diff --git a/src/view.graph.js b/src/view.graph.js index bc616e0d..14d912df 100644 --- a/src/view.graph.js +++ b/src/view.graph.js @@ -118,26 +118,27 @@ my.Graph = Backbone.View.extend({ return getFormattedX(x); }; + // infoboxes on mouse hover on points/bars etc var trackFormatter = function (obj) { - var x = obj.x; - var y = obj.y; - // it's horizontal so we have to flip - if (self.state.attributes.graphType === 'bars') { - var _tmp = x; - x = y; - y = _tmp; - } - - x = getFormattedX(x); + var x = obj.x; + var y = obj.y; + // it's horizontal so we have to flip + if (self.state.attributes.graphType === 'bars') { + var _tmp = x; + x = y; + y = _tmp; + } + + x = getFormattedX(x); - var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { - group: self.state.attributes.group, - x: x, - series: obj.series.label, - y: y - }); - - return content; + var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', { + group: self.state.attributes.group, + x: x, + series: obj.series.label, + y: y + }); + + return content; }; var getFormattedX = function (x) { @@ -208,18 +209,18 @@ my.Graph = Backbone.View.extend({ xaxis: yaxis, yaxis: xaxis, mouse: { - track: true, - relative: true, - trackFormatter: trackFormatter, - fillColor: '#FFFFFF', - fillOpacity: 0.3, - position: 'e' + track: true, + relative: true, + trackFormatter: trackFormatter, + fillColor: '#FFFFFF', + fillOpacity: 0.3, + position: 'e' }, bars: { - show: true, - horizontal: true, - shadowSize: 0, - barWidth: 0.8 + show: true, + horizontal: true, + shadowSize: 0, + barWidth: 0.8 }, }, columns: { diff --git a/src/view.map.js b/src/view.map.js index 1c26da2f..cd69bf34 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -23,6 +23,11 @@ this.recline.View = this.recline.View || {}; // latField: {id of field containing latitude in the dataset} // } //
    +// +// 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: ' \
    \ @@ -41,6 +46,8 @@ my.Map = Backbone.View.extend({ this.el = $(this.el); this.visible = true; this.mapReady = false; + // this will be the Leaflet L.Map object (setup below) + this.map = null; var stateData = _.extend({ geomField: null, @@ -78,6 +85,34 @@ my.Map = Backbone.View.extend({ this.elSidebar = this.menu.el; }, + // ## Customization Functions + // + // The following methods are designed for overriding in order to customize + // behaviour + + // ### infobox + // + // 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 (key in record.attributes){ + if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ + html += '
    ' + key + ': '+ record.attributes[key] + '
    '; + } + } + return html; + }, + + // END: Customization section + // ---- + // ### Public: Adds the necessary elements to the page. // // Also sets up the editor fields and the map if necessary. @@ -172,19 +207,12 @@ my.Map = Backbone.View.extend({ // Empty field return true; } else if (feature instanceof Object){ - // Build popup contents - // TODO: mustache? - html = ''; - for (key in doc.attributes){ - if (!(self.state.get('geomField') && key == self.state.get('geomField'))){ - html += '
    ' + key + ': '+ doc.attributes[key] + '
    '; - } - } - feature.properties = {popupContent: html}; - - // Add a reference to the model id, which will allow us to - // link this Leaflet layer to a Recline doc - feature.properties.cid = doc.cid; + 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); diff --git a/test/backend.csv.test.js b/test/backend.csv.test.js index 839c3cfe..c027f097 100644 --- a/test/backend.csv.test.js +++ b/test/backend.csv.test.js @@ -34,12 +34,12 @@ test("parseCSV", function() { deepEqual(row, {Name: 'Jones, Jay', Value: 10}); }); -test("parseCSVsemicolon", function() { +test("parseCSV - semicolon", function() { var csv = '"Jones; Jay";10\n' + '"Xyz ""ABC"" O\'Brien";11:35\n' + '"Other; AN";12:35\n'; - var array = recline.Backend.CSV.parseCSV(csv, {separator : ';'}); + var array = recline.Backend.CSV.parseCSV(csv, {delimiter : ';'}); var exp = [ ['Jones; Jay', 10], ['Xyz "ABC" O\'Brien', '11:35' ], @@ -49,12 +49,12 @@ test("parseCSVsemicolon", function() { }); -test("parseCSVdelimiter", function() { +test("parseCSV - quotechar", function() { var csv = "'Jones, Jay',10\n" + "'Xyz \"ABC\" O''Brien',11:35\n" + "'Other; AN',12:35\n"; - var array = recline.Backend.CSV.parseCSV(csv, {delimiter:"'"}); + var array = recline.Backend.CSV.parseCSV(csv, {quotechar:"'"}); var exp = [ ["Jones, Jay", 10], ["Xyz \"ABC\" O'Brien", "11:35" ], @@ -64,7 +64,7 @@ test("parseCSVdelimiter", function() { }); -test("serializeCSV", function() { +test("serializeCSV - Array", function() { var csv = [ ['Jones, Jay', 10], ['Xyz "ABC" O\'Brien', '11:35' ], @@ -78,5 +78,22 @@ test("serializeCSV", function() { deepEqual(array, exp); }); +test("serializeCSV - Object", function() { + var indata = { + fields: [ {id: 'name'}, {id: 'number'}], + records: [ + {name: 'Jones, Jay', number: 10}, + {name: 'Xyz "ABC" O\'Brien', number: '11:35' }, + {name: 'Other, AN', number: '12:35' } + ] + }; + + var array = recline.Backend.CSV.serializeCSV(indata); + var exp = 'name,number\n' + + '"Jones, Jay",10\n' + + '"Xyz \"ABC\" O\'Brien",11:35\n' + + '"Other, AN",12:35\n'; + deepEqual(array, exp); +}); })(this.jQuery); diff --git a/test/index.html b/test/index.html index 541d5f6d..aa786ada 100644 --- a/test/index.html +++ b/test/index.html @@ -4,8 +4,6 @@ Qunit Tests - - @@ -14,7 +12,6 @@ - diff --git a/test/view.map.test.js b/test/view.map.test.js index 19b1403f..1efd3d83 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -32,9 +32,6 @@ test('basics', function () { $('.fixtures').append(view.el); view.render(); - //Fire query, otherwise the map won't be initialized - dataset.query(); - assertPresent('.editor-field-type', view.elSidebar); // Check that the Leaflet map was set up @@ -95,9 +92,6 @@ test('GeoJSON geom field', function () { $('.fixtures').append(view.el); view.render(); - //Fire query, otherwise the map won't be initialized - dataset.query(); - // Check that all features were created equal(_getFeaturesCount(view.features),3); @@ -143,9 +137,6 @@ test('Popup', function () { $('.fixtures').append(view.el); view.render(); - //Fire query, otherwise the map won't be initialized - dataset.query(); - var marker = view.el.find('.leaflet-marker-icon').first(); assertPresent(marker); @@ -167,6 +158,30 @@ test('Popup', function () { view.remove(); }); +test('Popup - Custom', function () { + var dataset = GeoJSONFixture.getDataset(); + var view = new recline.View.Map({ + model: dataset + }); + $('.fixtures').append(view.el); + view.infobox = function(record) { + var html = Mustache.render('

    {{x}}

    y: {{y}}', record.toJSON()); + return html; + }; + view.render(); + + var marker = view.el.find('.leaflet-marker-icon').first(); + _.values(view.features._layers)[0].fire('click'); + var popup = view.el.find('.leaflet-popup-content'); + + assertPresent(popup); + + var text = popup.html(); + ok((text.indexOf('

    3

    y: 6') != -1)) + + view.remove(); +}); + test('MapMenu', function () { var dataset = Fixture.getDataset(); var controls = new recline.View.MapMenu({ diff --git a/vendor/jquery.flot/0.7/jquery.flot.js b/vendor/jquery.flot/0.7/jquery.flot.js deleted file mode 100644 index aabc544e..00000000 --- a/vendor/jquery.flot/0.7/jquery.flot.js +++ /dev/null @@ -1,2599 +0,0 @@ -/*! Javascript plotting library for jQuery, v. 0.7. - * - * Released under the MIT license by IOLA, December 2007. - * - */ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of colums in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85 // set to 0 to avoid background - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - - // mode specific options - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null, // number or [number, "unit"] - monthNames: null, // list of names of months - timeformat: null, // format string to use - twelveHourClock: false // 12 or 24 time in time mode - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // or "center" - horizontal: false - }, - shadowSize: 3 - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - hooks: {} - }, - canvas = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top) - }; - }; - plot.shutdown = shutdown; - plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(canvas); - resizeCanvas(overlay); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - var i; - - $.extend(true, options, opts); - - if (options.xaxis.color == null) - options.xaxis.color = options.grid.color; - if (options.yaxis.color == null) - options.yaxis.color = options.grid.color; - - if (options.xaxis.tickColor == null) // backwards-compatibility - options.xaxis.tickColor = options.grid.tickColor; - if (options.yaxis.tickColor == null) // backwards-compatibility - options.yaxis.tickColor = options.grid.tickColor; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // fill in defaults in axes, copy at least always the - // first as the rest of the code assumes it'll be there - for (i = 0; i < Math.max(1, options.xaxes.length); ++i) - options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]); - for (i = 0; i < Math.max(1, options.yaxes.length); ++i) - options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]); - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - var i; - - // collect what we already got of colors - var neededColors = series.length, - usedColors = [], - assignedColors = []; - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - --neededColors; - if (typeof sc == "number") - assignedColors.push(sc); - else - usedColors.push($.color.parse(series[i].color)); - } - } - - // we might need to generate more colors if higher indices - // are assigned - for (i = 0; i < assignedColors.length; ++i) { - neededColors = Math.max(neededColors, assignedColors[i] + 1); - } - - // produce colors as needed - var colors = [], variation = 0; - i = 0; - while (colors.length < neededColors) { - var c; - if (options.colors.length == i) // check degenerate case - c = $.color.make(100, 100, 100); - else - c = $.color.parse(options.colors[i]); - - // vary color if needed - var sign = variation % 2 == 1 ? -1 : 1; - c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2) - - // FIXME: if we're getting to close to something else, - // we should probably skip this one - colors.push(c); - - ++i; - if (i >= options.colors.length) { - i = 0; - ++variation; - } - } - - // fill in the options - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - var data = s.data, format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - format.push({ y: true, number: true, required: false, defaultValue: 0 }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.x) - updateAxis(s.xaxis, val, val); - if (f.y) - updateAxis(s.yaxis, val, val); - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points, - ps = s.datapoints.pointsize; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function makeCanvas(skipPositioning, cls) { - var c = document.createElement('canvas'); - c.className = cls; - c.width = canvasWidth; - c.height = canvasHeight; - - if (!skipPositioning) - $(c).css({ position: 'absolute', left: 0, top: 0 }); - - $(c).appendTo(placeholder); - - if (!c.getContext) // excanvas hack - c = window.G_vmlCanvasManager.initElement(c); - - // used for resetting in case we get replotted - c.getContext("2d").save(); - - return c; - } - - function getCanvasDimensions() { - canvasWidth = placeholder.width(); - canvasHeight = placeholder.height(); - - if (canvasWidth <= 0 || canvasHeight <= 0) - throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; - } - - function resizeCanvas(c) { - // resizing should reset the state (excanvas seems to be - // buggy though) - if (c.width != canvasWidth) - c.width = canvasWidth; - - if (c.height != canvasHeight) - c.height = canvasHeight; - - // so try to get back to the initial state (even if it's - // gone now, this should be safe according to the spec) - var cctx = c.getContext("2d"); - cctx.restore(); - - // and save again - cctx.save(); - } - - function setupCanvases() { - var reused, - existingCanvas = placeholder.children("canvas.base"), - existingOverlay = placeholder.children("canvas.overlay"); - - if (existingCanvas.length == 0 || existingOverlay == 0) { - // init everything - - placeholder.html(""); // make sure placeholder is clear - - placeholder.css({ padding: 0 }); // padding messes up the positioning - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - getCanvasDimensions(); - - canvas = makeCanvas(true, "base"); - overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features - - reused = false; - } - else { - // reuse existing elements - - canvas = existingCanvas.get(0); - overlay = existingOverlay.get(0); - - reused = true; - } - - ctx = canvas.getContext("2d"); - octx = overlay.getContext("2d"); - - // we include the canvas in the event holder too, because IE 7 - // sometimes has trouble with the stacking order - eventHolder = $([overlay, canvas]); - - if (reused) { - // run shutdown in the old plot object - placeholder.data("plot").shutdown(); - - // reset reused canvases - plot.resize(); - - // make sure overlay pixels are cleared (canvas is cleared when we redraw) - octx.clearRect(0, 0, canvasWidth, canvasHeight); - - // then whack any remaining obvious garbage left - eventHolder.unbind(); - placeholder.children().not([canvas, overlay]).remove(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - eventHolder.mouseleave(onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - var opts = axis.options, i, ticks = axis.ticks || [], labels = [], - l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; - - function makeDummyDiv(labels, width) { - return $('
    ' + - '
    ' - + labels.join("") + '
    ') - .appendTo(placeholder); - } - - if (axis.direction == "x") { - // to avoid measuring the widths of the labels (it's slow), we - // construct fixed-size boxes and put the labels inside - // them, we don't need the exact figures and the - // fixed-size box content is easy to center - if (w == null) - w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1)); - - // measure x label heights - if (h == null) { - labels = []; - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
    ' + l + '
    '); - } - - if (labels.length > 0) { - // stick them all in the same div and measure - // collective height - labels.push('
    '); - dummyDiv = makeDummyDiv(labels, "width:10000px;"); - h = dummyDiv.height(); - dummyDiv.remove(); - } - } - } - else if (w == null || h == null) { - // calculate y label dimensions - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
    ' + l + '
    '); - } - - if (labels.length > 0) { - dummyDiv = makeDummyDiv(labels, ""); - if (w == null) - w = dummyDiv.children().width(); - if (h == null) - h = dummyDiv.find("div.tickLabel").height(); - dummyDiv.remove(); - } - } - - if (w == null) - w = 0; - if (h == null) - h = 0; - - axis.labelWidth = w; - axis.labelHeight = h; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - tickLength = axis.options.tickLength, - axismargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - all = axis.direction == "x" ? xaxes : yaxes, - index; - - // determine axis margin - var samePosition = $.grep(all, function (a) { - return a && a.options.position == pos && a.reserveSpace; - }); - if ($.inArray(axis, samePosition) == samePosition.length - 1) - axismargin = 0; // outermost - - // determine tick length - if we're innermost, we can use "full" - if (tickLength == null) - tickLength = "full"; - - var sameDirection = $.grep(all, function (a) { - return a && a.reserveSpace; - }); - - var innermost = $.inArray(axis, sameDirection) == 0; - if (!innermost && tickLength == "full") - tickLength = 5; - - if (!isNaN(+tickLength)) - padding += +tickLength; - - // compute box - if (axis.direction == "x") { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axismargin; - axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axismargin, height: lh }; - plotOffset.top += lh + axismargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axismargin, width: lw }; - plotOffset.left += lw + axismargin; - } - else { - plotOffset.right += lw + axismargin; - axis.box = { left: canvasWidth - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // set remaining bounding box coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left; - axis.box.width = plotWidth; - } - else { - axis.box.top = plotOffset.top; - axis.box.height = plotHeight; - } - } - - function setupGrid() { - var i, axes = allAxes(); - - // first calculate the plot and axis box dimensions - - $.each(axes, function (_, axis) { - axis.show = axis.options.show; - if (axis.show == null) - axis.show = axis.used; // by default an axis is visible if it's got data - - axis.reserveSpace = axis.show || axis.options.reserveSpace; - - setRange(axis); - }); - - allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); - - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; - if (options.grid.show) { - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions in house, we can compute the - // axis boxes, start from the outside (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - var minMargin = options.grid.minBorderMargin; - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2); - } - - for (var a in plotOffset) { - plotOffset[a] += options.grid.borderWidth; - plotOffset[a] = Math.max(minMargin, plotOffset[a]); - } - } - - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - - // now we got the proper plotWidth/Height, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (options.grid.show) { - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - - insertAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight); - - var delta = (axis.max - axis.min) / noTicks, - size, generator, unit, formatter, i, magn, norm; - - if (opts.mode == "time") { - // pretty handling of time - - // map of app. size of time units in milliseconds - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - var spec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"], [3, "month"], [6, "month"], - [1, "year"] - ]; - - var minSize = 0; - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") - minSize = opts.tickSize; - else - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - - for (var i = 0; i < spec.length - 1; ++i) - if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) - break; - size = spec[i][0]; - unit = spec[i][1]; - - // special-case the possibility of several years - if (unit == "year") { - magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); - norm = (delta / timeUnitSize.year) / magn; - if (norm < 1.5) - size = 1; - else if (norm < 3) - size = 2; - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - } - - axis.tickSize = opts.tickSize || [size, unit]; - - generator = function(axis) { - var ticks = [], - tickSize = axis.tickSize[0], unit = axis.tickSize[1], - d = new Date(axis.min); - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") - d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); - if (unit == "minute") - d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); - if (unit == "hour") - d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); - if (unit == "month") - d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); - if (unit == "year") - d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); - - // reset smaller components - d.setUTCMilliseconds(0); - if (step >= timeUnitSize.minute) - d.setUTCSeconds(0); - if (step >= timeUnitSize.hour) - d.setUTCMinutes(0); - if (step >= timeUnitSize.day) - d.setUTCHours(0); - if (step >= timeUnitSize.day * 4) - d.setUTCDate(1); - if (step >= timeUnitSize.year) - d.setUTCMonth(0); - - - var carry = 0, v = Number.NaN, prev; - do { - prev = v; - v = d.getTime(); - ticks.push(v); - if (unit == "month") { - if (tickSize < 1) { - // a bit complicated - we'll divide the month - // up but we need to take care of fractions - // so we don't end up in the middle of a day - d.setUTCDate(1); - var start = d.getTime(); - d.setUTCMonth(d.getUTCMonth() + 1); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getUTCHours(); - d.setUTCHours(0); - } - else - d.setUTCMonth(d.getUTCMonth() + tickSize); - } - else if (unit == "year") { - d.setUTCFullYear(d.getUTCFullYear() + tickSize); - } - else - d.setTime(v + step); - } while (v < axis.max && v != prev); - - return ticks; - }; - - formatter = function (v, axis) { - var d = new Date(v); - - // first check global format - if (opts.timeformat != null) - return $.plot.formatDate(d, opts.timeformat, opts.monthNames); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - - if (t < timeUnitSize.minute) - fmt = "%h:%M:%S" + suffix; - else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) - fmt = "%h:%M" + suffix; - else - fmt = "%b %d %h:%M" + suffix; - } - else if (t < timeUnitSize.month) - fmt = "%b %d"; - else if (t < timeUnitSize.year) { - if (span < timeUnitSize.year) - fmt = "%b"; - else - fmt = "%b %y"; - } - else - fmt = "%y"; - - return $.plot.formatDate(d, fmt, opts.monthNames); - }; - } - else { - // pretty rounding of base-10 numbers - var maxDec = opts.tickDecimals; - var dec = -Math.floor(Math.log(delta) / Math.LN10); - if (maxDec != null && dec > maxDec) - dec = maxDec; - - magn = Math.pow(10, -dec); - norm = delta / magn; // norm is between 1.0 and 10.0 - - if (norm < 1.5) - size = 1; - else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) - size = opts.minTickSize; - - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - generator = function (axis) { - var ticks = []; - - // spew out all possible ticks - var start = floorInBase(axis.min, axis.tickSize), - i = 0, v = Number.NaN, prev; - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - formatter = function (v, axis) { - return v.toFixed(axis.tickDecimals); - }; - } - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = generator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - generator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (axis.mode != "time" && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1), - ts = generator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - - axis.tickGenerator = generator; - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - else - axis.tickFormatter = formatter; - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks({ min: axis.min, max: axis.max }); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) - drawGrid(); - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) - drawGrid(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - var axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - if (xrange.from == xrange.to && yrange.from == yrange.to) - continue; - - // then draw - xrange.from = xrange.axis.p2c(xrange.from); - xrange.to = xrange.axis.p2c(xrange.to); - yrange.from = yrange.axis.p2c(yrange.from); - yrange.to = yrange.axis.p2c(yrange.to); - - if (xrange.from == xrange.to || yrange.from == yrange.to) { - // draw line - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; - ctx.moveTo(xrange.from, yrange.from); - ctx.lineTo(xrange.to, yrange.to); - ctx.stroke(); - } - else { - // fill area - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - var axes = allAxes(), bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue - - ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString(); - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth; - else - yoff = plotHeight; - - if (ctx.lineWidth == 1) { - x = Math.floor(x) + 0.5; - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" && bw > 0 - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - - ctx.restore(); - } - - function insertAxisLabels() { - placeholder.find(".tickLabels").remove(); - - var html = ['
    ']; - - var axes = allAxes(); - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box; - if (!axis.show) - continue; - //debug: html.push('
    ') - html.push('
    '); - for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - var pos = {}, align; - - if (axis.direction == "x") { - align = "center"; - pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2); - if (axis.position == "bottom") - pos.top = box.top + box.padding; - else - pos.bottom = canvasHeight - (box.top + box.height - box.padding); - } - else { - pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2); - if (axis.position == "left") { - pos.right = canvasWidth - (box.left + box.width - box.padding) - align = "right"; - } - else { - pos.left = box.left + box.padding; - align = "left"; - } - } - - pos.width = axis.labelWidth; - - var style = ["position:absolute", "text-align:" + align ]; - for (var a in pos) - style.push(a + ":" + pos[a] + "px") - - html.push('
    ' + tick.label + '
    '); - } - html.push('
    '); - } - - html.push('
    '); - - placeholder.append(html.join("")); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.beginPath(); - c.moveTo(left, bottom); - c.lineTo(left, top); - c.lineTo(right, top); - c.lineTo(right, bottom); - c.fillStyle = fillStyleCallback(bottom, top); - c.fill(); - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom + offset); - if (drawLeft) - c.lineTo(left, top + offset); - else - c.moveTo(left, top + offset); - if (drawTop) - c.lineTo(right, top + offset); - else - c.moveTo(right, top + offset); - if (drawRight) - c.lineTo(right, bottom + offset); - else - c.moveTo(right, bottom + offset); - if (drawBottom) - c.lineTo(left, bottom + offset); - else - c.moveTo(left, bottom + offset); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - placeholder.find(".legend").remove(); - - if (!options.legend.show) - return; - - var fragments = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - for (var i = 0; i < series.length; ++i) { - s = series[i]; - label = s.label; - if (!label) - continue; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push('
    ' + label + '
    ' + fragments.join("") + '
    '; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
    ' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
    ').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
    ').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - ps = s.datapoints.pointsize, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, 30); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") - point = s.data[point]; - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis; - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var radius = 1.5 * pointRadius, - x = axisx.p2c(x), - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness) - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.7"; - - $.plot.plugins = []; - - // returns a string with the date d formatted according to fmt - $.plot.formatDate = function(d, fmt, monthNames) { - var leftPad = function(n) { - n = "" + n; - return n.length == 1 ? "0" + n : n; - }; - - var r = []; - var escape = false, padNext = false; - var hours = d.getUTCHours(); - var isAM = hours < 12; - if (monthNames == null) - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - - if (fmt.search(/%p|%P/) != -1) { - if (hours > 12) { - hours = hours - 12; - } else if (hours == 0) { - hours = 12; - } - } - for (var i = 0; i < fmt.length; ++i) { - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'h': c = "" + hours; break; - case 'H': c = leftPad(hours); break; - case 'M': c = leftPad(d.getUTCMinutes()); break; - case 'S': c = leftPad(d.getUTCSeconds()); break; - case 'd': c = "" + d.getUTCDate(); break; - case 'm': c = "" + (d.getUTCMonth() + 1); break; - case 'y': c = "" + d.getUTCFullYear(); break; - case 'b': c = "" + monthNames[d.getUTCMonth()]; break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case '0': c = ""; padNext = true; break; - } - if (c && padNext) { - c = leftPad(c); - padNext = false; - } - r.push(c); - if (!padNext) - escape = false; - } - else { - if (c == "%") - escape = true; - else - r.push(c); - } - } - return r.join(""); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/vendor/leaflet/0.3.1/images/layers.png b/vendor/leaflet/0.3.1/images/layers.png deleted file mode 100644 index 9be965fc..00000000 Binary files a/vendor/leaflet/0.3.1/images/layers.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/images/marker-shadow.png b/vendor/leaflet/0.3.1/images/marker-shadow.png deleted file mode 100644 index a64f6a67..00000000 Binary files a/vendor/leaflet/0.3.1/images/marker-shadow.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/images/marker.png b/vendor/leaflet/0.3.1/images/marker.png deleted file mode 100644 index bef032e6..00000000 Binary files a/vendor/leaflet/0.3.1/images/marker.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/images/popup-close.png b/vendor/leaflet/0.3.1/images/popup-close.png deleted file mode 100644 index c8faec5e..00000000 Binary files a/vendor/leaflet/0.3.1/images/popup-close.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/images/zoom-in.png b/vendor/leaflet/0.3.1/images/zoom-in.png deleted file mode 100644 index 9f473d64..00000000 Binary files a/vendor/leaflet/0.3.1/images/zoom-in.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/images/zoom-out.png b/vendor/leaflet/0.3.1/images/zoom-out.png deleted file mode 100644 index f0a5b5d6..00000000 Binary files a/vendor/leaflet/0.3.1/images/zoom-out.png and /dev/null differ diff --git a/vendor/leaflet/0.3.1/leaflet.css b/vendor/leaflet/0.3.1/leaflet.css deleted file mode 100644 index 119ddb2b..00000000 --- a/vendor/leaflet/0.3.1/leaflet.css +++ /dev/null @@ -1,323 +0,0 @@ -/* required styles */ - -.leaflet-map-pane, -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-tile-pane, -.leaflet-overlay-pane, -.leaflet-shadow-pane, -.leaflet-marker-pane, -.leaflet-popup-pane, -.leaflet-overlay-pane svg, -.leaflet-zoom-box, -.leaflet-image-layer { /* TODO optimize classes */ - position: absolute; - } -.leaflet-container { - overflow: hidden; - } -.leaflet-tile-pane, .leaflet-container { - -webkit-transform: translate3d(0,0,0); - } -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - } -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; - } -.leaflet-clickable { - cursor: pointer; - } -.leaflet-container img { - max-width: none !important; - } - -.leaflet-tile-pane { z-index: 2; } - -.leaflet-objects-pane { z-index: 3; } -.leaflet-overlay-pane { z-index: 4; } -.leaflet-shadow-pane { z-index: 5; } -.leaflet-marker-pane { z-index: 6; } -.leaflet-popup-pane { z-index: 7; } - -.leaflet-zoom-box { - width: 0; - height: 0; - } - -.leaflet-tile { - visibility: hidden; - } -.leaflet-tile-loaded { - visibility: inherit; - } - -a.leaflet-active { - outline: 2px solid orange; - } - - -/* Leaflet controls */ - -.leaflet-control { - position: relative; - z-index: 7; - } -.leaflet-top, -.leaflet-bottom { - position: absolute; - } -.leaflet-top { - top: 0; - } -.leaflet-right { - right: 0; - } -.leaflet-bottom { - bottom: 0; - } -.leaflet-left { - left: 0; - } -.leaflet-control { - float: left; - clear: both; - } -.leaflet-right .leaflet-control { - float: right; - } -.leaflet-top .leaflet-control { - margin-top: 10px; - } -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } -.leaflet-left .leaflet-control { - margin-left: 10px; - } -.leaflet-right .leaflet-control { - margin-right: 10px; - } - -.leaflet-control-zoom, .leaflet-control-layers { - -moz-border-radius: 7px; - -webkit-border-radius: 7px; - border-radius: 7px; - } -.leaflet-control-zoom { - padding: 5px; - background: rgba(0, 0, 0, 0.25); - } -.leaflet-control-zoom a { - background-color: rgba(255, 255, 255, 0.75); - } -.leaflet-control-zoom a, .leaflet-control-layers a { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } -.leaflet-control-zoom a { - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - width: 19px; - height: 19px; - } -.leaflet-control-zoom a:hover { - background-color: #fff; - } -.leaflet-big-buttons .leaflet-control-zoom a { - width: 27px; - height: 27px; - } -.leaflet-control-zoom-in { - background-image: url(images/zoom-in.png); - margin-bottom: 5px; - } -.leaflet-control-zoom-out { - background-image: url(images/zoom-out.png); - } - -.leaflet-control-layers { - -moz-box-shadow: 0 0 7px #999; - -webkit-box-shadow: 0 0 7px #999; - box-shadow: 0 0 7px #999; - - background: #f8f8f9; - } -.leaflet-control-layers a { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } -.leaflet-big-buttons .leaflet-control-layers a { - width: 44px; - height: 44px; - } -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - color: #333; - background: #fff; - } -.leaflet-control-layers input { - margin-top: 2px; - position: relative; - top: 1px; - } -.leaflet-control-layers label { - display: block; - } -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } - -.leaflet-container .leaflet-control-attribution { - margin: 0; - padding: 0 5px; - - font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - color: #333; - - background-color: rgba(255, 255, 255, 0.7); - - -moz-box-shadow: 0 0 7px #ccc; - -webkit-box-shadow: 0 0 7px #ccc; - box-shadow: 0 0 7px #ccc; - } - - -/* Fade animations */ - -.leaflet-fade-anim .leaflet-tile { - opacity: 0; - - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-tile-loaded { - opacity: 1; - } - -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } - -.leaflet-zoom-anim .leaflet-tile { - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; - } - -.leaflet-zoom-anim .leaflet-objects-pane { - visibility: hidden; - } - - -/* Popup layout */ - -.leaflet-popup { - position: absolute; - text-align: center; - -webkit-transform: translate3d(0,0,0); - } -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - } -.leaflet-popup-content { - margin: 19px; - } -.leaflet-popup-tip-container { - margin: 0 auto; - width: 40px; - height: 16px; - position: relative; - overflow: hidden; - } -.leaflet-popup-tip { - width: 15px; - height: 15px; - padding: 1px; - - margin: -8px auto 0; - - -moz-transform: rotate(45deg); - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); - } -.leaflet-popup-close-button { - position: absolute; - top: 9px; - right: 9px; - - width: 10px; - height: 10px; - - overflow: hidden; - } -.leaflet-popup-content p { - margin: 18px 0; - } - - -/* Visual appearance */ - -.leaflet-container { - background: #ddd; - } -.leaflet-container a { - color: #0078A8; - } -.leaflet-zoom-box { - border: 2px dotted #05f; - background: white; - opacity: 0.5; - } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { - background: white; - - box-shadow: 0 1px 10px #888; - -moz-box-shadow: 0 1px 10px #888; - -webkit-box-shadow: 0 1px 14px #999; - } -.leaflet-popup-content-wrapper { - -moz-border-radius: 20px; - -webkit-border-radius: 20px; - border-radius: 20px; - } -.leaflet-popup-content { - font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif; - } -.leaflet-popup-close-button { - background: white url(images/popup-close.png); - } diff --git a/vendor/leaflet/0.3.1/leaflet.ie.css b/vendor/leaflet/0.3.1/leaflet.ie.css deleted file mode 100644 index a120c0cb..00000000 --- a/vendor/leaflet/0.3.1/leaflet.ie.css +++ /dev/null @@ -1,48 +0,0 @@ -.leaflet-tile { - filter: inherit; - } - -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - -.leaflet-control { - display: inline; - } - -.leaflet-popup-tip { - width: 21px; - _width: 27px; - margin: 0 auto; - _margin-top: -3px; - - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - } -.leaflet-popup-tip-container { - margin-top: -1px; - } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { - border: 1px solid #bbb; - } - -.leaflet-control-zoom { - filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#3F000000',EndColorStr='#3F000000'); - } -.leaflet-control-zoom a { - background-color: #eee; - } -.leaflet-control-zoom a:hover { - background-color: #fff; - } -.leaflet-control-layers-toggle { - } -.leaflet-control-attribution, .leaflet-control-layers { - background: white; - } \ No newline at end of file diff --git a/vendor/leaflet/0.3.1/leaflet.js b/vendor/leaflet/0.3.1/leaflet.js deleted file mode 100644 index a2bebba1..00000000 --- a/vendor/leaflet/0.3.1/leaflet.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - Copyright (c) 2010-2011, CloudMade, Vladimir Agafonkin - Leaflet is a modern open-source JavaScript library for interactive maps. - http://leaflet.cloudmade.com -*/ -(function(a){a.L={VERSION:"0.3",ROOT_URL:a.L_ROOT_URL||function(){var a=document.getElementsByTagName("script"),b=/\/?leaflet[\-\._]?([\w\-\._]*)\.js\??/,c,d,e,f;for(c=0,d=a.length;c0},removeEventListener:function(a,b,c){if(!this.hasEventListeners(a))return this;for(var d=0,e=this._leaflet_events,f=e[a].length;d=this.min.x&&c.x<=this.max.x&&b.y>=this.min.y&&c.y<=this.max.y},intersects:function(a){var b=this.min,c=this.max,d=a.min,e=a.max,f=e.x>=b.x&&d.x<=c.x,g=e.y>=b.y&&d.y<=c.y;return f&&g}}),L.Transformation=L.Class.extend({initialize:function(a,b,c,d){this._a=a,this._b=b,this._c=c,this._d=d},transform:function(a,b){return this._transform(a.clone(),b)},_transform:function(a,b){return b=b||1,a.x=b*(this._a*a.x+this._b),a.y=b*(this._c*a.y+this._d),a},untransform:function(a,b){return b=b||1,new L.Point((a.x/b-this._b)/this._a,(a.y/b-this._d)/this._c)}}),L.DomUtil={get:function(a){return typeof a=="string"?document.getElementById(a):a},getStyle:function(a,b){var c=a.style[b];!c&&a.currentStyle&&(c=a.currentStyle[b]);if(!c||c==="auto"){var d=document.defaultView.getComputedStyle(a,null);c=d?d[b]:null}return c==="auto"?null:c},getViewportOffset:function(a){var b=0,c=0,d=a,e=document.body;do{b+=d.offsetTop||0,c+=d.offsetLeft||0;if(d.offsetParent===e&&L.DomUtil.getStyle(d,"position")==="absolute")break;d=d.offsetParent}while(d);d=a;do{if(d===e)break;b-=d.scrollTop||0,c-=d.scrollLeft||0,d=d.parentNode}while(d);return new L.Point(c,b)},create:function(a,b,c){var d=document.createElement(a);return d.className=b,c&&c.appendChild(d),d},disableTextSelection:function(){document.selection&&document.selection.empty&&document.selection.empty(),this._onselectstart||(this._onselectstart=document.onselectstart,document.onselectstart=L.Util.falseFn)},enableTextSelection:function(){document.onselectstart=this._onselectstart,this._onselectstart=null},hasClass:function(a,b){return a.className.length>0&&RegExp("(^|\\s)"+b+"(\\s|$)").test(a.className)},addClass:function(a,b){L.DomUtil.hasClass(a,b)||(a.className+=(a.className?" ":"")+b)},removeClass:function(a,b){a.className=a.className.replace(/(\S+)\s*/g,function(a,c){return c===b?"":a}).replace(/^\s+/,"")},setOpacity:function(a,b){L.Browser.ie?a.style.filter="alpha(opacity="+Math.round(b*100)+")":a.style.opacity=b},testProp:function(a){var b=document.documentElement.style;for(var c=0;c=b.lat&&e.lat<=c.lat&&d.lng>=b.lng&&e.lng<=c.lng},intersects:function(a){var b=this._southWest,c=this._northEast,d=a.getSouthWest(),e=a.getNorthEast(),f=e.lat>=b.lat&&d.lat<=c.lat,g=e.lng>=b.lng&&d.lng<=c.lng;return f&&g},toBBoxString:function(){var a=this._southWest,b=this._northEast;return[a.lng,a.lat,b.lng,b.lat].join(",")}}),L.Projection={},L.Projection.SphericalMercator={MAX_LATITUDE:85.0511287798,project:function(a){var b=L.LatLng.DEG_TO_RAD,c=this.MAX_LATITUDE,d=Math.max(Math.min(c,a.lat),-c),e=a.lng*b,f=d*b;return f=Math.log(Math.tan(Math.PI/4+f/2)),new L.Point(e,f)},unproject:function(a,b){var c=L.LatLng.RAD_TO_DEG,d=a.x*c,e=(2*Math.atan(Math.exp(a.y))-Math.PI/2)*c;return new L.LatLng(e,d,b)}},L.Projection.LonLat={project:function(a){return new L.Point(a.lng,a.lat)},unproject:function(a,b){return new L.LatLng(a.y,a.x,b)}},L.CRS={latLngToPoint:function(a,b){var c=this.projection.project(a);return this.transformation._transform(c,b)},pointToLatLng:function(a,b,c){var d=this.transformation.untransform(a,b);return this.projection.unproject(d,c)},project:function(a){return this.projection.project(a)}},L.CRS.EPSG3857=L.Util.extend({},L.CRS,{code:"EPSG:3857",projection:L.Projection.SphericalMercator,transformation:new L.Transformation(.5/Math.PI,.5,-0.5/Math.PI,.5),project:function(a){var b=this.projection.project(a),c=6378137;return b.multiplyBy(c)}}),L.CRS.EPSG900913=L.Util.extend({},L.CRS.EPSG3857,{code:"EPSG:900913"}),L.CRS.EPSG4326=L.Util.extend({},L.CRS,{code:"EPSG:4326",projection:L.Projection.LonLat,transformation:new L.Transformation(1/360,.5,-1/360,.5)}),L.Map=L.Class.extend({includes:L.Mixin.Events,options:{crs:L.CRS.EPSG3857||L.CRS.EPSG4326,scale:function(a){return 256*Math.pow(2,a)},center:null,zoom:null,layers:[],dragging:!0,touchZoom:L.Browser.touch&&!L.Browser.android,scrollWheelZoom:!L.Browser.touch,doubleClickZoom:!0,boxZoom:!0,zoomControl:!0,attributionControl:!0,fadeAnimation:L.DomUtil.TRANSITION&&!L.Browser.android,zoomAnimation:L.DomUtil.TRANSITION&&!L.Browser.android&&!L.Browser.mobileOpera,trackResize:!0,closePopupOnClick:!0,worldCopyJump:!0},initialize:function(a,b){L.Util.setOptions(this,b),this._container=L.DomUtil.get(a);if(this._container._leaflet)throw Error("Map container is already initialized.");this._container._leaflet=!0,this._initLayout(),L.DomEvent&&(this._initEvents(),L.Handler&&this._initInteraction(),L.Control&&this._initControls()),this.options.maxBounds&&this.setMaxBounds(this.options.maxBounds);var c=this.options.center,d=this.options.zoom;c!==null&&d!==null&&this.setView(c,d,!0);var e=this.options.layers;e=e instanceof Array?e:[e],this._tileLayersNum=0,this._initLayers(e)},setView:function(a,b){return this._resetView(a,this._limitZoom(b)),this},setZoom:function(a){return this.setView(this.getCenter(),a)},zoomIn:function(){return this.setZoom(this._zoom+1)},zoomOut:function(){return this.setZoom(this._zoom-1)},fitBounds:function(a){var b=this.getBoundsZoom(a);return this.setView(a.getCenter(),b)},fitWorld:function(){var a=new L.LatLng(-60,-170),b=new L.LatLng(85,179);return this.fitBounds(new L.LatLngBounds(a,b))},panTo:function(a){return this.setView(a,this._zoom)},panBy:function(a){return this.fire("movestart"),this._rawPanBy(a),this.fire("move"),this.fire("moveend"),this},setMaxBounds:function(a){this.options.maxBounds=a;if(!a)return this._boundsMinZoom=null,this;var b=this.getBoundsZoom(a,!0);return this._boundsMinZoom=b,this._loaded&&(this._zoomf.x&&(g=f.x-d.x),c.y>e.y&&(h=e.y-c.y),c.xl&&--m>0)o=h*Math.sin(j),n=Math.PI/2-2*Math.atan(i*Math.pow((1-o)/(1+o),.5*h))-j,j+=n;return new L.LatLng(j*c,f,b)}},L.CRS.EPSG3395=L.Util.extend({},L.CRS,{code:"EPSG:3395",projection:L.Projection.Mercator,transformation:function(){var a=L.Projection.Mercator,b=a.R_MAJOR,c=a.R_MINOR;return new L.Transformation(.5/(Math.PI*b),.5,-0.5/(Math.PI*c),.5)}()}),L.TileLayer=L.Class.extend({includes:L.Mixin.Events,options:{minZoom:0,maxZoom:18,tileSize:256,subdomains:"abc",errorTileUrl:"",attribution:"",opacity:1,scheme:"xyz",continuousWorld:!1,noWrap:!1,zoomOffset:0,zoomReverse:!1,unloadInvisibleTiles:L.Browser.mobile,updateWhenIdle:L.Browser.mobile,reuseTiles:!1},initialize:function(a,b,c){L.Util.setOptions(this,b),this._url=a,this._urlParams=c,typeof this.options.subdomains=="string"&&(this.options.subdomains=this.options.subdomains.split(""))},onAdd:function(a,b){this._map=a,this._insertAtTheBottom=b,this._initContainer(),this._createTileProto(),a.on("viewreset",this._resetCallback,this),this.options.updateWhenIdle?a.on("moveend",this._update,this):(this._limitedUpdate=L.Util.limitExecByInterval(this._update,150,this),a.on("move",this._limitedUpdate,this)),this._reset(),this._update()},onRemove:function(a){this._map.getPanes().tilePane.removeChild(this._container),this._container=null,this._map.off("viewreset",this._resetCallback,this),this.options.updateWhenIdle?this._map.off("moveend",this._update,this):this._map.off("move",this._limitedUpdate,this)},getAttribution:function(){return this.options.attribution},setOpacity:function(a){this.options.opacity=a,this._setOpacity(a);if(L.Browser.webkit)for(var b in this._tiles)this._tiles.hasOwnProperty(b)&&(this._tiles[b].style.webkitTransform+=" translate(0,0)")},_setOpacity:function(a){a<1&&L.DomUtil.setOpacity(this._container,a)},_initContainer:function(){var a=this._map.getPanes().tilePane,b=a.firstChild;if(!this._container||a.empty)this._container=L.DomUtil.create("div","leaflet-layer"),this._insertAtTheBottom&&b?a.insertBefore(this._container,b):a.appendChild(this._container),this._setOpacity(this.options.opacity)},_resetCallback:function(a){this._reset(a.hard)},_reset:function(a){var b;for(b in this._tiles)this._tiles.hasOwnProperty(b)&&this.fire("tileunload",{tile:this._tiles[b]});this._tiles={},this.options.reuseTiles&&(this._unusedTiles=[]),a&&this._container&&(this._container.innerHTML=""),this._initContainer()},_update:function(){var a=this._map.getPixelBounds(),b=this._map.getZoom(),c=this.options.tileSize;if(b>this.options.maxZoom||ba.max.x||da.max.y)f=this._tiles[e],this.fire("tileunload",{tile:f,url:f.src}),f.parentNode===this._container&&this._container.removeChild(f),this.options.reuseTiles&&this._unusedTiles.push(this._tiles[e]),f.src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=",delete this._tiles[e]}},_addTile:function(a,b){var c=this._getTilePos(a),d=this._map.getZoom(),e=a.x+":"+a.y,f=Math.pow(2,this._getOffsetZoom(d));if(!this.options.continuousWorld){if(!this.options.noWrap)a.x=(a.x%f+f)%f;else if(a.x<0||a.x>=f){this._tilesToLoad--;return}if(a.y<0||a.y>=f){this._tilesToLoad--;return}}var g=this._getTile();L.DomUtil.setPosition(g,c),this._tiles[e]=g,this.options.scheme==="tms"&&(a.y=f-a.y-1),this._loadTile(g,a,d),b.appendChild(g)},_getOffsetZoom:function(a){return a=this.options.zoomReverse?this.options.maxZoom-a:a,a+this.options.zoomOffset},_getTilePos:function(a){var b=this._map.getPixelOrigin(),c=this.options.tileSize;return a.multiplyBy(c).subtract(b)},getTileUrl:function(a,b){var c=this.options.subdomains,d=this.options.subdomains[(a.x+a.y)%c.length];return L.Util.template(this._url,L.Util.extend({s:d,z:this._getOffsetZoom(b),x:a.x,y:a.y},this._urlParams))},_createTileProto:function(){this._tileImg=L.DomUtil.create("img","leaflet-tile"),this._tileImg.galleryimg="no";var a=this.options.tileSize;this._tileImg.style.width=a+"px",this._tileImg.style.height=a+"px"},_getTile:function(){if(this.options.reuseTiles&&this._unusedTiles.length>0){var a=this._unusedTiles.pop();return this._resetTile(a),a}return this._createTile()},_resetTile:function(a){},_createTile:function(){var a=this._tileImg.cloneNode(!1);return a.onselectstart=a.onmousemove=L.Util.falseFn,a},_loadTile:function(a,b,c){a._layer=this,a.onload=this._tileOnLoad,a.onerror=this._tileOnError,a.src=this.getTileUrl(b,c)},_tileOnLoad:function(a){var b=this._layer;this.className+=" leaflet-tile-loaded",b.fire("tileload",{tile:this,url:this.src}),b._tilesToLoad--,b._tilesToLoad||b.fire("load")},_tileOnError:function(a){var b=this._layer;b.fire("tileerror",{tile:this,url:this.src});var c=b.options.errorTileUrl;c&&(this.src=c)}}),L.TileLayer.WMS=L.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",version:"1.1.1",layers:"",styles:"",format:"image/jpeg",transparent:!1},initialize:function(a,b){this._url=a,this.wmsParams=L.Util.extend({},this.defaultWmsParams),this.wmsParams.width=this.wmsParams.height=this.options.tileSize;for(var c in b)this.options.hasOwnProperty(c)||(this.wmsParams[c]=b[c]);L.Util.setOptions(this,b)},onAdd:function(a){var b=parseFloat(this.wmsParams.version)<1.3?"srs":"crs";this.wmsParams[b]=a.options.crs.code,L.TileLayer.prototype.onAdd.call(this,a)},getTileUrl:function(a,b){var c=this.options.tileSize,d=a.multiplyBy(c),e=d.add(new L.Point(c,c)),f=this._map.unproject(d,this._zoom,!0),g=this._map.unproject(e,this._zoom,!0),h=this._map.options.crs.project(f),i=this._map.options.crs.project(g),j=[h.x,i.y,i.x,h.y].join(",");return this._url+L.Util.getParamString(this.wmsParams)+"&bbox="+j}}),L.TileLayer.Canvas=L.TileLayer.extend({options:{async:!1},initialize:function(a){L.Util.setOptions(this,a)},redraw:function(){for(var a in this._tiles){var b=this._tiles[a];this._redrawTile(b)}},_redrawTile:function(a){this.drawTile(a,a._tilePoint,a._zoom)},_createTileProto:function(){this._canvasProto=L.DomUtil.create("canvas","leaflet-tile");var a=this.options.tileSize;this._canvasProto.width=a,this._canvasProto.height=a},_createTile:function(){var a=this._canvasProto.cloneNode(!1);return a.onselectstart=a.onmousemove=L.Util.falseFn,a},_loadTile:function(a,b,c){a._layer=this,a._tilePoint=b,a._zoom=c,this.drawTile(a,b,c),this.options.async||this.tileDrawn(a)},drawTile:function(a,b,c){},tileDrawn:function(a){this._tileOnLoad.call(a)}}),L.ImageOverlay=L.Class.extend({includes:L.Mixin.Events,initialize:function(a,b){this._url=a,this._bounds=b},onAdd:function(a){this._map=a,this._image||this._initImage(),a.getPanes().overlayPane.appendChild(this._image),a.on("viewreset",this._reset,this),this._reset()},onRemove:function(a){a.getPanes().overlayPane.removeChild(this._image),a.off("viewreset",this._reset,this)},_initImage:function(){this._image=L.DomUtil.create("img","leaflet-image-layer"),this._image.style.visibility="hidden",L.Util.extend(this._image,{galleryimg:"no",onselectstart:L.Util.falseFn,onmousemove:L.Util.falseFn,onload:L.Util.bind(this._onImageLoad,this),src:this._url})},_reset:function(){var a=this._map.latLngToLayerPoint(this._bounds.getNorthWest()),b=this._map.latLngToLayerPoint(this._bounds.getSouthEast()),c=b.subtract(a);L.DomUtil.setPosition(this._image,a),this._image.style.width=c.x+"px",this._image.style.height=c.y+"px"},_onImageLoad:function(){this._image.style.visibility="",this.fire("load")}}),L.Icon=L.Class.extend({iconUrl:L.ROOT_URL+"images/marker.png",shadowUrl:L.ROOT_URL+"images/marker-shadow.png",iconSize:new L.Point(25,41),shadowSize:new L.Point(41,41),iconAnchor:new L.Point(13,41),popupAnchor:new L.Point(0,-33),initialize:function(a){a&&(this.iconUrl=a)},createIcon:function(){return this._createIcon("icon")},createShadow:function(){return this._createIcon("shadow")},_createIcon:function(a){var b=this[a+"Size"],c=this[a+"Url"];if(!c&&a==="shadow")return null;var d;return c?d=this._createImg(c):d=this._createDiv(),d.className="leaflet-marker-"+a,d.style.marginLeft=-this.iconAnchor.x+"px",d.style.marginTop=-this.iconAnchor.y+"px",b&&(d.style.width=b.x+"px",d.style.height=b.y+"px"),d},_createImg:function(a){var b;return L.Browser.ie6?(b=document.createElement("div"),b.style.filter='progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+a+'")'):(b=document.createElement("img"),b.src=a),b},_createDiv:function(){return document.createElement("div")}}),L.Marker=L.Class.extend({includes:L.Mixin.Events,options:{icon:new L.Icon,title:"",clickable:!0,draggable:!1,zIndexOffset:0},initialize:function(a,b){L.Util.setOptions(this,b),this._latlng=a},onAdd:function(a){this._map=a,this._initIcon(),a.on("viewreset",this._reset,this),this._reset()},onRemove:function(a){this._removeIcon(),this.closePopup&&this.closePopup(),this._map=null,a.off("viewreset",this._reset,this)},getLatLng:function(){return this._latlng},setLatLng:function(a){this._latlng=a,this._icon&&(this._reset(),this._popup&&this._popup.setLatLng(this._latlng))},setZIndexOffset:function(a){this.options.zIndexOffset=a,this._icon&&this._reset()},setIcon:function(a){this._map&&this._removeIcon(),this.options.icon=a,this._map&&(this._initIcon(),this._reset())},_initIcon:function(){this._icon||(this._icon=this.options.icon.createIcon(),this.options.title&&(this._icon.title=this.options.title),this._initInteraction()),this._shadow||(this._shadow=this.options.icon.createShadow()),this._map._panes.markerPane.appendChild(this._icon),this._shadow&&this._map._panes.shadowPane.appendChild(this._shadow)},_removeIcon:function(){this._map._panes.markerPane.removeChild(this._icon),this._shadow&&this._map._panes.shadowPane.removeChild(this._shadow),this._icon=this._shadow=null},_reset:function(){var a=this._map.latLngToLayerPoint(this._latlng).round();L.DomUtil.setPosition(this._icon,a),this._shadow&&L.DomUtil.setPosition(this._shadow,a),this._icon.style.zIndex=a.y+this.options.zIndexOffset},_initInteraction:function(){if(this.options.clickable){this._icon.className+=" leaflet-clickable",L.DomEvent.addListener(this._icon,"click",this._onMouseClick,this);var a=["dblclick","mousedown","mouseover","mouseout"];for(var b=0;bthis.options.maxWidth?this.options.maxWidth:af.x&&(d.x=c.x+this._containerWidth-f.x+e.x),c.y<0&&(d.y=c.y-e.y),c.y+a>f.y&&(d.y=c.y+a-f.y+e.y),(d.x||d.y)&&this._map.panBy(d)},_onCloseButtonClick:function(a){this._close(),L.DomEvent.stop(a)}}),L.Marker.include({openPopup:function(){return this._popup.setLatLng(this._latlng),this._map&&this._map.openPopup(this._popup),this},closePopup:function(){return this._popup&&this._popup._close(),this},bindPopup:function(a,b){return b=L.Util.extend({offset:this.options.icon.popupAnchor},b),this._popup||this.on("click",this.openPopup,this),this._popup=new L.Popup(b,this),this._popup.setContent(a),this},unbindPopup:function(){return this._popup&&(this._popup=null,this.off("click",this.openPopup)),this}}),L.Map.include({openPopup:function(a){return this.closePopup(),this._popup=a,this.addLayer(a),this.fire("popupopen",{popup:this._popup}),this},closePopup:function(){return this._popup&&(this.removeLayer(this._popup),this.fire("popupclose",{popup:this._popup}),this._popup=null),this}}),L.LayerGroup=L.Class.extend({initialize:function(a){this._layers={};if(a)for(var b=0,c=a.length;b')}}catch(a){return function(a){return document.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}()},_initPath:function(){this._container=L.Path._createElement("shape"),this._container.className+=" leaflet-vml-shape"+(this.options.clickable?" leaflet-clickable":""),this._container.coordsize="1 1",this._path=L.Path._createElement("path"),this._container.appendChild(this._path),this._map._pathRoot.appendChild(this._container)},_initStyle:function(){this.options.stroke?(this._stroke=L.Path._createElement("stroke"),this._stroke.endcap="round",this._container.appendChild(this._stroke)):this._container.stroked=!1,this.options.fill?(this._container.filled=!0,this._fill=L.Path._createElement("fill"),this._container.appendChild(this._fill)):this._container.filled=!1,this._updateStyle()},_updateStyle:function(){this.options.stroke&&(this._stroke.weight=this.options.weight+"px",this._stroke.color=this.options.color,this._stroke.opacity=this.options.opacity),this.options.fill&&(this._fill.color=this.options.fillColor||this.options.color,this._fill.opacity=this.options.fillOpacity)},_updatePath:function(){this._container.style.display="none",this._path.v=this.getPathString()+" ",this._container.style.display=""}}),L.Map.include(L.Browser.svg||!L.Browser.vml?{}:{_initPathRoot:function(){this._pathRoot||(this._pathRoot=document.createElement("div"),this._pathRoot.className="leaflet-vml-container",this._panes.overlayPane.appendChild(this._pathRoot),this.on("moveend",this._updatePathViewport),this._updatePathViewport())}}),L.Browser.canvas=function(){return!!document.createElement("canvas").getContext}(),L.Path=L.Path.SVG&&!window.L_PREFER_CANVAS||!L.Browser.canvas?L.Path:L.Path.extend({statics:{CANVAS:!0,SVG:!1},options:{updateOnMoveEnd:!0},_initElements:function(){this._map._initPathRoot(),this._ctx=this._map._canvasCtx},_updateStyle:function(){this.options.stroke&&(this._ctx.lineWidth=this.options.weight,this._ctx.strokeStyle=this.options.color),this.options.fill&&(this._ctx.fillStyle=this.options.fillColor||this.options.color)},_drawPath:function(){var a,b,c,d,e,f;this._ctx.beginPath();for(a=0,c=this._parts.length;af&&(g=h,f=i);f>c&&(b[g]=1,this._simplifyDPStep(a,b,c,d,g),this._simplifyDPStep(a,b,c,g,e))},_reducePoints:function(a,b){var c=[a[0]];for(var d=1,e=0,f=a.length;db&&(c.push(a[d]),e=d);return eb.max.x&&(c|=2),a.yb.max.y&&(c|=8),c},_sqDist:function(a,b){var c=b.x-a.x,d=b.y-a.y;return c*c+d*d},_sqClosestPointOnSegment:function(a,b,c,d){var e=b.x,f=b.y,g=c.x-e,h=c.y-f,i=g*g+h*h,j;return i>0&&(j=((a.x-e)*g+(a.y-f)*h)/i,j>1?(e=c.x,f=c.y):j>0&&(e+=g*j,f+=h*j)),g=a.x-e,h=a.y-f,d?g*g+h*h:new L.Point(e,f)}},L.Polyline=L.Path.extend({initialize:function(a,b){L.Path.prototype.initialize.call(this,b),this._latlngs=a},options:{smoothFactor:1,noClip:!1,updateOnMoveEnd:!0},projectLatlngs:function(){this._originalPoints=[];for(var a=0,b=this._latlngs.length;aa.max.x||c.y-b>a.max.y||c.x+ba.y!=e.y>a.y&&a.x<(e.x-d.x)*(a.y-d.y)/(e.y-d.y)+d.x&&(b=!b)}return b}}:{}),L.Circle.include(L.Path.CANVAS?{_drawPath:function(){var a=this._point;this._ctx.beginPath(),this._ctx.arc(a.x,a.y,this._radius,0,Math.PI*2)},_containsPoint:function(a){var b=this._point,c=this.options.stroke?this.options.weight/2:0;return a.distanceTo(b)<=this._radius+c}}:{}),L.GeoJSON=L.FeatureGroup.extend({initialize:function(a,b){L.Util.setOptions(this,b),this._geojson=a,this._layers={},a&&this.addGeoJSON(a)},addGeoJSON:function(a){if(a.features){for(var b=0,c=a.features.length;b1)return;var b=a.touches&&a.touches.length===1?a.touches[0]:a,c=b.target;L.DomEvent.preventDefault(a),L.Browser.touch&&c.tagName.toLowerCase()==="a"&&(c.className+=" leaflet-active"),this._moved=!1;if(this._moving)return;L.Browser.touch||(L.DomUtil.disableTextSelection(),this._setMovingCursor()),this._startPos=this._newPos=L.DomUtil.getPosition(this._element),this._startPoint=new L.Point(b.clientX,b.clientY),L.DomEvent.addListener(document,L.Draggable.MOVE,this._onMove,this),L.DomEvent.addListener(document,L.Draggable.END,this._onUp,this)},_onMove:function(a){if(a.touches&&a.touches.length>1)return;L.DomEvent.preventDefault(a);var b=a.touches&&a.touches.length===1?a.touches[0]:a;this._moved||(this.fire("dragstart"),this._moved=!0),this._moving=!0;var c=new L.Point(b.clientX,b.clientY);this._newPos=this._startPos.add(c).subtract(this._startPoint),L.Util.requestAnimFrame(this._updatePosition,this,!0,this._dragStartTarget)},_updatePosition:function(){this.fire("predrag"),L.DomUtil.setPosition(this._element,this._newPos),this.fire("drag")},_onUp:function(a){if(a.changedTouches){var b=a.changedTouches[0],c=b.target,d=this._newPos&&this._newPos.distanceTo(this._startPos)||0;c.tagName.toLowerCase()==="a"&&(c.className=c.className.replace(" leaflet-active","")),d0&&c<=f,d=b}function l(a){e&&(g.type="dblclick",b(g),d=null)}var d,e=!1,f=250,g,h="_leaflet_",i="touchstart",j="touchend";a[h+i+c]=k,a[h+j+c]=l,a.addEventListener(i,k,!1),a.addEventListener(j,l,!1)},removeDoubleTapListener:function(a,b){var c="_leaflet_";a.removeEventListener(a,a[c+"touchstart"+b],!1),a.removeEventListener(a,a[c+"touchend"+b],!1)}}),L.Map.TouchZoom=L.Handler.extend({addHooks:function(){L.DomEvent.addListener(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){L.DomEvent.removeListener(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(a){if(!a.touches||a.touches.length!==2||this._map._animatingZoom)return;var b=this._map.mouseEventToLayerPoint(a.touches[0]),c=this._map.mouseEventToLayerPoint(a.touches[1]),d=this._map.containerPointToLayerPoint(this._map.getSize().divideBy(2));this._startCenter=b.add(c).divideBy(2,!0),this._startDist=b.distanceTo(c),this._moved=!1,this._zooming=!0,this._centerOffset=d.subtract(this._startCenter),L.DomEvent.addListener(document,"touchmove",this._onTouchMove,this),L.DomEvent.addListener(document,"touchend",this._onTouchEnd,this),L.DomEvent.preventDefault(a)},_onTouchMove:function(a){if(!a.touches||a.touches.length!==2)return;this._moved||(this._map._mapPane.className+=" leaflet-zoom-anim",this._map.fire("zoomstart").fire("movestart")._prepareTileBg(),this._moved=!0);var b=this._map.mouseEventToLayerPoint(a.touches[0]),c=this._map.mouseEventToLayerPoint(a.touches[1]);this._scale=b.distanceTo(c)/this._startDist,this._delta=b.add(c).divideBy(2,!0).subtract(this._startCenter),this._map._tileBg.style.webkitTransform=[L.DomUtil.getTranslateString(this._delta),L.DomUtil.getScaleString(this._scale,this._startCenter)].join(" "),L.DomEvent.preventDefault(a)},_onTouchEnd:function(a){if(!this._moved||!this._zooming)return;this._zooming=!1;var b=this._map.getZoom(),c=Math.log(this._scale)/Math.LN2,d=c>0?Math.ceil(c):Math.floor(c),e=this._map._limitZoom(b+d),f=e-b,g=this._centerOffset.subtract(this._delta).divideBy(this._scale),h=this._map.getPixelOrigin().add(this._startCenter).add(g),i=this._map.unproject(h);L.DomEvent.removeListener(document,"touchmove",this._onTouchMove),L.DomEvent.removeListener(document,"touchend",this._onTouchEnd);var j=Math.pow(2,f);this._map._runAnimation(i,e,j/this._scale,this._startCenter.add(g))}}),L.Map.BoxZoom=L.Handler.extend({initialize:function(a){this._map=a,this._container=a._container,this._pane=a._panes.overlayPane},addHooks:function(){L.DomEvent.addListener(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){L.DomEvent.removeListener(this._container,"mousedown",this._onMouseDown)},_onMouseDown:function(a){if(!a.shiftKey||a.which!==1&&a.button!==1)return!1;L.DomUtil.disableTextSelection(),this._startLayerPoint=this._map.mouseEventToLayerPoint(a),this._box=L.DomUtil.create("div","leaflet-zoom-box",this._pane),L.DomUtil.setPosition(this._box,this._startLayerPoint),this._container.style.cursor="crosshair",L.DomEvent.addListener(document,"mousemove",this._onMouseMove,this),L.DomEvent.addListener(document,"mouseup",this._onMouseUp,this),L.DomEvent.preventDefault(a)},_onMouseMove:function(a){var b=this._map.mouseEventToLayerPoint(a),c=b.x-this._startLayerPoint.x,d=b.y-this._startLayerPoint.y,e=Math.min(b.x,this._startLayerPoint.x),f=Math.min(b.y,this._startLayerPoint.y),g=new L.Point(e,f);L.DomUtil.setPosition(this._box,g),this._box.style.width=Math.abs(c)-4+"px",this._box.style.height=Math.abs(d)-4+"px"},_onMouseUp:function(a){this._pane.removeChild(this._box),this._container.style.cursor="",L.DomUtil.enableTextSelection(),L.DomEvent.removeListener(document,"mousemove",this._onMouseMove),L.DomEvent.removeListener(document,"mouseup",this._onMouseUp);var b=this._map.mouseEventToLayerPoint(a),c=new L.LatLngBounds(this._map.layerPointToLatLng(this._startLayerPoint),this._map.layerPointToLatLng(b));this._map.fitBounds(c)}}),L.Handler.MarkerDrag=L.Handler.extend({initialize:function(a){this._marker=a},addHooks:function(){var a=this._marker._icon;this._draggable||(this._draggable=new L.Draggable(a,a),this._draggable.on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this)),this._draggable.enable()},removeHooks:function(){this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(a){this._marker.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(a){var b=L.DomUtil.getPosition(this._marker._icon);this._marker._shadow&&L.DomUtil.setPosition(this._marker._shadow,b),this._marker._latlng=this._marker._map.layerPointToLatLng(b),this._marker.fire("move").fire("drag")},_onDragEnd:function(){this._marker.fire("moveend").fire("dragend")}}),L.Control={},L.Control.Position={TOP_LEFT:"topLeft",TOP_RIGHT:"topRight",BOTTOM_LEFT:"bottomLeft",BOTTOM_RIGHT:"bottomRight"},L.Map.include({addControl:function(a){a.onAdd(this);var b=a.getPosition(),c=this._controlCorners[b],d=a.getContainer();return L.DomUtil.addClass(d,"leaflet-control"),b.indexOf("bottom")!==-1?c.insertBefore(d,c.firstChild):c.appendChild(d),this},removeControl:function(a){var b=a.getPosition(),c=this._controlCorners[b],d=a.getContainer();return c.removeChild(d),a.onRemove&&a.onRemove(this),this},_initControlPos:function(){var a=this._controlCorners={},b="leaflet-",c=b+"top",d=b+"bottom",e=b+"left",f=b+"right",g=L.DomUtil.create("div",b+"control-container",this._container);L.Browser.touch&&(g.className+=" "+b+"big-buttons"),a.topLeft=L.DomUtil.create("div",c+" "+e,g),a.topRight=L.DomUtil.create("div",c+" "+f,g),a.bottomLeft=L.DomUtil.create("div",d+" "+e,g),a.bottomRight=L.DomUtil.create("div",d+" "+f,g)}}),L.Control.Zoom=L.Class.extend({onAdd:function(a){this._map=a,this._container=L.DomUtil.create("div","leaflet-control-zoom"),this._zoomInButton=this._createButton("Zoom in","leaflet-control-zoom-in",this._map.zoomIn,this._map),this._zoomOutButton=this._createButton("Zoom out","leaflet-control-zoom-out",this._map.zoomOut,this._map),this._container.appendChild(this._zoomInButton),this._container.appendChild(this._zoomOutButton)},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_LEFT},_createButton:function(a,b,c,d){var e=document.createElement("a");return e.href="#",e.title=a,e.className=b,L.Browser.touch||L.DomEvent.disableClickPropagation(e),L.DomEvent.addListener(e,"click",L.DomEvent.preventDefault),L.DomEvent.addListener(e,"click",c,d),e}}),L.Control.Attribution=L.Class.extend({initialize:function(a){this._prefix=a||'Powered by Leaflet',this._attributions={}},onAdd:function(a){this._container=L.DomUtil.create("div","leaflet-control-attribution"),L.DomEvent.disableClickPropagation(this._container),this._map=a,this._update()},getPosition:function(){return L.Control.Position.BOTTOM_RIGHT},getContainer:function(){return this._container},setPrefix:function(a){this._prefix=a,this._update()},addAttribution:function(a){if(!a)return;this._attributions[a]||(this._attributions[a]=0),this._attributions[a]++,this._update()},removeAttribution:function(a){if(!a)return;this._attributions[a]--,this._update()},_update:function(){if(!this._map)return;var a=[];for(var b in this._attributions)this._attributions.hasOwnProperty(b)&&a.push(b);var c=[];this._prefix&&c.push(this._prefix),a.length&&c.push(a.join(", ")),this._container.innerHTML=c.join(" — ")}}),L.Control.Layers=L.Class.extend({options:{collapsed:!0},initialize:function(a,b,c){L.Util.setOptions(this,c),this._layers={};for(var d in a)a.hasOwnProperty(d)&&this._addLayer(a[d],d);for(d in b)b.hasOwnProperty(d)&&this._addLayer(b[d],d,!0)},onAdd:function(a){this._map=a,this._initLayout(),this._update()},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_RIGHT},addBaseLayer:function(a,b){return this._addLayer(a,b),this._update(),this},addOverlay:function(a,b){return this._addLayer(a,b,!0),this._update(),this},removeLayer:function(a){var b=L.Util.stamp(a);return delete this._layers[b],this._update(),this},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-control-layers"),L.Browser.touch||L.DomEvent.disableClickPropagation(this._container),this._form=L.DomUtil.create("form","leaflet-control-layers-list");if(this.options.collapsed){L.DomEvent.addListener(this._container,"mouseover",this._expand,this),L.DomEvent.addListener(this._container,"mouseout",this._collapse,this);var a=this._layersLink=L.DomUtil.create("a","leaflet-control-layers-toggle");a.href="#",a.title="Layers",L.Browser.touch?L.DomEvent.addListener(a,"click",this._expand,this):L.DomEvent.addListener(a,"focus",this._expand,this),this._map.on("movestart",this._collapse,this),this._container.appendChild(a)}else this._expand();this._baseLayersList=L.DomUtil.create("div","leaflet-control-layers-base",this._form),this._separator=L.DomUtil.create("div","leaflet-control-layers-separator",this._form),this._overlaysList=L.DomUtil.create("div","leaflet-control-layers-overlays",this._form),this._container.appendChild(this._form)},_addLayer:function(a,b,c){var d=L.Util.stamp(a);this._layers[d]={layer:a,name:b,overlay:c}},_update:function(){if(!this._container)return;this._baseLayersList.innerHTML="",this._overlaysList.innerHTML="";var a=!1,b=!1;for(var c in this._layers)if(this._layers.hasOwnProperty(c)){var d=this._layers[c];this._addItem(d),b=b||d.overlay,a=a||!d.overlay}this._separator.style.display=b&&a?"":"none"},_addItem:function(a,b){var c=document.createElement("label"),d=document.createElement("input");a.overlay||(d.name="leaflet-base-layers"),d.type=a.overlay?"checkbox":"radio",d.checked=this._map.hasLayer(a.layer),d.layerId=L.Util.stamp(a.layer),L.DomEvent.addListener(d,"click",this._onInputClick,this);var e=document.createTextNode(" "+a.name);c.appendChild(d),c.appendChild(e);var f=a.overlay?this._overlaysList:this._baseLayersList;f.appendChild(c)},_onInputClick:function(){var a,b,c,d=this._form.getElementsByTagName("input"),e=d.length;for(a=0;a