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/css-site/style.css b/css-site/style.css index b1c5b023..dea128c3 100644 --- a/css-site/style.css +++ b/css-site/style.css @@ -21,13 +21,25 @@ Author URI: http://www.mintcanary.com/ @import url(http://fonts.googleapis.com/css?family=PT+Sans:400,400italic,700); +body, p { + font-family: 'PT Sans',Helvetica,Arial,sans-serif; + font-size: 15px; +} + h1, h2, h3, h4, h5, h6 { font-family:'PT Sans',Helvetica, Arial, sans-serif; } -body, p { - font-family: 'PT Sans',Helvetica,Arial,sans-serif; - font-size: 15px; +h2 { + font-size: 24px; +} + +h3 { + font-size: 20px; +} + +h4 { + font-size: 18px; } a { diff --git a/css/flot.css b/css/flot.css index 03f21e56..cf203cda 100644 --- a/css/flot.css +++ b/css/flot.css @@ -1,23 +1,23 @@ -.recline-graph .graph { +.recline-flot .graph { height: 500px; overflow: hidden; } -.recline-graph .legend table { +.recline-flot .legend table { width: auto; margin-bottom: 0; } -.recline-graph .legend td { +.recline-flot .legend td { padding: 5px; line-height: 13px; } -.recline-graph .graph .alert { +.recline-flot .graph .alert { width: 450px; } -#recline-graph-tooltip { +#recline-flot-tooltip { position: absolute; background-color: #FEE !important; color: #000000 !important; diff --git a/css/graph.css b/css/flotr2.css similarity index 100% rename from css/graph.css rename to css/flotr2.css diff --git a/demos/search/demo.search.app.js b/demos/search/demo.search.app.js index 72b5937a..49f5f9b9 100644 --- a/demos/search/demo.search.app.js +++ b/demos/search/demo.search.app.js @@ -207,32 +207,6 @@ var templates = { data: data }); }, - 'http://openspending.org/api/search': function(record) { - record['time'] = record['time.label_facet'] - var template = '
\ -

\ - {{record.dataset}} {{record.time}} \ - – {{amount_formatted}} \ -

