Merge master into 309-expand-content-when-sidebar-empty
This commit is contained in:
@@ -27,6 +27,9 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
|
||||
|
||||
my.__type__ = 'ckan';
|
||||
|
||||
// private - use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// Default CKAN API endpoint used for requests (you can change this but it will affect every request!)
|
||||
//
|
||||
// DEPRECATION: this will be removed in v0.7. Please set endpoint attribute on dataset instead
|
||||
@@ -41,7 +44,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
|
||||
dataset.id = out.resource_id;
|
||||
var wrapper = my.DataStore(out.endpoint);
|
||||
}
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
|
||||
jqxhr.done(function(results) {
|
||||
// map ckan types to our usual types ...
|
||||
@@ -84,7 +87,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
|
||||
var wrapper = my.DataStore(out.endpoint);
|
||||
}
|
||||
var actualQuery = my._normalizeQuery(queryObj, dataset);
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var jqxhr = wrapper.search(actualQuery);
|
||||
jqxhr.done(function(results) {
|
||||
var out = {
|
||||
|
||||
@@ -5,6 +5,9 @@ this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
|
||||
(function($, my) {
|
||||
my.__type__ = 'couchdb';
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## CouchDB Wrapper
|
||||
//
|
||||
// Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints.
|
||||
@@ -197,7 +200,7 @@ my.__type__ = 'couchdb';
|
||||
var db_url = dataset.db_url;
|
||||
var view_url = dataset.view_url;
|
||||
var cdb = new my.CouchDBWrapper(db_url, view_url);
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
|
||||
// if 'doc' attribute is present, return schema of that
|
||||
// else return schema of 'value' attribute which contains
|
||||
@@ -239,7 +242,7 @@ my.__type__ = 'couchdb';
|
||||
//
|
||||
//
|
||||
my.save = function (changes, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var total = changes.creates.length + changes.updates.length + changes.deletes.length;
|
||||
var results = {'done': [], 'fail': [] };
|
||||
|
||||
@@ -280,7 +283,7 @@ my.save = function (changes, dataset) {
|
||||
// @param {Object} recline.Dataset instance
|
||||
// @param {Object} recline.Query instance.
|
||||
my.query = function(queryObj, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var db_url = dataset.db_url;
|
||||
var view_url = dataset.view_url;
|
||||
var query_options = dataset.query_options;
|
||||
@@ -475,7 +478,7 @@ function randomId(length, chars) {
|
||||
}
|
||||
|
||||
_createDocument = function (new_doc, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var db_url = dataset.db_url;
|
||||
var view_url = dataset.view_url;
|
||||
var _id = new_doc['id'];
|
||||
@@ -497,7 +500,7 @@ _createDocument = function (new_doc, dataset) {
|
||||
};
|
||||
|
||||
_updateDocument = function (new_doc, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var db_url = dataset.db_url;
|
||||
var view_url = dataset.view_url;
|
||||
var _id = new_doc['id'];
|
||||
@@ -527,7 +530,7 @@ _updateDocument = function (new_doc, dataset) {
|
||||
};
|
||||
|
||||
_deleteDocument = function (del_doc, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var db_url = dataset.db_url;
|
||||
var view_url = dataset.view_url;
|
||||
var _id = del_doc['id'];
|
||||
|
||||
@@ -4,6 +4,10 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
||||
|
||||
// Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file)
|
||||
(function(my) {
|
||||
my.__type__ = 'csv';
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## fetch
|
||||
//
|
||||
@@ -23,7 +27,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
||||
// }
|
||||
// </pre>
|
||||
my.fetch = function(dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
if (dataset.file) {
|
||||
var reader = new FileReader();
|
||||
var encoding = dataset.encoding || 'UTF-8';
|
||||
|
||||
@@ -10,6 +10,10 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
// Needed because use JSONP so do not receive e.g. 500 errors
|
||||
my.timeout = 5000;
|
||||
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## load
|
||||
//
|
||||
// Load data from a URL via the [DataProxy](http://github.com/okfn/dataproxy).
|
||||
@@ -26,7 +30,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
data: data,
|
||||
dataType: 'jsonp'
|
||||
});
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
_wrapInTimeout(jqxhr).done(function(results) {
|
||||
if (results.error) {
|
||||
dfd.reject(results.error);
|
||||
@@ -50,7 +54,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
// Many of backends use JSONP and so will not get error messages and this is
|
||||
// a crude way to catch those errors.
|
||||
var _wrapInTimeout = function(ourFunction) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var timer = setTimeout(function() {
|
||||
dfd.reject({
|
||||
message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
|
||||
|
||||
@@ -5,6 +5,9 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
(function($, my) {
|
||||
my.__type__ = 'elasticsearch';
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## ElasticSearch Wrapper
|
||||
//
|
||||
// A simple JS wrapper around an [ElasticSearch](http://www.elasticsearch.org/) endpoints.
|
||||
@@ -179,7 +182,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
// ### fetch
|
||||
my.fetch = function(dataset) {
|
||||
var es = new my.Wrapper(dataset.url, my.esOptions);
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
es.mapping().done(function(schema) {
|
||||
|
||||
if (!schema){
|
||||
@@ -207,7 +210,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
my.save = function(changes, dataset) {
|
||||
var es = new my.Wrapper(dataset.url, my.esOptions);
|
||||
if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
msg = 'Saving more than one item at a time not yet supported';
|
||||
alert(msg);
|
||||
dfd.reject(msg);
|
||||
@@ -225,7 +228,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
|
||||
// ### query
|
||||
my.query = function(queryObj, dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var es = new my.Wrapper(dataset.url, my.esOptions);
|
||||
var jqxhr = es.query(queryObj);
|
||||
jqxhr.done(function(results) {
|
||||
|
||||
@@ -5,6 +5,9 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
|
||||
(function(my) {
|
||||
my.__type__ = 'gdocs';
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## Google spreadsheet backend
|
||||
//
|
||||
// Fetch data from a Google Docs spreadsheet.
|
||||
@@ -29,13 +32,13 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
|
||||
// * fields: array of Field objects
|
||||
// * records: array of objects for each row
|
||||
my.fetch = function(dataset) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var urls = my.getGDocsAPIUrls(dataset.url);
|
||||
|
||||
// TODO cover it with tests
|
||||
// get the spreadsheet title
|
||||
(function () {
|
||||
var titleDfd = new _.Deferred();
|
||||
var titleDfd = new Deferred();
|
||||
|
||||
jQuery.getJSON(urls.spreadsheet, function (d) {
|
||||
titleDfd.resolve({
|
||||
|
||||
@@ -5,6 +5,9 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
(function(my) {
|
||||
my.__type__ = 'memory';
|
||||
|
||||
// private data - use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## Data Wrapper
|
||||
//
|
||||
// Turn a simple array of JS objects into a mini data-store with
|
||||
@@ -48,7 +51,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
|
||||
this.save = function(changes, dataset) {
|
||||
var self = this;
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
// TODO _.each(changes.creates) { ... }
|
||||
_.each(changes.updates, function(record) {
|
||||
self.update(record);
|
||||
@@ -61,7 +64,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
},
|
||||
|
||||
this.query = function(queryObj) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
var numRows = queryObj.size || this.records.length;
|
||||
var start = queryObj.from || 0;
|
||||
var results = this.records;
|
||||
@@ -229,7 +232,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
};
|
||||
|
||||
this.transform = function(editFunc) {
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
// TODO: should we clone before mapping? Do not see the point atm.
|
||||
self.records = _.map(self.records, editFunc);
|
||||
// now deal with deletes (i.e. nulls)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Backend = this.recline.Backend || {};
|
||||
this.recline.Backend.Solr = this.recline.Backend.Solr || {};
|
||||
|
||||
(function($, my) {
|
||||
my.__type__ = 'solr';
|
||||
|
||||
// ### fetch
|
||||
//
|
||||
// dataset must have a solr or url attribute pointing to solr endpoint
|
||||
my.fetch = function(dataset) {
|
||||
var jqxhr = $.ajax({
|
||||
url: dataset.solr || dataset.url,
|
||||
data: {
|
||||
rows: 1,
|
||||
wt: 'json'
|
||||
},
|
||||
dataType: 'jsonp',
|
||||
jsonp: 'json.wrf'
|
||||
});
|
||||
var dfd = new _.Deferred();
|
||||
jqxhr.done(function(results) {
|
||||
// if we get 0 results we cannot get fields
|
||||
var fields = []
|
||||
if (results.response.numFound > 0) {
|
||||
fields = _.map(_.keys(results.response.docs[0]), function(fieldName) {
|
||||
return { id: fieldName };
|
||||
});
|
||||
}
|
||||
var out = {
|
||||
fields: fields,
|
||||
useMemoryStore: false
|
||||
};
|
||||
dfd.resolve(out);
|
||||
});
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
// TODO - much work on proper query support is needed!!
|
||||
my.query = function(queryObj, dataset) {
|
||||
var q = queryObj.q || '*:*';
|
||||
var data = {
|
||||
q: q,
|
||||
rows: queryObj.size,
|
||||
start: queryObj.from,
|
||||
wt: 'json'
|
||||
};
|
||||
var jqxhr = $.ajax({
|
||||
url: dataset.solr || dataset.url,
|
||||
data: data,
|
||||
dataType: 'jsonp',
|
||||
jsonp: 'json.wrf'
|
||||
});
|
||||
var dfd = new _.Deferred();
|
||||
jqxhr.done(function(results) {
|
||||
var out = {
|
||||
total: results.response.numFound,
|
||||
hits: results.response.docs
|
||||
};
|
||||
dfd.resolve(out);
|
||||
});
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
}(jQuery, this.recline.Backend.Solr));
|
||||
@@ -4,6 +4,9 @@ this.recline.Model = this.recline.Model || {};
|
||||
|
||||
(function(my) {
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
|
||||
|
||||
// ## <a id="dataset">Dataset</a>
|
||||
my.Dataset = Backbone.Model.extend({
|
||||
constructor: function Dataset() {
|
||||
@@ -47,7 +50,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
// Retrieve dataset and (some) records from the backend.
|
||||
fetch: function() {
|
||||
var self = this;
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
|
||||
if (this.backend !== recline.Backend.Memory) {
|
||||
this.backend.fetch(this.toJSON())
|
||||
@@ -181,7 +184,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
// also returned.
|
||||
query: function(queryObj) {
|
||||
var self = this;
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
this.trigger('query:start');
|
||||
|
||||
if (queryObj) {
|
||||
@@ -245,7 +248,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
this.fields.each(function(field) {
|
||||
query.addFacet(field.id);
|
||||
});
|
||||
var dfd = new _.Deferred();
|
||||
var dfd = new Deferred();
|
||||
this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
|
||||
if (queryResult.facets) {
|
||||
_.each(queryResult.facets, function(facetResult, facetId) {
|
||||
|
||||
@@ -12,17 +12,18 @@ this.recline.View = this.recline.View || {};
|
||||
// * model: recline.Model.Dataset
|
||||
// * state: (optional) configuration hash of form:
|
||||
//
|
||||
// {
|
||||
// {
|
||||
// group: {column name for x-axis},
|
||||
// series: [{column name for series A}, {column name series B}, ... ],
|
||||
// graphType: 'line'
|
||||
// graphType: 'line',
|
||||
// graphOptions: {custom [flot options]}
|
||||
// }
|
||||
//
|
||||
// NB: should *not* provide an el argument to the view but must let the view
|
||||
// generate the element itself (you can then append view.el to the DOM.
|
||||
my.Flot = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-graph"> \
|
||||
<div class="recline-flot"> \
|
||||
<div class="panel graph" style="display: block;"> \
|
||||
<div class="js-temp-notice alert alert-block"> \
|
||||
<h3 class="alert-heading">Hey there!</h3> \
|
||||
@@ -80,7 +81,7 @@ my.Flot = Backbone.View.extend({
|
||||
// There are issues generating a Flot graph if either:
|
||||
// * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
|
||||
// Uncaught Invalid dimensions for plot, width = 0, height = 0
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
|
||||
if ((!areWeVisible || this.model.records.length === 0)) {
|
||||
this.needToRedraw = true;
|
||||
@@ -89,9 +90,6 @@ my.Flot = Backbone.View.extend({
|
||||
|
||||
// check we have something to plot
|
||||
if (this.state.get('group') && this.state.get('series')) {
|
||||
// faff around with width because flot draws axes *outside* of the element
|
||||
// width which means graph can get push down as it hits element next to it
|
||||
this.$graph.width(this.el.width() - 240);
|
||||
var series = this.createSeries();
|
||||
var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
|
||||
this.plot = $.plot(this.$graph, series, options);
|
||||
@@ -112,7 +110,7 @@ my.Flot = Backbone.View.extend({
|
||||
this.previousTooltipPoint.y !== item.seriesIndex) {
|
||||
this.previousTooltipPoint.x = item.dataIndex;
|
||||
this.previousTooltipPoint.y = item.seriesIndex;
|
||||
$("#recline-graph-tooltip").remove();
|
||||
$("#recline-flot-tooltip").remove();
|
||||
|
||||
var x = item.datapoint[0].toFixed(2),
|
||||
y = item.datapoint[1].toFixed(2);
|
||||
@@ -134,13 +132,13 @@ my.Flot = Backbone.View.extend({
|
||||
yLocation = item.pageY - 20;
|
||||
}
|
||||
|
||||
$('<div id="recline-graph-tooltip">' + content + '</div>').css({
|
||||
$('<div id="recline-flot-tooltip">' + content + '</div>').css({
|
||||
top: yLocation,
|
||||
left: xLocation
|
||||
}).appendTo("body").fadeIn(200);
|
||||
}
|
||||
} else {
|
||||
$("#recline-graph-tooltip").remove();
|
||||
$("#recline-flot-tooltip").remove();
|
||||
this.previousTooltipPoint.x = null;
|
||||
this.previousTooltipPoint.y = null;
|
||||
}
|
||||
@@ -191,7 +189,7 @@ my.Flot = Backbone.View.extend({
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
|
||||
var xaxis = {};
|
||||
xaxis.tickFormatter = tickFormatter;
|
||||
|
||||
@@ -222,7 +220,7 @@ my.Flot = Backbone.View.extend({
|
||||
var yaxis = {};
|
||||
yaxis.autoscale = true;
|
||||
yaxis.autoscaleMargin = 0.02;
|
||||
|
||||
|
||||
var legend = {};
|
||||
legend.position = 'ne';
|
||||
|
||||
@@ -231,8 +229,8 @@ my.Flot = Backbone.View.extend({
|
||||
grid.clickable = true;
|
||||
grid.borderColor = "#aaaaaa";
|
||||
grid.borderWidth = 1;
|
||||
|
||||
var optionsPerGraphType = {
|
||||
|
||||
var optionsPerGraphType = {
|
||||
lines: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
@@ -259,22 +257,43 @@ my.Flot = Backbone.View.extend({
|
||||
grid: grid
|
||||
},
|
||||
bars: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: yaxis,
|
||||
yaxis: xaxis,
|
||||
grid: grid,
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: true,
|
||||
shadowSize: 0,
|
||||
align: 'center',
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
grid: grid,
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: false,
|
||||
shadowSize: 0,
|
||||
align: 'center',
|
||||
barWidth: 0.8
|
||||
},
|
||||
grid: grid
|
||||
}
|
||||
}
|
||||
};
|
||||
return optionsPerGraphType[typeId];
|
||||
|
||||
if (self.state.get('graphOptions')) {
|
||||
return _.extend(optionsPerGraphType[typeId],
|
||||
self.state.get('graphOptions'));
|
||||
} else {
|
||||
return optionsPerGraphType[typeId];
|
||||
}
|
||||
},
|
||||
|
||||
createSeries: function() {
|
||||
@@ -289,22 +308,29 @@ my.Flot = Backbone.View.extend({
|
||||
// time series
|
||||
var xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
|
||||
|
||||
if (isDateTime) {
|
||||
// datetime
|
||||
if (self.state.attributes.graphType != 'bars') {
|
||||
if (self.state.attributes.graphType != 'bars' &&
|
||||
self.state.attributes.graphType != 'columns') {
|
||||
x = new Date(x).getTime();
|
||||
} else {
|
||||
x = index;
|
||||
}
|
||||
} else if (typeof x === 'string') {
|
||||
x = index;
|
||||
x = parseFloat(x);
|
||||
if (isNaN(x)) {
|
||||
x = index;
|
||||
}
|
||||
}
|
||||
|
||||
var yfield = self.model.fields.get(field);
|
||||
var y = doc.getFieldValue(yfield);
|
||||
|
||||
points.push([x, y]);
|
||||
|
||||
if (self.state.attributes.graphType == 'bars') {
|
||||
points.push([y, x]);
|
||||
} else {
|
||||
points.push([x, y]);
|
||||
}
|
||||
});
|
||||
series.push({
|
||||
data: points,
|
||||
@@ -329,9 +355,10 @@ my.FlotControls = Backbone.View.extend({
|
||||
<option value="lines">Lines</option> \
|
||||
<option value="points">Points</option> \
|
||||
<option value="bars">Bars</option> \
|
||||
<option value="columns">Columns</option> \
|
||||
</select> \
|
||||
</div> \
|
||||
<label>X-Axis</label> \
|
||||
<label>Group Column (Axis 1)</label> \
|
||||
<div class="input editor-group"> \
|
||||
<select> \
|
||||
<option value="">Please choose ...</option> \
|
||||
@@ -344,7 +371,7 @@ my.FlotControls = Backbone.View.extend({
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-buttons"> \
|
||||
<button class="btn editor-add">Add Additional Y-Axis Plot</button> \
|
||||
<button class="btn editor-add">Add Series</button> \
|
||||
</div> \
|
||||
<div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
|
||||
<button class="editor-save">Save</button> \
|
||||
@@ -355,7 +382,7 @@ my.FlotControls = Backbone.View.extend({
|
||||
',
|
||||
templateSeriesEditor: ' \
|
||||
<div class="editor-series js-series-{{seriesIndex}}"> \
|
||||
<label>Y-Axis (<span>{{seriesName}})</span> \
|
||||
<label>Series <span>{{seriesName}} (Axis 2)</span> \
|
||||
[<a href="#remove" class="action-remove-series">Remove</a>] \
|
||||
</label> \
|
||||
<div class="input"> \
|
||||
@@ -470,4 +497,3 @@ my.FlotControls = Backbone.View.extend({
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
|
||||
462
src/view.flotr2.js
Normal file
462
src/view.flotr2.js
Normal file
@@ -0,0 +1,462 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
// ## Graph view for a Dataset using Flotr2 graphing library.
|
||||
//
|
||||
// Initialization arguments (in a hash in first parameter):
|
||||
//
|
||||
// * model: recline.Model.Dataset
|
||||
// * state: (optional) configuration hash of form:
|
||||
//
|
||||
// {
|
||||
// group: {column name for x-axis},
|
||||
// series: [{column name for series A}, {column name series B}, ... ],
|
||||
// graphType: 'line',
|
||||
// graphOptions: {custom [Flotr2 options](http://www.humblesoftware.com/flotr2/documentation#configuration)}
|
||||
// }
|
||||
//
|
||||
// NB: should *not* provide an el argument to the view but must let the view
|
||||
// generate the element itself (you can then append view.el to the DOM.
|
||||
my.Flotr2 = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-graph"> \
|
||||
<div class="panel graph" style="display: block;"> \
|
||||
<div class="js-temp-notice alert alert-block"> \
|
||||
<h3 class="alert-heading">Hey there!</h3> \
|
||||
<p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
|
||||
<p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
|
||||
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render', 'redraw');
|
||||
this.needToRedraw = false;
|
||||
this.model.bind('change', this.render);
|
||||
this.model.fields.bind('reset', this.render);
|
||||
this.model.fields.bind('add', this.render);
|
||||
this.model.records.bind('add', this.redraw);
|
||||
this.model.records.bind('reset', this.redraw);
|
||||
var stateData = _.extend({
|
||||
group: null,
|
||||
// so that at least one series chooser box shows up
|
||||
series: [],
|
||||
graphType: 'lines-and-points'
|
||||
},
|
||||
options.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
this.editor = new my.Flotr2Controls({
|
||||
model: this.model,
|
||||
state: this.state.toJSON()
|
||||
});
|
||||
this.editor.state.bind('change', function() {
|
||||
self.state.set(self.editor.state.toJSON());
|
||||
self.redraw();
|
||||
});
|
||||
this.elSidebar = this.editor.el;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
$(this.el).html(htmls);
|
||||
this.$graph = this.el.find('.panel.graph');
|
||||
return this;
|
||||
},
|
||||
|
||||
redraw: function() {
|
||||
// There appear to be issues generating a Flotr2 graph if either:
|
||||
|
||||
// * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flotr2 will complain with
|
||||
//
|
||||
// Uncaught Invalid dimensions for plot, width = 0, height = 0
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
|
||||
if ((!areWeVisible || this.model.records.length === 0)) {
|
||||
this.needToRedraw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// check we have something to plot
|
||||
if (this.state.get('group') && this.state.get('series')) {
|
||||
// faff around with width because flot draws axes *outside* of the element width which means graph can get push down as it hits element next to it
|
||||
this.$graph.width(this.el.width() - 20);
|
||||
var series = this.createSeries();
|
||||
var options = this.getGraphOptions(this.state.attributes.graphType);
|
||||
this.plot = Flotr.draw(this.$graph.get(0), series, options);
|
||||
}
|
||||
},
|
||||
|
||||
show: function() {
|
||||
// because we cannot redraw when hidden we may need to when becoming visible
|
||||
if (this.needToRedraw) {
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
|
||||
// ### getGraphOptions
|
||||
//
|
||||
// Get options for Flotr2 Graph
|
||||
//
|
||||
// needs to be function as can depend on state
|
||||
//
|
||||
// @param typeId graphType id (lines, lines-and-points etc)
|
||||
getGraphOptions: function(typeId) {
|
||||
var self = this;
|
||||
|
||||
var tickFormatter = function (x) {
|
||||
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 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 xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
|
||||
if (self.model.records.models[parseInt(x)]) {
|
||||
x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
|
||||
if (isDateTime) {
|
||||
x = new Date(x).toLocaleDateString();
|
||||
}
|
||||
} else if (isDateTime) {
|
||||
x = new Date(parseInt(x)).toLocaleDateString();
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
var xaxis = {};
|
||||
xaxis.tickFormatter = tickFormatter;
|
||||
|
||||
var yaxis = {};
|
||||
yaxis.autoscale = true;
|
||||
yaxis.autoscaleMargin = 0.02;
|
||||
|
||||
var mouse = {};
|
||||
mouse.track = true;
|
||||
mouse.relative = true;
|
||||
mouse.trackFormatter = trackFormatter;
|
||||
|
||||
var legend = {};
|
||||
legend.position = 'ne';
|
||||
|
||||
// mouse.lineColor is set in createSeries
|
||||
var optionsPerGraphType = {
|
||||
lines: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse
|
||||
},
|
||||
points: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse,
|
||||
grid: { hoverable: true, clickable: true }
|
||||
},
|
||||
'lines-and-points': {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse,
|
||||
grid: { hoverable: true, clickable: true }
|
||||
},
|
||||
bars: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: yaxis,
|
||||
yaxis: xaxis,
|
||||
mouse: {
|
||||
track: true,
|
||||
relative: true,
|
||||
trackFormatter: trackFormatter,
|
||||
fillColor: '#FFFFFF',
|
||||
fillOpacity: 0.3,
|
||||
position: 'e'
|
||||
},
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: true,
|
||||
shadowSize: 0,
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: {
|
||||
track: true,
|
||||
relative: true,
|
||||
trackFormatter: trackFormatter,
|
||||
fillColor: '#FFFFFF',
|
||||
fillOpacity: 0.3,
|
||||
position: 'n'
|
||||
},
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: false,
|
||||
shadowSize: 0,
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
grid: { hoverable: true, clickable: true }
|
||||
};
|
||||
|
||||
if (self.state.get('graphOptions')){
|
||||
return _.extend(optionsPerGraphType[typeId],
|
||||
self.state.get('graphOptions')
|
||||
)
|
||||
}else{
|
||||
return optionsPerGraphType[typeId];
|
||||
}
|
||||
},
|
||||
|
||||
createSeries: function() {
|
||||
var self = this;
|
||||
var series = [];
|
||||
_.each(this.state.attributes.series, function(field) {
|
||||
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 xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
|
||||
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);
|
||||
if (isNaN(x)) {
|
||||
x = index;
|
||||
}
|
||||
}
|
||||
|
||||
var yfield = self.model.fields.get(field);
|
||||
var y = doc.getFieldValue(yfield);
|
||||
|
||||
// horizontal bar chart
|
||||
if (self.state.attributes.graphType == 'bars') {
|
||||
points.push([y, x]);
|
||||
} else {
|
||||
points.push([x, y]);
|
||||
}
|
||||
});
|
||||
series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}});
|
||||
});
|
||||
return series;
|
||||
}
|
||||
});
|
||||
|
||||
my.Flotr2Controls = Backbone.View.extend({
|
||||
className: "editor",
|
||||
template: ' \
|
||||
<div class="editor"> \
|
||||
<form class="form-stacked"> \
|
||||
<div class="clearfix"> \
|
||||
<label>Graph Type</label> \
|
||||
<div class="input editor-type"> \
|
||||
<select> \
|
||||
<option value="lines-and-points">Lines and Points</option> \
|
||||
<option value="lines">Lines</option> \
|
||||
<option value="points">Points</option> \
|
||||
<option value="bars">Bars</option> \
|
||||
<option value="columns">Columns</option> \
|
||||
</select> \
|
||||
</div> \
|
||||
<label>Group Column (Axis 1)</label> \
|
||||
<div class="input editor-group"> \
|
||||
<select> \
|
||||
<option value="">Please choose ...</option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
<div class="editor-series-group"> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-buttons"> \
|
||||
<button class="btn editor-add">Add Series</button> \
|
||||
</div> \
|
||||
<div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
|
||||
<button class="editor-save">Save</button> \
|
||||
<input type="hidden" class="editor-id" value="chart-1" /> \
|
||||
</div> \
|
||||
</form> \
|
||||
</div> \
|
||||
',
|
||||
templateSeriesEditor: ' \
|
||||
<div class="editor-series js-series-{{seriesIndex}}"> \
|
||||
<label>Series <span>{{seriesName}} (Axis 2)</span> \
|
||||
[<a href="#remove" class="action-remove-series">Remove</a>] \
|
||||
</label> \
|
||||
<div class="input"> \
|
||||
<select> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'change form select': 'onEditorSubmit',
|
||||
'click .editor-add': '_onAddSeries',
|
||||
'click .action-remove-series': 'removeSeries'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.fields.bind('reset', this.render);
|
||||
this.model.fields.bind('add', this.render);
|
||||
this.state = new recline.Model.ObjectState(options.state);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
this.el.html(htmls);
|
||||
|
||||
// set up editor from state
|
||||
if (this.state.get('graphType')) {
|
||||
this._selectOption('.editor-type', this.state.get('graphType'));
|
||||
}
|
||||
if (this.state.get('group')) {
|
||||
this._selectOption('.editor-group', this.state.get('group'));
|
||||
}
|
||||
// ensure at least one series box shows up
|
||||
var tmpSeries = [""];
|
||||
if (this.state.get('series').length > 0) {
|
||||
tmpSeries = this.state.get('series');
|
||||
}
|
||||
_.each(tmpSeries, function(series, idx) {
|
||||
self.addSeries(idx);
|
||||
self._selectOption('.editor-series.js-series-' + idx, series);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// Private: Helper function to select an option from a select list
|
||||
//
|
||||
_selectOption: function(id,value){
|
||||
var options = this.el.find(id + ' select > option');
|
||||
if (options) {
|
||||
options.each(function(opt){
|
||||
if (this.value == value) {
|
||||
$(this).attr('selected','selected');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onEditorSubmit: function(e) {
|
||||
var select = this.el.find('.editor-group select');
|
||||
var $editor = this;
|
||||
var $series = this.el.find('.editor-series select');
|
||||
var series = $series.map(function () {
|
||||
return $(this).val();
|
||||
});
|
||||
var updatedState = {
|
||||
series: $.makeArray(series),
|
||||
group: this.el.find('.editor-group select').val(),
|
||||
graphType: this.el.find('.editor-type select').val()
|
||||
};
|
||||
this.state.set(updatedState);
|
||||
},
|
||||
|
||||
// Public: Adds a new empty series select box to the editor.
|
||||
//
|
||||
// @param [int] idx index of this series in the list of series
|
||||
//
|
||||
// Returns itself.
|
||||
addSeries: function (idx) {
|
||||
var data = _.extend({
|
||||
seriesIndex: idx,
|
||||
seriesName: String.fromCharCode(idx + 64 + 1)
|
||||
}, this.model.toTemplateJSON());
|
||||
|
||||
var htmls = Mustache.render(this.templateSeriesEditor, data);
|
||||
this.el.find('.editor-series-group').append(htmls);
|
||||
return this;
|
||||
},
|
||||
|
||||
_onAddSeries: function(e) {
|
||||
e.preventDefault();
|
||||
this.addSeries(this.state.get('series').length);
|
||||
},
|
||||
|
||||
// Public: Removes a series list item from the editor.
|
||||
//
|
||||
// Also updates the labels of the remaining series elements.
|
||||
removeSeries: function (e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
$el.parent().parent().remove();
|
||||
this.onEditorSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,462 +1,4 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
// ## Graph view for a Dataset using Flot graphing library.
|
||||
//
|
||||
// Initialization arguments (in a hash in first parameter):
|
||||
//
|
||||
// * model: recline.Model.Dataset
|
||||
// * state: (optional) configuration hash of form:
|
||||
//
|
||||
// {
|
||||
// group: {column name for x-axis},
|
||||
// series: [{column name for series A}, {column name series B}, ... ],
|
||||
// graphType: 'line',
|
||||
// graphOptions: {custom [Flotr2 options](http://www.humblesoftware.com/flotr2/documentation#configuration)}
|
||||
// }
|
||||
//
|
||||
// NB: should *not* provide an el argument to the view but must let the view
|
||||
// generate the element itself (you can then append view.el to the DOM.
|
||||
my.Graph = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-graph"> \
|
||||
<div class="panel graph" style="display: block;"> \
|
||||
<div class="js-temp-notice alert alert-block"> \
|
||||
<h3 class="alert-heading">Hey there!</h3> \
|
||||
<p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
|
||||
<p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
|
||||
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render', 'redraw');
|
||||
this.needToRedraw = false;
|
||||
this.model.bind('change', this.render);
|
||||
this.model.fields.bind('reset', this.render);
|
||||
this.model.fields.bind('add', this.render);
|
||||
this.model.records.bind('add', this.redraw);
|
||||
this.model.records.bind('reset', this.redraw);
|
||||
var stateData = _.extend({
|
||||
group: null,
|
||||
// so that at least one series chooser box shows up
|
||||
series: [],
|
||||
graphType: 'lines-and-points'
|
||||
},
|
||||
options.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
this.editor = new my.GraphControls({
|
||||
model: this.model,
|
||||
state: this.state.toJSON()
|
||||
});
|
||||
this.editor.state.bind('change', function() {
|
||||
self.state.set(self.editor.state.toJSON());
|
||||
self.redraw();
|
||||
});
|
||||
this.elSidebar = this.editor.el;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
$(this.el).html(htmls);
|
||||
this.$graph = this.el.find('.panel.graph');
|
||||
return this;
|
||||
},
|
||||
|
||||
redraw: function() {
|
||||
// There appear to be issues generating a Flot graph if either:
|
||||
|
||||
// * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
|
||||
//
|
||||
// Uncaught Invalid dimensions for plot, width = 0, height = 0
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
|
||||
if ((!areWeVisible || this.model.records.length === 0)) {
|
||||
this.needToRedraw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// check we have something to plot
|
||||
if (this.state.get('group') && this.state.get('series')) {
|
||||
// faff around with width because flot draws axes *outside* of the element width which means graph can get push down as it hits element next to it
|
||||
this.$graph.width(this.el.width() - 240);
|
||||
var series = this.createSeries();
|
||||
var options = this.getGraphOptions(this.state.attributes.graphType);
|
||||
this.plot = Flotr.draw(this.$graph.get(0), series, options);
|
||||
}
|
||||
},
|
||||
|
||||
show: function() {
|
||||
// because we cannot redraw when hidden we may need to when becoming visible
|
||||
if (this.needToRedraw) {
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
|
||||
// ### getGraphOptions
|
||||
//
|
||||
// Get options for Flot Graph
|
||||
//
|
||||
// needs to be function as can depend on state
|
||||
//
|
||||
// @param typeId graphType id (lines, lines-and-points etc)
|
||||
getGraphOptions: function(typeId) {
|
||||
var self = this;
|
||||
|
||||
var tickFormatter = function (x) {
|
||||
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 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 xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
|
||||
if (self.model.records.models[parseInt(x)]) {
|
||||
x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
|
||||
if (isDateTime) {
|
||||
x = new Date(x).toLocaleDateString();
|
||||
}
|
||||
} else if (isDateTime) {
|
||||
x = new Date(parseInt(x)).toLocaleDateString();
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
var xaxis = {};
|
||||
xaxis.tickFormatter = tickFormatter;
|
||||
|
||||
var yaxis = {};
|
||||
yaxis.autoscale = true;
|
||||
yaxis.autoscaleMargin = 0.02;
|
||||
|
||||
var mouse = {};
|
||||
mouse.track = true;
|
||||
mouse.relative = true;
|
||||
mouse.trackFormatter = trackFormatter;
|
||||
|
||||
var legend = {};
|
||||
legend.position = 'ne';
|
||||
|
||||
// mouse.lineColor is set in createSeries
|
||||
var optionsPerGraphType = {
|
||||
lines: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse
|
||||
},
|
||||
points: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse,
|
||||
grid: { hoverable: true, clickable: true }
|
||||
},
|
||||
'lines-and-points': {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: mouse,
|
||||
grid: { hoverable: true, clickable: true }
|
||||
},
|
||||
bars: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: yaxis,
|
||||
yaxis: xaxis,
|
||||
mouse: {
|
||||
track: true,
|
||||
relative: true,
|
||||
trackFormatter: trackFormatter,
|
||||
fillColor: '#FFFFFF',
|
||||
fillOpacity: 0.3,
|
||||
position: 'e'
|
||||
},
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: true,
|
||||
shadowSize: 0,
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
mouse: {
|
||||
track: true,
|
||||
relative: true,
|
||||
trackFormatter: trackFormatter,
|
||||
fillColor: '#FFFFFF',
|
||||
fillOpacity: 0.3,
|
||||
position: 'n'
|
||||
},
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: false,
|
||||
shadowSize: 0,
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
grid: { hoverable: true, clickable: true }
|
||||
};
|
||||
|
||||
if (self.state.get('graphOptions')){
|
||||
return _.extend(optionsPerGraphType[typeId],
|
||||
self.state.get('graphOptions')
|
||||
)
|
||||
}else{
|
||||
return optionsPerGraphType[typeId];
|
||||
}
|
||||
},
|
||||
|
||||
createSeries: function() {
|
||||
var self = this;
|
||||
var series = [];
|
||||
_.each(this.state.attributes.series, function(field) {
|
||||
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 xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
|
||||
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);
|
||||
if (isNaN(x)) {
|
||||
x = index;
|
||||
}
|
||||
}
|
||||
|
||||
var yfield = self.model.fields.get(field);
|
||||
var y = doc.getFieldValue(yfield);
|
||||
|
||||
// horizontal bar chart
|
||||
if (self.state.attributes.graphType == 'bars') {
|
||||
points.push([y, x]);
|
||||
} else {
|
||||
points.push([x, y]);
|
||||
}
|
||||
});
|
||||
series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}});
|
||||
});
|
||||
return series;
|
||||
}
|
||||
});
|
||||
|
||||
my.GraphControls = Backbone.View.extend({
|
||||
className: "editor",
|
||||
template: ' \
|
||||
<div class="editor"> \
|
||||
<form class="form-stacked"> \
|
||||
<div class="clearfix"> \
|
||||
<label>Graph Type</label> \
|
||||
<div class="input editor-type"> \
|
||||
<select> \
|
||||
<option value="lines-and-points">Lines and Points</option> \
|
||||
<option value="lines">Lines</option> \
|
||||
<option value="points">Points</option> \
|
||||
<option value="bars">Bars</option> \
|
||||
<option value="columns">Columns</option> \
|
||||
</select> \
|
||||
</div> \
|
||||
<label>Group Column (x-axis)</label> \
|
||||
<div class="input editor-group"> \
|
||||
<select> \
|
||||
<option value="">Please choose ...</option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
<div class="editor-series-group"> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-buttons"> \
|
||||
<button class="btn editor-add">Add Series</button> \
|
||||
</div> \
|
||||
<div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
|
||||
<button class="editor-save">Save</button> \
|
||||
<input type="hidden" class="editor-id" value="chart-1" /> \
|
||||
</div> \
|
||||
</form> \
|
||||
</div> \
|
||||
',
|
||||
templateSeriesEditor: ' \
|
||||
<div class="editor-series js-series-{{seriesIndex}}"> \
|
||||
<label>Series <span>{{seriesName}} (y-axis)</span> \
|
||||
[<a href="#remove" class="action-remove-series">Remove</a>] \
|
||||
</label> \
|
||||
<div class="input"> \
|
||||
<select> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'change form select': 'onEditorSubmit',
|
||||
'click .editor-add': '_onAddSeries',
|
||||
'click .action-remove-series': 'removeSeries'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.fields.bind('reset', this.render);
|
||||
this.model.fields.bind('add', this.render);
|
||||
this.state = new recline.Model.ObjectState(options.state);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
this.el.html(htmls);
|
||||
|
||||
// set up editor from state
|
||||
if (this.state.get('graphType')) {
|
||||
this._selectOption('.editor-type', this.state.get('graphType'));
|
||||
}
|
||||
if (this.state.get('group')) {
|
||||
this._selectOption('.editor-group', this.state.get('group'));
|
||||
}
|
||||
// ensure at least one series box shows up
|
||||
var tmpSeries = [""];
|
||||
if (this.state.get('series').length > 0) {
|
||||
tmpSeries = this.state.get('series');
|
||||
}
|
||||
_.each(tmpSeries, function(series, idx) {
|
||||
self.addSeries(idx);
|
||||
self._selectOption('.editor-series.js-series-' + idx, series);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// Private: Helper function to select an option from a select list
|
||||
//
|
||||
_selectOption: function(id,value){
|
||||
var options = this.el.find(id + ' select > option');
|
||||
if (options) {
|
||||
options.each(function(opt){
|
||||
if (this.value == value) {
|
||||
$(this).attr('selected','selected');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onEditorSubmit: function(e) {
|
||||
var select = this.el.find('.editor-group select');
|
||||
var $editor = this;
|
||||
var $series = this.el.find('.editor-series select');
|
||||
var series = $series.map(function () {
|
||||
return $(this).val();
|
||||
});
|
||||
var updatedState = {
|
||||
series: $.makeArray(series),
|
||||
group: this.el.find('.editor-group select').val(),
|
||||
graphType: this.el.find('.editor-type select').val()
|
||||
};
|
||||
this.state.set(updatedState);
|
||||
},
|
||||
|
||||
// Public: Adds a new empty series select box to the editor.
|
||||
//
|
||||
// @param [int] idx index of this series in the list of series
|
||||
//
|
||||
// Returns itself.
|
||||
addSeries: function (idx) {
|
||||
var data = _.extend({
|
||||
seriesIndex: idx,
|
||||
seriesName: String.fromCharCode(idx + 64 + 1)
|
||||
}, this.model.toTemplateJSON());
|
||||
|
||||
var htmls = Mustache.render(this.templateSeriesEditor, data);
|
||||
this.el.find('.editor-series-group').append(htmls);
|
||||
return this;
|
||||
},
|
||||
|
||||
_onAddSeries: function(e) {
|
||||
e.preventDefault();
|
||||
this.addSeries(this.state.get('series').length);
|
||||
},
|
||||
|
||||
// Public: Removes a series list item from the editor.
|
||||
//
|
||||
// Also updates the labels of the remaining series elements.
|
||||
removeSeries: function (e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
$el.parent().parent().remove();
|
||||
this.onEditorSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
this.recline.View.Graph = this.recline.View.Flot;
|
||||
this.recline.View.GraphControls = this.recline.View.FlotControls;
|
||||
|
||||
@@ -141,7 +141,7 @@ my.FilterEditor = Backbone.View.extend({
|
||||
var $input = $(input);
|
||||
var filterType = $input.attr('data-filter-type');
|
||||
var fieldId = $input.attr('data-filter-field');
|
||||
var filterIndex = parseInt($input.attr('data-filter-id'));
|
||||
var filterIndex = parseInt($input.attr('data-filter-id'), 10);
|
||||
var name = $input.attr('name');
|
||||
var value = $input.val();
|
||||
|
||||
@@ -162,7 +162,7 @@ my.FilterEditor = Backbone.View.extend({
|
||||
break;
|
||||
}
|
||||
});
|
||||
self.model.queryState.set({filters: filters});
|
||||
self.model.queryState.set({filters: filters, from: 0});
|
||||
self.model.queryState.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user