diff --git a/README.md b/README.md
index ba90eed4..d9e1d58f 100755
--- a/README.md
+++ b/README.md
@@ -17,6 +17,14 @@ A simple but powerful library for building data applications in pure Javascript
Run the tests by opening `test/index.html` in your browser.
+Note that the demos and documentation utilize the [jekyll templating
+system][jekyll] and to use them *locally* you will need to build them using
+jekyll. Once installed, all you need to do from the command line is run jekyll:
+
+ jekyll
+
+[jekyll]: https://github.com/mojombo/jekyll
+
Notes on the architecture can be found in the [documentation online](http://okfnlabs.org/recline).
### Contributing
diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html
index 84d43041..7b78093b 100644
--- a/_includes/recline-deps.html
+++ b/_includes/recline-deps.html
@@ -13,7 +13,6 @@
-
@@ -23,11 +22,9 @@
-
-
@@ -66,8 +63,8 @@
-
+
diff --git a/_layouts/default.html b/_layouts/default.html
index 03f35f4f..f293915c 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -59,9 +59,9 @@
diff --git a/dist/recline.css b/dist/recline.css
index 6a7f23d7..bc71116c 100644
--- a/dist/recline.css
+++ b/dist/recline.css
@@ -1,3 +1,29 @@
+.recline-flot .graph {
+ height: 500px;
+ overflow: hidden;
+}
+
+.recline-flot .legend table {
+ width: auto;
+ margin-bottom: 0;
+}
+
+.recline-flot .legend td {
+ padding: 5px;
+ line-height: 13px;
+}
+
+.recline-flot .graph .alert {
+ width: 450px;
+}
+
+#recline-flot-tooltip {
+ position: absolute;
+ background-color: #FEE !important;
+ color: #000000 !important;
+ opacity: 0.8 !important;
+ border: 1px solid #fdd !important;
+}
.recline-graph .graph {
height: 500px;
}
diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js
index da5c2a96..92183702 100644
--- a/dist/recline.dataset.js
+++ b/dist/recline.dataset.js
@@ -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;
+
// ## Dataset
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) {
@@ -594,6 +597,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
@@ -637,7 +643,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);
@@ -650,7 +656,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;
@@ -818,7 +824,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)
diff --git a/dist/recline.js b/dist/recline.js
index 86137939..297cc26b 100644
--- a/dist/recline.js
+++ b/dist/recline.js
@@ -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 = {
@@ -143,6 +146,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
//
@@ -162,7 +169,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
// }
//
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';
@@ -437,6 +444,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).
@@ -453,7 +464,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);
@@ -477,7 +488,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'
@@ -503,6 +514,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.
@@ -677,7 +691,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){
@@ -705,7 +719,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);
@@ -723,7 +737,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) {
@@ -785,6 +799,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.
@@ -809,13 +826,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({
@@ -949,6 +966,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
@@ -992,7 +1012,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);
@@ -1005,7 +1025,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;
@@ -1173,7 +1193,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)
@@ -1193,6 +1213,9 @@ this.recline.Backend.Solr = this.recline.Backend.Solr || {};
(function($, my) {
my.__type__ = 'solr';
+ // use either jQuery or Underscore Deferred depending on what is available
+ var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;
+
// ### fetch
//
// dataset must have a solr or url attribute pointing to solr endpoint
@@ -1206,7 +1229,7 @@ this.recline.Backend.Solr = this.recline.Backend.Solr || {};
dataType: 'jsonp',
jsonp: 'json.wrf'
});
- var dfd = new _.Deferred();
+ var dfd = new Deferred();
jqxhr.done(function(results) {
// if we get 0 results we cannot get fields
var fields = []
@@ -1239,7 +1262,7 @@ this.recline.Backend.Solr = this.recline.Backend.Solr || {};
dataType: 'jsonp',
jsonp: 'json.wrf'
});
- var dfd = new _.Deferred();
+ var dfd = new Deferred();
jqxhr.done(function(results) {
var out = {
total: results.response.numFound,
@@ -1390,6 +1413,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;
+
// ## Dataset
my.Dataset = Backbone.Model.extend({
constructor: function Dataset() {
@@ -1433,7 +1459,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())
@@ -1567,7 +1593,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) {
@@ -1631,7 +1657,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) {
@@ -1987,6 +2013,505 @@ 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',
+// 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: ' \
+
\
+
\
+
\
+
Hey there!
\
+
There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
\
+
Please tell us by using the menu on the right and a graph will automatically appear.
\
+
\
+
\
+
\
+',
+
+ initialize: function(options) {
+ var self = this;
+ this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
+
+ this.el = $(this.el);
+ _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
+ 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.previousTooltipPoint = {x: null, y: null};
+ this.editor = new my.FlotControls({
+ 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');
+ this.$graph.on("plothover", this._toolTip);
+ return this;
+ },
+
+ redraw: function() {
+ // 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'
+ 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')) {
+ var series = this.createSeries();
+ var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
+ this.plot = $.plot(this.$graph, series, options);
+ }
+ },
+
+ show: function() {
+ // because we cannot redraw when hidden we may need to when becoming visible
+ if (this.needToRedraw) {
+ this.redraw();
+ }
+ },
+
+ // infoboxes on mouse hover on points/bars etc
+ _toolTip: function (event, pos, item) {
+ if (item) {
+ if (this.previousTooltipPoint.x !== item.dataIndex ||
+ this.previousTooltipPoint.y !== item.seriesIndex) {
+ this.previousTooltipPoint.x = item.dataIndex;
+ this.previousTooltipPoint.y = item.seriesIndex;
+ $("#recline-flot-tooltip").remove();
+
+ var x = item.datapoint[0].toFixed(2),
+ y = item.datapoint[1].toFixed(2);
+
+ var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
+ group: this.state.attributes.group,
+ x: this._xaxisLabel(x),
+ series: item.series.label,
+ y: y
+ });
+
+ // use a different tooltip location offset for bar charts
+ var xLocation, yLocation;
+ if (this.state.attributes.graphType === 'bars') {
+ xLocation = item.pageX + 15;
+ yLocation = item.pageY;
+ } else {
+ xLocation = item.pageX + 10;
+ yLocation = item.pageY - 20;
+ }
+
+ $('
' + content + '
').css({
+ top: yLocation,
+ left: xLocation
+ }).appendTo("body").fadeIn(200);
+ }
+ } else {
+ $("#recline-flot-tooltip").remove();
+ this.previousTooltipPoint.x = null;
+ this.previousTooltipPoint.y = null;
+ }
+ },
+
+ _xaxisLabel: function (x) {
+ var xfield = this.model.fields.get(this.state.attributes.group);
+
+ // time series
+ var xtype = xfield.get('type');
+ var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
+
+ if (this.model.records.models[parseInt(x, 10)]) {
+ x = this.model.records.models[parseInt(x, 10)].get(this.state.attributes.group);
+ if (isDateTime) {
+ x = new Date(x).toLocaleDateString();
+ }
+ } else if (isDateTime) {
+ x = new Date(parseInt(x, 10)).toLocaleDateString();
+ }
+
+ return x;
+ },
+
+ // ### getGraphOptions
+ //
+ // Get options for Flot Graph
+ //
+ // needs to be function as can depend on state
+ //
+ // @param typeId graphType id (lines, lines-and-points etc)
+ // @param numPoints the number of points that will be plotted
+ getGraphOptions: function(typeId, numPoints) {
+ var self = this;
+
+ var tickFormatter = function (x) {
+ // convert x to a string and make sure that it is not too long or the
+ // tick labels will overlap
+ // TODO: find a more accurate way of calculating the size of tick labels
+ var label = self._xaxisLabel(x);
+
+ if (typeof label !== 'string') {
+ label = label.toString();
+ }
+ if (label.length > 8) {
+ label = label.slice(0, 5) + "...";
+ }
+
+ return label;
+ };
+
+ var xaxis = {};
+ xaxis.tickFormatter = tickFormatter;
+
+ // calculate the x-axis ticks
+ //
+ // the number of ticks should be a multiple of the number of points so that
+ // each tick lines up with a point
+ if (numPoints) {
+ var ticks = [],
+ maxTicks = 10,
+ x = 1,
+ i = 0;
+
+ while (x <= maxTicks) {
+ if ((numPoints / x) <= maxTicks) {
+ break;
+ }
+ x = x + 1;
+ }
+
+ for (i = 0; i < numPoints; i = i + x) {
+ ticks.push(i);
+ }
+
+ xaxis.ticks = ticks;
+ }
+
+ var yaxis = {};
+ yaxis.autoscale = true;
+ yaxis.autoscaleMargin = 0.02;
+
+ var legend = {};
+ legend.position = 'ne';
+
+ var grid = {};
+ grid.hoverable = true;
+ grid.clickable = true;
+ grid.borderColor = "#aaaaaa";
+ grid.borderWidth = 1;
+
+ var optionsPerGraphType = {
+ lines: {
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: true },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ grid: grid
+ },
+ points: {
+ legend: legend,
+ colors: this.graphColors,
+ points: { show: true, hitRadius: 5 },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ grid: grid
+ },
+ 'lines-and-points': {
+ legend: legend,
+ colors: this.graphColors,
+ points: { show: true, hitRadius: 5 },
+ lines: { show: true },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ grid: grid
+ },
+ bars: {
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: false },
+ xaxis: yaxis,
+ yaxis: xaxis,
+ grid: grid,
+ bars: {
+ show: true,
+ horizontal: true,
+ shadowSize: 0,
+ align: 'center',
+ barWidth: 0.8
+ }
+ },
+ columns: {
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: false },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ grid: grid,
+ bars: {
+ show: true,
+ horizontal: false,
+ shadowSize: 0,
+ align: 'center',
+ barWidth: 0.8
+ }
+ }
+ };
+
+ if (self.state.get('graphOptions')) {
+ return _.extend(optionsPerGraphType[typeId],
+ self.state.get('graphOptions'));
+ } else {
+ return optionsPerGraphType[typeId];
+ }
+ },
+
+ 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) {
+ 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 = parseFloat(x);
+ if (isNaN(x)) {
+ x = index;
+ }
+ }
+
+ var yfield = self.model.fields.get(field);
+ var y = doc.getFieldValue(yfield);
+
+ if (self.state.attributes.graphType == 'bars') {
+ points.push([y, x]);
+ } else {
+ points.push([x, y]);
+ }
+ });
+ series.push({
+ data: points,
+ label: field,
+ hoverable: true
+ });
+ });
+ return series;
+ }
+});
+
+my.FlotControls = Backbone.View.extend({
+ className: "editor",
+ template: ' \
+
\
+ \
+
\
+',
+ templateSeriesEditor: ' \
+
\
+ \
+
\
+ \
+
\
+
\
+ ',
+ 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);
+/*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}, ... ],
@@ -1996,7 +2521,7 @@ this.recline.View = this.recline.View || {};
//
// 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({
+my.Flotr2 = Backbone.View.extend({
template: ' \
\
\
@@ -2030,7 +2555,7 @@ my.Graph = Backbone.View.extend({
options.state
);
this.state = new recline.Model.ObjectState(stateData);
- this.editor = new my.GraphControls({
+ this.editor = new my.Flotr2Controls({
model: this.model,
state: this.state.toJSON()
});
@@ -2051,9 +2576,9 @@ my.Graph = Backbone.View.extend({
},
redraw: function() {
- // There appear to be issues generating a Flot graph if either:
+ // 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 -- Flot will complain with
+ // * 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'
@@ -2082,7 +2607,7 @@ my.Graph = Backbone.View.extend({
// ### getGraphOptions
//
- // Get options for Flot Graph
+ // Get options for Flotr2 Graph
//
// needs to be function as can depend on state
//
@@ -2279,7 +2804,7 @@ my.Graph = Backbone.View.extend({
}
});
-my.GraphControls = Backbone.View.extend({
+my.Flotr2Controls = Backbone.View.extend({
className: "editor",
template: ' \