diff --git a/_includes/data.js b/_includes/data.js new file mode 100644 index 00000000..44015ffd --- /dev/null +++ b/_includes/data.js @@ -0,0 +1,9 @@ +var data = [ + {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}, + {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', label: 'second', lat:54.97, lon:-1.60}, + {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5}, + {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20}, + {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0}, + {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', label: 'sixth', lat:51.04, lon:7.9} +]; + diff --git a/app/index.html b/app/index.html index 9fe1ae93..08d17ff4 100644 --- a/app/index.html +++ b/app/index.html @@ -97,36 +97,37 @@

Welcome to the Recline Data Explorer

Recline allows you to explore and work with data in your browser and then share with others

- In basic operation it's much like a spreadsheet - though it's - feature set is a little different. In particular, the Data - Explorer provides: - -
-
-

View the demo

-

Take a look at a local demo dataset.

-

View the demo dataset »

-
-
-

Read the tutorial

-

Take a look at the tutorial for using the data explorer:

- Read the tutorial » -
-
-

Import some data

-

Starting working with some data straight away. You can import some data using the menu at the top right of this page.

-
+
+
+
+
+

View the demo

+

Try out the demo using a local example dataset.

+

View the demo dataset »

+
+
+
+
+

Features

+
    +
  • Data grid
  • +
  • Data editing including programmatic data transformation in javascript
  • +
  • Visualizations includes graphs and maps
  • +
  • Import and export from a variety of sources including online sources such as online Excel and CSV files, Google docs and + the DataHub and offline sources like CSV files on your local machine.
  • +
  • Use online or offline - because the app is built in pure javascript and html you can use it anywhere there's a modern web browser. Using offline is as easy and downloading this web page to your local machine.
  • +
+
+
+
+
+

Get started

+

Get started straight away for example by importing some data from an external source using the menu at the top right of this page.