\ - \ -
\ - '; - var data = []; - _.each(_.keys(record), function(key) { - if (key !='_id' && key != 'id') { - data.push({ key: key, value: record[key] }); - } - }); - return Mustache.render(template, { - record: record, - amount_formatted: formatAmount(record['amount']), - data: data - }); - }, 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdExXSTl2Y01xZEszOTBFZjVzcGtzVVE': function(record) { var template = '
\

\ diff --git a/demos/search/index.html b/demos/search/index.html index e19ada5b..28d3bb6a 100644 --- a/demos/search/index.html +++ b/demos/search/index.html @@ -86,7 +86,7 @@ ul.facet-items {

This demo shows how Recline can be used to build a search app. It includes faceting as well as search. You can find the source javascript here (plus prettified version of source for readability) – please feel free to reuse!

-

The default setup uses local example data but you can also connect directly to any other backend supported by Recline, for example SOLR, ElasticSearch or even a google docs spreadsheet. As an example: here's an example running against the SOLR-style OpenSpending API and here's an example running against a GDocs spreadsheet (Oil spills in the Niger Delta).

+

The default setup uses local example data but you can also connect directly to any other backend supported by Recline, for example SOLR, ElasticSearch or even a google docs spreadsheet. Here's an example running against a GDocs spreadsheet (Oil spills in the Niger Delta).


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: ' \
\ @@ -2295,7 +2820,7 @@ my.GraphControls = Backbone.View.extend({ \ \
\ - \ + \
\ \
\ - \ + \
\ \ + \ + \ + \ + \ + \ + \ +
\ + \ +
\ + \ +
\ +
\ +
\ +
\ +
\ + \ +
\ + \ + \ +
\ +', + 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); + diff --git a/src/view.graph.js b/src/view.graph.js index 6dcef514..fb3d8356 100644 --- a/src/view.graph.js +++ b/src/view.graph.js @@ -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: ' \ -
\ -
\ -
\ -

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'); - 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: ' \ -
\ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -', - 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); - +this.recline.View.Graph = this.recline.View.Flot; +this.recline.View.GraphControls = this.recline.View.FlotControls; diff --git a/src/widget.filtereditor.js b/src/widget.filtereditor.js index 28c80bb3..14112511 100644 --- a/src/widget.filtereditor.js +++ b/src/widget.filtereditor.js @@ -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'); } }); diff --git a/test/backend.solr.test.js b/test/backend.solr.test.js deleted file mode 100644 index 2b7ec65a..00000000 --- a/test/backend.solr.test.js +++ /dev/null @@ -1,85 +0,0 @@ -(function ($) { -module("Backend SOLR"); - -test("fetch", function() { - var dataset = new recline.Model.Dataset({ - url: 'http://openspending.org/api/search', - backend: 'solr' - }); - // stop(); - - var stub = sinon.stub($, 'ajax', function(options) { - return { - done: function(callback) { - callback(sample_data); - return this; - }, - fail: function() { - } - }; - }); - - dataset.fetch().done(function(dataset) { - var exp = [ - "_id", - "amount", - "category.label_facet", - "dataset", - "from.label_facet", - "id", - "subcategory.label_facet", - "time.label_facet", - "to.label_facet" - ]; - deepEqual( - exp, - _.pluck(dataset.fields.toJSON(), 'id') - ); - // check we've mapped types correctly - equal(dataset.fields.get('amount').get('type'), 'string'); - - // fetch does a query so we can check for records - equal(dataset.recordCount, 10342132); - equal(dataset.records.length, 2); - equal(dataset.records.at(0).get('id'), '3e3e25d7737634127b76d5ee4a7df280987013c7'); - // start(); - }); - $.ajax.restore(); -}); - -var sample_data = { - "response": { - "docs": [ - { - "_id": "south-african-national-gov-budget-2012-13::3e3e25d7737634127b76d5ee4a7df280987013c7", - "amount": 30905738200000.0, - "category.label_facet": "General public services", - "dataset": "south-african-national-gov-budget-2012-13", - "from.label_facet": "National Treasury", - "id": "3e3e25d7737634127b76d5ee4a7df280987013c7", - "subcategory.label_facet": "Transfers of a general character between different levels of government", - "time.label_facet": "01. April 2012", - "to.label_facet": "Provincial Equitable Share" - }, - { - "_id": "south-african-national-gov-budget-2012-13::738849e28e6b3c45e5b0001e142b51479b3a3e41", - "amount": 8938807300000.0, - "category.label_facet": "General public services", - "dataset": "south-african-national-gov-budget-2012-13", - "from.label_facet": "National Treasury", - "id": "738849e28e6b3c45e5b0001e142b51479b3a3e41", - "subcategory.label_facet": "Public debt transactions", - "time.label_facet": "01. April 2012", - "to.label_facet": "State Debt Costs" - } - ], - "numFound": 10342132, - "start": 0 - }, - "responseHeader": { - "QTime": 578, - "status": 0 - } -}; - -})(this.jQuery); diff --git a/test/index.html b/test/index.html index 7a102b4f..cfa1151e 100644 --- a/test/index.html +++ b/test/index.html @@ -10,7 +10,6 @@ - @@ -39,7 +38,6 @@ - @@ -48,14 +46,14 @@ - - + + @@ -66,7 +64,7 @@ - + diff --git a/test/view.graph.test.js b/test/view.flotr2.test.js similarity index 87% rename from test/view.graph.test.js rename to test/view.flotr2.test.js index 25a04d6c..89edfada 100644 --- a/test/view.graph.test.js +++ b/test/view.flotr2.test.js @@ -1,8 +1,8 @@ -module("View - Graph"); +module("View - Flotr2"); test('basics', function () { var dataset = Fixture.getDataset(); - var view = new recline.View.Graph({ + var view = new recline.View.Flotr2({ model: dataset }); $('.fixtures').append(view.el); @@ -14,7 +14,7 @@ test('basics', function () { test('initialize', function () { var dataset = Fixture.getDataset(); - var view = new recline.View.Graph({ + var view = new recline.View.Flotr2({ model: dataset, state: { 'graphType': 'lines', @@ -40,7 +40,7 @@ test('initialize', function () { test('dates in graph view', function () { expect(0); var dataset = Fixture.getDataset(); - var view = new recline.View.Graph({ + var view = new recline.View.Flotr2({ model: dataset, state: { 'graphType': 'lines', @@ -53,9 +53,9 @@ test('dates in graph view', function () { view.remove(); }); -test('GraphControls basics', function () { +test('Flotr2Controls basics', function () { var dataset = Fixture.getDataset(); - var view = new recline.View.GraphControls({ + var view = new recline.View.Flotr2Controls({ model: dataset, state: { graphType: 'bars', @@ -72,7 +72,7 @@ test('GraphControls basics', function () { test('Overriding graph options', function () { var dataset = Fixture.getDataset(); var randomWidth = Math.random(); - var view = new recline.View.Graph({ + var view = new recline.View.Flotr2({ model: dataset, state: { 'graphType': 'bars', @@ -83,4 +83,4 @@ test('Overriding graph options', function () { }); equal(view.getGraphOptions('bars').bars.barWidth, randomWidth) view.remove(); -}); \ No newline at end of file +});