+
@@ -153,7 +154,7 @@
diff --git a/app/js/app.js b/app/js/app.js index 4070c841..145ec05b 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -21,7 +21,7 @@ var ExplorerApp = Backbone.View.extend({ this.router.route(/explorer/, 'explorer', this.viewExplorer); Backbone.history.start(); - var state = recline.Util.parseQueryString(window.location.search); + var state = recline.Util.parseQueryString(decodeURIComponent(window.location.search)); if (state) { _.each(state, function(value, key) { try { diff --git a/css/grid.css b/css/grid.css index c0439459..a8ff0625 100644 --- a/css/grid.css +++ b/css/grid.css @@ -2,20 +2,22 @@ * (Data) Grid *********************************************************/ +table.recline-grid { + table-layout: fixed; + width: 100%; +} + .recline-grid .btn-group .dropdown-toggle { padding: 1px 3px; line-height: auto; } -.recline-grid { - border: 1px solid #ccc; - width: 100%; -} - .recline-grid td, .recline-grid th { border-left: 1px solid #ccc; padding: 3px 4px; text-align: left; + word-wrap: break-word; + white-space: normal; } .recline-grid td { @@ -26,6 +28,14 @@ width: 20px; } +.recline-grid tbody tr:last-child { + border-bottom: 1px solid #ccc; +} + +.recline-grid tbody td:last-child { + border-right: 1px solid #ccc; +} + /* direct borrowing from twitter buttons */ .recline-grid th, .transform-column-view .expression-preview-table-wrapper th @@ -53,6 +63,42 @@ transition: 0.1s linear all; } +/********************************************************** + * Fixed Header - http://www.imaputz.com/cssStuff/bigFourVersion.html + *********************************************************/ + +div.table-container { + overflow: auto; +} + +/* Reset overflow value to hidden for all non-IE browsers. */ +html>body div.table-container { + overflow: hidden; +} + +/* set table header to a fixed position. WinIE 6.x only */ +/* In WinIE 6.x, any element with a position property set to relative and is a child of */ +/* an element that has an overflow property set, the relative value translates into fixed. */ +/* Ex: parent element DIV with a class of table-container has an overflow property set to auto */ +thead.fixed-header tr { + position: relative +} + +/* set THEAD element to have block level attributes. All other non-IE browsers */ +/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ +html>body thead.fixed-header tr { + display: block +} + +/* define the table content to be scrollable */ +/* set TBODY element to have block level attributes. All other non-IE browsers */ +/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ +/* induced side effect is that child TDs no longer accept width: auto */ +tbody.scroll-content { + display: block; + max-height: 500px; + overflow: auto; +} /********************************************************** * Data Table Menus diff --git a/example-quickstart.markdown b/example-quickstart.markdown index e87ab283..266d8e6c 100644 --- a/example-quickstart.markdown +++ b/example-quickstart.markdown @@ -20,41 +20,42 @@ Before writing any code with Recline, you need to do the following preparation s 2. Include the relevant CSS in the head section of your document: {% highlight html %} - + - {% endhighlight %} +{% endhighlight %} 3. Include the relevant Javascript files somewhere on the page (preferably before body close tag): {% highlight html %} - - - + {% endhighlight %} 4. Create a div to hold the Recline view(s): {% highlight html %} -
{% endhighlight %} +
{% endhighlight %} You're now ready to start working with Recline. ### Creating a Dataset -We are going to be working with the following set of data: +Here's some example data We are going to work with: {% highlight javascript %} -var data = [ - {id: 0, x: 1, y: 2, z: 3, country: 'UK', label: 'first'}, - {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}, - {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'} - ]; +{% include data.js %} {% endhighlight %} -Here we have 3 documents / rows each of which is a javascript object containing keys and values (note that all values here are 'simple' but there is no reason you cannot have full objects as values. +In this data we have 6 documents / rows. Each document is a javascript object +containing keys and values (note that all values here are 'simple' but there is +no reason you cannot have objects as values allowing you to nest data. We can now create a recline Dataset object (and memory backend) from this raw data: @@ -67,28 +68,25 @@ Note that behind the scenes Recline will create a Memory backend for this datase ### Setting up the Grid -Let's create a data grid view to display the dataset we have just created, binding the view to the `
` we created earlier: +Let's create a data grid view to display the dataset we have just created, binding the view to the `
` we created earlier: {% highlight javascript %} +var $el = $('#mygrid'); var grid = new recline.View.Grid({ - model: dataset, - el: $('#recline-grid') + model: dataset }); +$el.append(grid.el); grid.render(); {% endhighlight %} And hey presto: -
 
+
 
+### Creating a Graph + +Let's create a graph view to display a line graph for this dataset. + +First, create a new div for the graph: + +{% highlight html %} +
+{% endhighlight %} + +Now let's create the graph, we will use the same dataset we had earlier: + +{% highlight javascript %} +var $el = $('#mygraph'); +var graph = new recline.View.Graph({ + model: dataset +}); +$el.append(grid.el); +graph.render(); +{% endhighlight %} + +And ... we have a graph view -- with instructions on how to use the controls to +create a graph -- but no graph. Go ahead and play around with the controls to +create a graph of your choosing: + +
 
+ + + +But I wanted to create a graph not a graph editor. Can we do that? Yes you can! +All you need to do is set the 'state' of the graph view: + +{% highlight javascript %} +var $el = $('#mygraph'); +var graph = new recline.View.Graph({ + model: dataset, + state: { + group: "date", + series: ["x", "z"] + } +}); +$el.append(grid.el); +graph.render(); +graph.redraw(); +{% endhighlight %} + +We would get this rendered graph: + +
 
+ + + +
+State: The concept of a state is a common feature of Recline views being an object +which stores information about the state and configuration of a given view. You +can read more about it in the general Views +documentation as well as the documentation of individual views such as the +Graph View. +
+ diff --git a/recline.js b/recline.js index 261de327..80a7b909 100644 --- a/recline.js +++ b/recline.js @@ -612,7 +612,7 @@ my.composeQueryString = function(queryParams) { if (typeof(value) === 'object') { value = JSON.stringify(value); } - items.push(key + '=' + value); + items.push(key + '=' + encodeURIComponent(value)); }); queryString += items.join('&'); return queryString; @@ -1177,8 +1177,9 @@ my.Grid = Backbone.View.extend({ // ====================================================== // #### Templating template: ' \ +
\ \ - \ + \ \ {{#notEmpty}} \ \ {{/notEmpty}} \ {{#fields}} \ - \ {{/fields}} \ + \ \ \ - \ + \
\ @@ -1191,7 +1192,7 @@ my.Grid = Backbone.View.extend({ \ + \
\ \
\ +
\ ', toTemplateJSON: function() { + var self = this; var modelData = this.model.toJSON(); modelData.notEmpty = ( this.fields.length > 0 ); // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); }); + modelData.fields = _.map(this.fields, function(field) { + return field.toJSON(); + }); + // last header width = scroll bar - border (2px) */ + modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2; return modelData; }, render: function() { @@ -1228,6 +1236,20 @@ my.Grid = Backbone.View.extend({ this.fields = this.model.fields.filter(function(field) { return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; }); + this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions + var numFields = this.fields.length; + // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) + var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; + var width = parseInt(Math.max(50, fullWidth / numFields)); + var remainder = fullWidth - numFields * width; + _.each(this.fields, function(field, idx) { + // add the remainder to the first field width so we make up full col + if (idx == 0) { + field.set({width: width+remainder}); + } else { + field.set({width: width}); + } + }); var htmls = $.mustache(this.template, this.toTemplateJSON()); this.el.html(htmls); this.model.currentDocuments.forEach(function(doc) { @@ -1240,8 +1262,25 @@ my.Grid = Backbone.View.extend({ }); newView.render(); }); + // hide extra header col if no scrollbar to avoid unsightly overhang + var $tbody = this.el.find('tbody')[0]; + if ($tbody.scrollHeight <= $tbody.offsetHeight) { + this.el.find('th.last-header').hide(); + } this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); return this; + }, + + // ### _scrollbarSize + // + // Measure width of a vertical scrollbar and height of a horizontal scrollbar. + // + // @return: { width: pixelWidth, height: pixelHeight } + _scrollbarSize: function() { + var $c = $("
").appendTo("body"); + var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight }; + $c.remove(); + return dim; } }); @@ -1278,7 +1317,7 @@ my.GridRow = Backbone.View.extend({ \ \ {{#cells}} \ - \ + \
\   \
{{{value}}}
\ @@ -1298,6 +1337,7 @@ my.GridRow = Backbone.View.extend({ var cellData = this._fields.map(function(field) { return { field: field.id, + width: field.get('width'), value: doc.getFieldValue(field) }; }); @@ -1795,7 +1835,7 @@ my.Map = Backbone.View.extend({ // on [OpenStreetMap](http://openstreetmap.org). // _setupMap: function(){ - var self = this; + this.map = new L.Map(this.$map.get(0)); var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; @@ -1835,14 +1875,6 @@ my.Map = Backbone.View.extend({ this.map.setView(new L.LatLng(0, 0), 2); - var popup = new L.Popup(); - this.map.on('click', function(e) { - var latlngStr = '(' + e.latlng.lat.toFixed(3) + ', ' + e.latlng.lng.toFixed(3) + ')'; - popup.setLatLng(e.latlng); - popup.setContent("You clicked the map at " + latlngStr); - self.map.openPopup(popup); - }); - this.mapReady = true; }, @@ -2043,10 +2075,11 @@ my.ColumnTransform = Backbone.View.extend({ // # Recline Views // -// Recline Views are Backbone Views and in keeping with normal Backbone views -// are Widgets / Components displaying something in the DOM. Like all Backbone -// views they have a pointer to a model or a collection and is bound to an -// element. +// Recline Views are instances of Backbone Views and they act as 'WUI' (web +// user interface) component displaying some model object in the DOM. Like all +// Backbone views they have a pointer to a model (or a collection) and have an +// associated DOM-style element (usually this element will be bound into the +// page at some point). // // Views provided by core Recline are crudely divided into two types: // @@ -2262,12 +2295,11 @@ my.DataExplorer = Backbone.View.extend({ } this.model.bind('query:start', function() { - self.notify({message: 'Loading data', loader: true}); + self.notify({loader: true, persist: true}); }); this.model.bind('query:done', function() { self.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - self.notify({message: 'Data loaded', category: 'success'}); }); this.model.bind('query:fail', function(error) { self.clearNotifications(); @@ -2432,19 +2464,25 @@ my.DataExplorer = Backbone.View.extend({ // * loader: if true show loading spinner notify: function(flash) { var tmplData = _.extend({ - message: '', - category: 'warning' + message: 'Loading', + category: 'warning', + loader: false }, flash ); - var _template = ' \ -
× \ - {{message}} \ - {{#loader}} \ + if (tmplData.loader) { + var _template = ' \ +
\ + {{message}} \   \ - {{/loader}} \ -
'; - var _templated = $.mustache(_template, tmplData); +
'; + } else { + var _template = ' \ +
× \ + {{message}} \ +
'; + } + var _templated = $($.mustache(_template, tmplData)); _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); if (!flash.persist) { setTimeout(function() { @@ -2460,7 +2498,9 @@ my.DataExplorer = Backbone.View.extend({ // Clear all existing notifications clearNotifications: function() { var $notifications = $('.recline-data-explorer .alert-messages .alert'); - $notifications.remove(); + $notifications.fadeOut(1500, function() { + $(this).remove(); + }); } }); diff --git a/src/util.js b/src/util.js index 55e9390a..dbba7b0e 100644 --- a/src/util.js +++ b/src/util.js @@ -56,7 +56,7 @@ my.composeQueryString = function(queryParams) { if (typeof(value) === 'object') { value = JSON.stringify(value); } - items.push(key + '=' + value); + items.push(key + '=' + encodeURIComponent(value)); }); queryString += items.join('&'); return queryString; diff --git a/src/view-grid.js b/src/view-grid.js index fc158723..f2a3bb2f 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -130,8 +130,9 @@ my.Grid = Backbone.View.extend({ // ====================================================== // #### Templating template: ' \ +
\ \ - \ + \ \ {{#notEmpty}} \ \ {{/notEmpty}} \ {{#fields}} \ - \ {{/fields}} \ + \ \ \ - \ + \
\ @@ -144,7 +145,7 @@ my.Grid = Backbone.View.extend({ \ + \
\ \
\ +
\ ', toTemplateJSON: function() { + var self = this; var modelData = this.model.toJSON(); modelData.notEmpty = ( this.fields.length > 0 ); // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); }); + modelData.fields = _.map(this.fields, function(field) { + return field.toJSON(); + }); + // last header width = scroll bar - border (2px) */ + modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2; return modelData; }, render: function() { @@ -181,6 +189,20 @@ my.Grid = Backbone.View.extend({ this.fields = this.model.fields.filter(function(field) { return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; }); + this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions + var numFields = this.fields.length; + // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) + var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; + var width = parseInt(Math.max(50, fullWidth / numFields)); + var remainder = fullWidth - numFields * width; + _.each(this.fields, function(field, idx) { + // add the remainder to the first field width so we make up full col + if (idx == 0) { + field.set({width: width+remainder}); + } else { + field.set({width: width}); + } + }); var htmls = $.mustache(this.template, this.toTemplateJSON()); this.el.html(htmls); this.model.currentDocuments.forEach(function(doc) { @@ -193,8 +215,25 @@ my.Grid = Backbone.View.extend({ }); newView.render(); }); + // hide extra header col if no scrollbar to avoid unsightly overhang + var $tbody = this.el.find('tbody')[0]; + if ($tbody.scrollHeight <= $tbody.offsetHeight) { + this.el.find('th.last-header').hide(); + } this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); return this; + }, + + // ### _scrollbarSize + // + // Measure width of a vertical scrollbar and height of a horizontal scrollbar. + // + // @return: { width: pixelWidth, height: pixelHeight } + _scrollbarSize: function() { + var $c = $("
").appendTo("body"); + var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight }; + $c.remove(); + return dim; } }); @@ -231,7 +270,7 @@ my.GridRow = Backbone.View.extend({
\ \ {{#cells}} \ - \ + \
\   \
{{{value}}}
\ @@ -251,6 +290,7 @@ my.GridRow = Backbone.View.extend({ var cellData = this._fields.map(function(field) { return { field: field.id, + width: field.get('width'), value: doc.getFieldValue(field) }; });