this . recline = this . recline || {};
+ dataproxy.js
dataproxy.js this . recline = this . recline || {};
this . recline . Backend = this . recline . Backend || {};
( function ( $ , my ) { DataProxy Backend
@@ -25,28 +25,9 @@
sync : function ( method , model , options ) {
var self = this ;
if ( method === "read" ) {
- if ( model . __type__ == 'Dataset' ) {
- var base = self . get ( 'dataproxy_url' ); TODO: should we cache for extra efficiency
var data = {
- url : model . get ( 'url' )
- , 'max-results' : 1
- , type : model . get ( 'format' ) || 'csv'
- };
- var jqxhr = $ . ajax ({
- url : base
- , data : data
- , dataType : 'jsonp'
- });
- var dfd = $ . Deferred ();
- my . wrapInTimeout ( jqxhr ). done ( function ( results ) {
- model . fields . reset ( _ . map ( results . fields , function ( fieldId ) {
- return { id : fieldId };
- })
- );
- dfd . resolve ( model , jqxhr );
- })
- . fail ( function ( arguments ) {
- dfd . reject ( arguments );
- });
+ if ( model . __type__ == 'Dataset' ) { Do nothing as we will get fields in query step (and no metadata to
+retrieve)
var dfd = $ . Deferred ();
+ dfd . resolve ( model );
return dfd . promise ();
}
} else {
@@ -66,7 +47,14 @@
, dataType : 'jsonp'
});
var dfd = $ . Deferred ();
- jqxhr . done ( function ( results ) {
+ my . wrapInTimeout ( jqxhr ). done ( function ( results ) {
+ if ( results . error ) {
+ dfd . reject ( results . error );
+ }
+ dataset . fields . reset ( _ . map ( results . fields , function ( fieldId ) {
+ return { id : fieldId };
+ })
+ );
var _out = _ . map ( results . data , function ( doc ) {
var tmp = {};
_ . each ( results . fields , function ( key , idx ) {
@@ -75,6 +63,9 @@
return tmp ;
});
dfd . resolve ( _out );
+ })
+ . fail ( function ( arguments ) {
+ dfd . reject ( arguments );
});
return dfd . promise ();
}
diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html
index d576b1cf..0b91fa81 100644
--- a/docs/backend/elasticsearch.html
+++ b/docs/backend/elasticsearch.html
@@ -1,4 +1,4 @@
- elasticsearch.js
elasticsearch.js this . recline = this . recline || {};
+ elasticsearch.js
elasticsearch.js this . recline = this . recline || {};
this . recline . Backend = this . recline . Backend || {};
( function ( $ , my ) { ElasticSearch Backend
diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html
index d503f679..c058c6f5 100644
--- a/docs/backend/gdocs.html
+++ b/docs/backend/gdocs.html
@@ -1,4 +1,4 @@
- gdocs.js
gdocs.js this . recline = this . recline || {};
+ gdocs.js
gdocs.js this . recline = this . recline || {};
this . recline . Backend = this . recline . Backend || {};
( function ( $ , my ) { Google spreadsheet backend
@@ -15,18 +15,36 @@ var dataset = new recline.Model.Dataset({
'gdocs'
);
my . GDoc = Backbone . Model . extend ({
+ getUrl : function ( dataset ) {
+ var url = dataset . get ( 'url' );
+ if ( url . indexOf ( 'feeds/list' ) != - 1 ) {
+ return url ;
+ } else { https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0
var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/
+ var matches = url . match ( regex );
+ if ( matches ) {
+ var key = matches [ 1 ];
+ var worksheet = 1 ;
+ var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
+ return out ;
+ } else {
+ alert ( 'Failed to extract gdocs key from ' + url );
+ }
+ }
+ },
sync : function ( method , model , options ) {
var self = this ;
if ( method === "read" ) {
var dfd = $ . Deferred ();
var dataset = model ;
- $ . getJSON ( model . get ( 'url' ), function ( d ) {
+ var url = this . getUrl ( model );
+
+ $ . getJSON ( url , function ( d ) {
result = self . gdocsToJavascript ( d );
model . fields . reset ( _ . map ( result . field , function ( fieldId ) {
return { id : fieldId };
})
- ); cache data onto dataset (we have loaded whole gdoc it seems!)
model . _dataCache = result . data ;
+ ); cache data onto dataset (we have loaded whole gdoc it seems!)
model . _dataCache = result . data ;
dfd . resolve ( model );
})
return dfd . promise (); }
@@ -34,7 +52,7 @@ var dataset = new recline.Model.Dataset({
query : function ( dataset , queryObj ) {
var dfd = $ . Deferred ();
- var fields = _ . pluck ( dataset . fields . toJSON (), 'id' ); zip the fields with the data rows to produce js objs
+ var fields = _ . pluck ( dataset . fields . toJSON (), 'id' );
zip the fields with the data rows to produce js objs
TODO: factor this out as a common method with other backends
var objs = _ . map ( dataset . _dataCache , function ( d ) {
var obj = {};
_ . each ( _ . zip ( fields , d ), function ( x ) { obj [ x [ 0 ]] = x [ 1 ]; })
@@ -59,11 +77,11 @@ TODO: factor this out as a common method with other backends var results = {
'field' : [],
'data' : []
- }; default is no special info on type of columns
default is no special info on type of columns
var colTypes = {};
if ( options . colTypes ) {
colTypes = options . colTypes ;
- } either extract column headings from spreadsheet directly, or used supplied ones
if ( options . columnsToUse ) { columns set to subset supplied
results . field = options . columnsToUse ;
- } else { set columns to use to be all available
if ( gdocsSpreadsheet . feed . entry . length > 0 ) {
+ } either extract column headings from spreadsheet directly, or used supplied ones
if ( options . columnsToUse ) { columns set to subset supplied
results . field = options . columnsToUse ;
+ } else { set columns to use to be all available
if ( gdocsSpreadsheet . feed . entry . length > 0 ) {
for ( var k in gdocsSpreadsheet . feed . entry [ 0 ]) {
if ( k . substr ( 0 , 3 ) == 'gsx' ) {
var col = k . substr ( 4 )
@@ -71,13 +89,13 @@ TODO: factor this out as a common method with other backends }
}
}
- } converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])
var rep = /^([\d\.\-]+)\%$/ ;
+ } converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])
var rep = /^([\d\.\-]+)\%$/ ;
$ . each ( gdocsSpreadsheet . feed . entry , function ( i , entry ) {
var row = [];
for ( var k in results . field ) {
var col = results . field [ k ];
var _keyname = 'gsx$' + col ;
- var value = entry [ _keyname ][ '$t' ]; if labelled as % and value contains %, convert
if ( colTypes [ col ] == 'percent' ) {
+ var value = entry [ _keyname ][ '$t' ]; if labelled as % and value contains %, convert
if ( colTypes [ col ] == 'percent' ) {
if ( rep . test ( value )) {
var value2 = rep . exec ( value );
var value3 = parseFloat ( value2 );
diff --git a/docs/backend/memory.html b/docs/backend/memory.html
index 982be977..8b9fe834 100644
--- a/docs/backend/memory.html
+++ b/docs/backend/memory.html
@@ -1,4 +1,4 @@
- memory.js
memory.js this . recline = this . recline || {};
+ memory.js
memory.js this . recline = this . recline || {};
this . recline . Backend = this . recline . Backend || {};
( function ( $ , my ) { Memory Backend - uses in-memory data
diff --git a/docs/backend/webstore.html b/docs/backend/webstore.html
deleted file mode 100644
index aae1bbc5..00000000
--- a/docs/backend/webstore.html
+++ /dev/null
@@ -1,61 +0,0 @@
- webstore.js
webstore.js this . recline = this . recline || {};
-this . recline . Backend = this . recline . Backend || {};
-
-( function ( $ , my ) { Webstore Backend
-
-Connecting to Webstores
-
-To use this backend ensure your Dataset has a webstore_url in its attributes.
my . Webstore = Backbone . Model . extend ({
- sync : function ( method , model , options ) {
- if ( method === "read" ) {
- if ( model . __type__ == 'Dataset' ) {
- var base = model . get ( 'webstore_url' );
- var schemaUrl = base + '/schema.json' ;
- var jqxhr = $ . ajax ({
- url : schemaUrl ,
- dataType : 'jsonp' ,
- jsonp : '_callback'
- });
- var dfd = $ . Deferred ();
- my . wrapInTimeout ( jqxhr ). done ( function ( schema ) {
- var fieldData = _ . map ( schema . data , function ( item ) {
- item . id = item . name ;
- delete item . name ;
- return item ;
- });
- model . fields . reset ( fieldData );
- model . docCount = schema . count ;
- dfd . resolve ( model , jqxhr );
- })
- . fail ( function ( arguments ) {
- dfd . reject ( arguments );
- });
- return dfd . promise ();
- }
- }
- },
- query : function ( model , queryObj ) {
- var base = model . get ( 'webstore_url' );
- var data = {
- _limit : queryObj . size
- , _offset : queryObj . from
- };
- var jqxhr = $ . ajax ({
- url : base + '.json' ,
- data : data ,
- dataType : 'jsonp' ,
- jsonp : '_callback' ,
- cache : true
- });
- var dfd = $ . Deferred ();
- jqxhr . done ( function ( results ) {
- dfd . resolve ( results . data );
- });
- return dfd . promise ();
- }
- });
- recline . Model . backends [ 'webstore' ] = new my . Webstore ();
-
-}( jQuery , this . recline . Backend ));
-
-
\ No newline at end of file
diff --git a/docs/model.html b/docs/model.html
index 053d7577..ae7bcb5c 100644
--- a/docs/model.html
+++ b/docs/model.html
@@ -36,7 +36,7 @@ updated by queryObj (if provided).
also returned. query : function ( queryObj ) {
this . trigger ( 'query:start' );
var self = this ;
- this . queryState . set ( queryObj , { silent : true });
+ this . queryState . set ( queryObj );
var dfd = $ . Deferred ();
this . backend . query ( this , this . queryState . toJSON ()). done ( function ( rows ) {
var docs = _ . map ( rows , function ( row ) {
diff --git a/docs/view-flot-graph.html b/docs/view-flot-graph.html
index dac85e8a..cd3d6080 100644
--- a/docs/view-flot-graph.html
+++ b/docs/view-flot-graph.html
@@ -36,7 +36,10 @@ generate the element itself (you can then append view.el to the DOM.
<label>Graph Type</label> \
<div class="input editor-type"> \
<select> \
- <option value="line">Line</option> \
+ <option value="lines-and-points">Lines and Points</option> \
+ <option value="lines">Lines</option> \
+ <option value="points">Points</option> \
+ <option value="bars">Bars</option> \
</select> \
</div> \
<label>Group Column (x-axis)</label> \
@@ -95,7 +98,7 @@ generate the element itself (you can then append view.el to the DOM.
this . chartConfig = _ . extend ({
group : null ,
series : [],
- graphType : 'line'
+ graphType : 'lines-and-points'
},
configFromHash ,
config
@@ -113,9 +116,14 @@ could be simpler just to have a common template!
onEditorSubmit : function ( e ) {
var select = this . el . find ( '.editor-group select' );
- this . _getEditorData (); update navigation
-TODO: make this less invasive (e.g. preserve other keys in query string)
var qs = my . parseHashQueryString ();
- qs [ 'graph' ] = this . chartConfig ;
+ $editor = this ;
+ var series = this . $series . map ( function () {
+ return $ ( this ). val ();
+ });
+ this . chartConfig . series = $ . makeArray ( series )
+ this . chartConfig . group = this . el . find ( '.editor-group select' ). val ();
+ this . chartConfig . graphType = this . el . find ( '.editor-type select' ). val (); update navigation
var qs = my . parseHashQueryString ();
+ qs [ 'graph' ] = JSON . stringify ( this . chartConfig );
my . setHashQueryString ( qs );
this . redraw ();
},
@@ -128,25 +136,98 @@ TODO: make this less invasive (e.g. preserve other keys in query string)
var areWeVisible = ! jQuery . expr . filters . hidden ( this . el [ 0 ]);
if (( ! areWeVisible || this . model . currentDocuments . length == 0 )) {
return
- } create this.plot and cache it
only lines for the present
options = {
- id : 'line' ,
- name : 'Line Chart'
- };
- this . plot = $ . plot ( this . $graph , this . createSeries (), options );
- }
- this . plot . setData ( this . createSeries ());
- this . plot . resize ();
- this . plot . setupGrid ();
- this . plot . draw ();
+ }
+ var series = this . createSeries ();
+ var options = this . graphOptions [ this . chartConfig . graphType ];
+ this . plot = $ . plot ( this . $graph , series , options );
+ if ( this . chartConfig . graphType in { 'points' : '' , 'lines-and-points' : '' }) {
+ this . setupTooltips ();
+ } create this.plot and cache it
+ if (!this.plot) {
+ this.plot = $.plot(this.$graph, series, options);
+ } else {
+ this.plot.parseOptions(options);
+ this.plot.setData(this.createSeries());
+ this.plot.resize();
+ this.plot.setupGrid();
+ this.plot.draw();
+ }
},
+
+ graphOptions : {
+ lines : {
+ series : {
+ lines : { show : true }
+ }
+ }
+ , points : {
+ series : {
+ points : { show : true }
+ },
+ grid : { hoverable : true , clickable : true }
+ }
+ , 'lines-and-points' : {
+ series : {
+ points : { show : true },
+ lines : { show : true }
+ },
+ grid : { hoverable : true , clickable : true }
+ }
+ , bars : {
+ series : {
+ lines : { show : false },
+ bars : {
+ show : true ,
+ barWidth : 1 ,
+ align : "left" ,
+ fill : true
+ }
+ },
+ xaxis : {
+ tickSize : 1 ,
+ tickLength : 1 ,
+ }
+ }
},
- _getEditorData : function () {
- $editor = this
- var series = this . $series . map ( function () {
- return $ ( this ). val ();
+ setupTooltips : function () {
+ var self = this ;
+ function showTooltip ( x , y , contents ) {
+ $ ( '<div id="flot-tooltip">' + contents + '</div>' ). css ( {
+ position : 'absolute' ,
+ display : 'none' ,
+ top : y + 5 ,
+ left : x + 5 ,
+ border : '1px solid #fdd' ,
+ padding : '2px' ,
+ 'background-color' : '#fee' ,
+ opacity : 0.80
+ }). appendTo ( "body" ). fadeIn ( 200 );
+ }
+
+ var previousPoint = null ;
+ this . $graph . bind ( "plothover" , function ( event , pos , item ) {
+ if ( item ) {
+ if ( previousPoint != item . dataIndex ) {
+ previousPoint = item . dataIndex ;
+
+ $ ( "#flot-tooltip" ). remove ();
+ var x = item . datapoint [ 0 ]. toFixed ( 2 ),
+ y = item . datapoint [ 1 ]. toFixed ( 2 );
+
+ var content = _ . template ( '<%= group %> = <%= x %>, <%= series %> = <%= y %>' , {
+ group : self . chartConfig . group ,
+ x : x ,
+ series : item . series . label ,
+ y : y
+ });
+ showTooltip ( item . pageX , item . pageY , content );
+ }
+ }
+ else {
+ $ ( "#flot-tooltip" ). remove ();
+ previousPoint = null ;
+ }
});
- this . chartConfig . series = $ . makeArray ( series )
- this . chartConfig . group = this . el . find ( '.editor-group select' ). val ();
},
createSeries : function () {
@@ -167,7 +248,7 @@ TODO: make this less invasive (e.g. preserve other keys in query string)
});
}
return series ;
- }, Public: Adds a new empty series select box to the editor.
+ }, Public: Adds a new empty series select box to the editor.
All but the first select box will have a remove button that allows them
to be removed.
@@ -183,7 +264,7 @@ to be removed.
label . append ( ' [<a href="#remove" class="action-remove-series">Remove</a>]' );
label . find ( 'span' ). text ( String . fromCharCode ( this . $series . length + 64 ));
return this ;
- }, Public: Removes a series list item from the editor.
+ }, Public: Removes a series list item from the editor.
Also updates the labels of the remaining series elements.
removeSeries : function ( e ) {
e . preventDefault ();
@@ -201,7 +282,7 @@ to be removed.
toggleHelp : function () {
this . el . find ( '.editor-info' ). toggleClass ( 'editor-hide-info' );
- }, Private: Resets the series property to reference the select elements.
+ }, Private: Resets the series property to reference the select elements.
Returns itself.
_updateSeries : function () {
this . $series = this . el . find ( '.editor-series select' );
diff --git a/docs/view-grid.html b/docs/view-grid.html
index c4c935d5..f5472749 100644
--- a/docs/view-grid.html
+++ b/docs/view-grid.html
@@ -44,19 +44,19 @@ showDialog: function(template, data) {
}, ======================================================
Column and row menus
onColumnHeaderClick : function ( e ) {
this . state . currentColumn = $ ( e . target ). closest ( '.column-header' ). attr ( 'data-field' );
- util . position ( 'data-table-menu' , e );
- util . render ( 'columnActions' , 'data-table-menu' );
},
onRowHeaderClick : function ( e ) {
this . state . currentRow = $ ( e . target ). parents ( 'tr:first' ). attr ( 'data-id' );
- util . position ( 'data-table-menu' , e );
- util . render ( 'rowActions' , 'data-table-menu' );
},
onRootHeaderClick : function ( e ) {
- util . position ( 'data-table-menu' , e );
- util . render ( 'rootActions' , 'data-table-menu' , { 'columns' : this . hiddenFields });
+ var tmpl = ' \
+ {{#columns}} \
+ <li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
+ {{/columns}}' ;
+ var tmp = $ . mustache ( tmpl , { 'columns' : this . hiddenFields });
+ this . el . find ( '.root-header-menu .dropdown-menu' ). html ( tmp );
},
onMenuClick : function ( e ) {
@@ -90,7 +90,6 @@ from DOM) while id may be int })
}
}
- util . hide ( 'data-table-menu' );
actions [ $ ( e . target ). attr ( 'data-action' )]();
},
@@ -141,26 +140,32 @@ from DOM) while id may be int }, ======================================================
Templating template : ' \
- <div class="data-table-menu-overlay" style="display: none; z-index: 101; "> </div> \
- <ul class="data-table-menu"></ul> \
- <table class="data-table" cellspacing="0"> \
+ <table class="data-table table-striped table-condensed" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}} \
<th class="column-header"> \
- <div class="column-header-title"> \
- <a class="root-header-menu"></a> \
- <span class="column-header-name"></span> \
+ <div class="btn-group root-header-menu"> \
+ <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
+ <ul class="dropdown-menu data-table-menu"> \
+ </ul> \
</div> \
+ <span class="column-header-name"></span> \
</th> \
{{/notEmpty}} \
{{#fields}} \
<th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \
- <div class="column-header-title"> \
- <a class="column-header-menu"></a> \
- <span class="column-header-name">{{label}}</span> \
- </div> \
+ <div class="btn-group column-header-menu"> \
+ <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
+ <ul class="dropdown-menu data-table-menu"> \
+ <li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
+ <li class="write-op"><a data-action="deleteColumn" href="JavaScript:void(0);">Delete this column</a></li> \
+ <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
+ <li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
+ <li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
+ </ul> \
</div> \
+ <span class="column-header-name">{{label}}</span> \
</th> \
{{/fields}} \
</tr> \
@@ -239,7 +244,14 @@ var row = new DataGridRow({
},
template : ' \
- <td><a class="row-header-menu"></a></td> \
+ <td> \
+ <div class="btn-group row-header-menu"> \
+ <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
+ <ul class="dropdown-menu data-table-menu"> \
+ <li class="write-op"><a data-action="deleteRow" href="JavaScript:void(0);">Delete this row</a></li> \
+ </ul> \
+ </div> \
+ </td> \
{{#cells}} \
<td data-field="{{field}}"> \
<div class="data-table-cell-content"> \
diff --git a/docs/view.html b/docs/view.html
index 28474dd2..8e0c77f3 100644
--- a/docs/view.html
+++ b/docs/view.html
@@ -100,22 +100,40 @@ FlotGraph subview.
this . router = new Backbone . Router ();
this . setupRouting ();
- this . model . bind ( 'query:start' , function ( eventName ) {
+ this . model . bind ( 'query:start' , function () {
my . notify ( 'Loading data' , { loader : true });
});
- this . model . bind ( 'query:done' , function ( eventName ) {
+ this . model . bind ( 'query:done' , function () {
my . clearNotifications ();
self . el . find ( '.doc-count' ). text ( self . model . docCount || 'Unknown' );
- my . notify ( 'Data loaded' , { category : 'success' });
+ my . notify ( 'Data loaded' , { category : 'success' }); update navigation
var qs = my . parseHashQueryString ();
+ qs [ 'reclineQuery' ] = JSON . stringify ( self . model . queryState . toJSON ());
+ my . setHashQueryString ( qs );
});
- this . model . bind ( 'query:fail' , function ( eventName , error ) {
+ this . model . bind ( 'query:fail' , function ( error ) {
my . clearNotifications ();
- my . notify ( error . message , { category : 'error' , persist : true });
- }); retrieve basic data like fields etc
+ var msg = '' ;
+ if ( typeof ( error ) == 'string' ) {
+ msg = error ;
+ } else if ( typeof ( error ) == 'object' ) {
+ if ( error . title ) {
+ msg = error . title + ': ' ;
+ }
+ if ( error . message ) {
+ msg += error . message ;
+ }
+ } else {
+ msg = 'There was an error querying the backend' ;
+ }
+ my . notify ( msg , { category : 'error' , persist : true });
+ });
retrieve basic data like fields etc
note this.model and dataset returned are the same
this . model . fetch ()
. done ( function ( dataset ) {
- self . el . find ( '.doc-count' ). text ( self . model . docCount || 'Unknown' );
- self . model . query ();
+ var queryState = my . parseHashQueryString (). reclineQuery ;
+ if ( queryState ) {
+ queryState = JSON . parse ( queryState );
+ }
+ self . model . query ( queryState );
})
. fail ( function ( error ) {
my . notify ( error . message , { category : 'error' , persist : true });
@@ -143,7 +161,7 @@ note this.model and dataset returned are the same
},
setupRouting : function () {
- var self = this ; Default route
this . router . route ( '' , this . pageViews [ 0 ]. id , function () {
+ var self = this ; Default route
this . router . route ( '' , this . pageViews [ 0 ]. id , function () {
self . updateNav ( self . pageViews [ 0 ]. id );
});
$ . each ( this . pageViews , function ( idx , view ) {
@@ -155,8 +173,10 @@ note this.model and dataset returned are the same
updateNav : function ( pageName , queryString ) {
this . el . find ( '.navigation li' ). removeClass ( 'active' );
+ this . el . find ( '.navigation li a' ). removeClass ( 'disabled' );
var $el = this . el . find ( '.navigation li a[href=#' + pageName + ']' );
- $el . parent (). addClass ( 'active' ); show the specific page
_ . each ( this . pageViews , function ( view , idx ) {
+ $el . parent (). addClass ( 'active' );
+ $el . addClass ( 'disabled' ); show the specific page
_ . each ( this . pageViews , function ( view , idx ) {
if ( view . id === pageName ) {
view . view . el . show ();
} else {
@@ -171,21 +191,31 @@ note this.model and dataset returned are the same
className : 'recline-query-editor' ,
template : ' \
<form action="" method="GET" class="form-inline"> \
- <input type="text" name="q" value="{{q}}" class="text-query" /> \
+ <div class="input-prepend text-query"> \
+ <span class="add-on"><i class="icon-search"></i></span> \
+ <input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
+ <div class="btn-group menu"> \
+ <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
+ <ul class="dropdown-menu"> \
+ <li><a data-action="size" href="">Number of items to show ({{size}})</a></li> \
+ <li><a data-action="from" href="">Show from ({{from}})</a></li> \
+ </ul> \
+ </div> \
+ </div> \
<div class="pagination"> \
<ul> \
- <li class="prev action-pagination-update"><a>« back</a></li> \
- <li class="active"><a><input name="from" type="text" value="{{from}}" /> – <input name="to" type="text" value="{{to}}" /> </a></li> \
- <li class="next action-pagination-update"><a>next »</a></li> \
+ <li class="prev action-pagination-update"><a href="">«</a></li> \
+ <li class="active"><a>{{from}} – {{to}}</a></li> \
+ <li class="next action-pagination-update"><a href="">»</a></li> \
</ul> \
</div> \
- <button type="submit" class="btn" style="">Update »</button> \
</form> \
' ,
events : {
- 'submit form' : 'onFormSubmit' ,
- 'click .action-pagination-update' : 'onPaginationUpdate'
+ 'submit form' : 'onFormSubmit'
+ , 'click .action-pagination-update' : 'onPaginationUpdate'
+ , 'click .menu li a' : 'onMenuItemClick'
},
initialize : function () {
@@ -196,10 +226,8 @@ note this.model and dataset returned are the same
},
onFormSubmit : function ( e ) {
e . preventDefault ();
- var newFrom = parseInt ( this . el . find ( 'input[name="from"]' ). val ());
- var newSize = parseInt ( this . el . find ( 'input[name="to"]' ). val ()) - newFrom ;
- var query = this . el . find ( '.text-query' ). val ();
- this . model . set ({ size : newSize , from : newFrom , q : query });
+ var query = this . el . find ( '.text-query input' ). val ();
+ this . model . set ({ q : query });
},
onPaginationUpdate : function ( e ) {
e . preventDefault ();
@@ -211,6 +239,20 @@ note this.model and dataset returned are the same
}
this . model . set ({ from : newFrom });
},
+ onMenuItemClick : function ( e ) {
+ e . preventDefault ();
+ var attrName = $ ( e . target ). attr ( 'data-action' );
+ var msg = _ . template ( 'New value (<%= value %>)' ,
+ { value : this . model . get ( attrName )}
+ );
+ var newValue = prompt ( msg );
+ if ( newValue ) {
+ newValue = parseInt ( newValue );
+ var update = {};
+ update [ attrName ] = newValue ;
+ this . model . set ( update );
+ }
+ },
render : function () {
var tmplData = this . model . toJSON ();
tmplData . to = this . model . get ( 'from' ) + this . model . get ( 'size' );
@@ -220,7 +262,7 @@ note this.model and dataset returned are the same
});
-/* ========================================================== */ Miscellaneous Utilities var urlPathRegex = /^([^?]+)(\?.*)?/ ; Parse the Hash section of a URL into path and query string
my . parseHashUrl = function ( hashUrl ) {
+/* ========================================================== */ Miscellaneous Utilities var urlPathRegex = /^([^?]+)(\?.*)?/ ; Parse the Hash section of a URL into path and query string
my . parseHashUrl = function ( hashUrl ) {
var parsed = urlPathRegex . exec ( hashUrl );
if ( parsed == null ) {
return {};
@@ -230,7 +272,10 @@ note this.model and dataset returned are the same
query : parsed [ 2 ] || ''
}
}
-} Parse a URL query string (?xyz=abc...) into a dictionary.
my . parseQueryString = function ( q ) {
+} Parse a URL query string (?xyz=abc...) into a dictionary.
my . parseQueryString = function ( q ) {
+ if ( ! q ) {
+ return {};
+ }
var urlParams = {},
e , d = function ( s ) {
return unescape ( s . replace ( /\+/g , " " ));
@@ -240,17 +285,17 @@ note this.model and dataset returned are the same
if ( q && q . length && q [ 0 ] === '?' ) {
q = q . slice ( 1 );
}
- while ( e = r . exec ( q )) { TODO: have values be array as query string allow repetition of keys
urlParams [ d ( e [ 1 ])] = d ( e [ 2 ]);
+ while ( e = r . exec ( q )) { TODO: have values be array as query string allow repetition of keys
urlParams [ d ( e [ 1 ])] = d ( e [ 2 ]);
}
return urlParams ;
-} Parse the query string out of the URL hash
my . parseHashQueryString = function () {
+} Parse the query string out of the URL hash
my . parseHashQueryString = function () {
q = my . parseHashUrl ( window . location . hash ). query ;
return my . parseQueryString ( q );
-} Compse a Query String
my . composeQueryString = function ( queryParams ) {
+} Compse a Query String
my . composeQueryString = function ( queryParams ) {
var queryString = '?' ;
var items = [];
$ . each ( queryParams , function ( key , value ) {
- items . push ( key + '=' + JSON . stringify ( value ));
+ items . push ( key + '=' + value );
});
queryString += items . join ( '&' );
return queryString ;
@@ -258,7 +303,7 @@ note this.model and dataset returned are the same
my . setHashQueryString = function ( queryParams ) {
window . location . hash = window . location . hash . split ( '?' )[ 0 ] + my . composeQueryString ( queryParams );
-} notify
+} notify
Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are:
@@ -277,7 +322,7 @@ note this.model and dataset returned are the same
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
{{msg}} \
{{#loader}} \
- <img src="images/small-spinner.gif" class="notification-loader"> \
+ <span class="notification-loader"> </span> \
{{/loader}} \
</div>' ;
var _templated = $ . mustache ( _template , tmplData );
@@ -289,7 +334,7 @@ note this.model and dataset returned are the same
});
}, 1000 );
}
-} clearNotifications
+} clearNotifications
Clear all existing notifications
my . clearNotifications = function () {
var $notifications = $ ( '.data-explorer .alert-messages .alert' );
diff --git a/index.html b/index.html
index 17ed7698..699c9540 100644
--- a/index.html
+++ b/index.html
@@ -69,14 +69,14 @@
Recline combines a data grid, Google Refine-style data transforms
and visualizations all in lightweight javascript and html.
Designed for standalone use or as a library to integrate into your own
- app. Recline utilizes the lightweight but powerful Backbone framework, and
- so is a cinch to extend or adapt.
+ app. Recline builds on the powerful but lightweight Backbone framework
+ making it extremely easy to extend and adapt.
Main Features
@@ -113,27 +113,7 @@
CSS : the demo utilizes bootstrap but you can integrate with your own HTML and CSS. Data Explorer specific CSS can be found here in the repo: https://github.com/okfn/recline/tree/master/css .
Documentation
- Recline has a simple structure layered on top of the basic Model/View
- distinction inherent in Backbone. There are the following two main domain objects (all Backbone Models):
-
- Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.
- Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).
-
- More on the models in the Model source docs .
-
- Backends (more info below ) connect Dataset and Documents to data
- from a specific 'Backend' data source. They provide methods for loading and
- saving Datasets and individuals Documents as well as for bulk loading via a
- query API and doing bulk transforms on the backend.
-
- Complementing the model are various Views (you can easily write your own). Each view holds a pointer to a Dataset:
-
- DataExplorer: the parent view which manages the overall app and sets up sub views.
- DataGrid: the data grid view.
- FlotGraph: a simple graphing view using Flot .
-
-
- Using It
+ Quickstart
// Note: you should have included the relevant JS libraries (and CSS)
// See above for dependencies
@@ -158,6 +138,29 @@ Backbone.history.start();
href="demo/">Demo -- just hit view source (NB: the javascript for the
demo is in: app.js ).
+ Architecture and Model
+ Recline has a simple structure layered on top of the basic Model/View
+ distinction inherent in Backbone. There are the following two main domain objects (both Backbone Models):
+
+ Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.
+ Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).
+
+
More detail of how these work can be found in the Model source docs .
+
+
Backends connect Dataset and Documents to data
+ from a specific 'Backend' data source. They provide methods for loading and
+ saving Datasets and individuals Documents as well as for bulk loading via a
+ query API and doing bulk transforms on the backend. More info on backends can be found below .
+
+
Complementing the model are various Views (you can easily write your own). Each view holds a pointer to a Dataset:
+
+ DataExplorer: the parent view which manages the overall app and sets up sub views.
+ DataGrid: the data grid view.
+ FlotGraph: a simple graphing view using Flot .
+
+
+
Backends
Backends are connectors to backend data sources from which data can be retrieved.
@@ -197,10 +200,11 @@ used by the Dataset.query method to search the backend for documents,
retrieving the results in bulk. This method should also set the docCount
attribute on the dataset.
-
queryObj should be either a recline.Model.Query object or a
+
queryObj should be either a recline.Model.Query object or a
Hash. The structure of data in the Query object or Hash should follow that
-defined in issue 34. (That said, if you are writing your own backend and have
-control over the query object you can obviously use whatever structure you
+defined in issue 34 . (Of
+course, if you are writing your own backend, and hence have control over the
+interpretation of the query object, you can use whatever structure you
like).
Source Docs (via Docco)
@@ -211,6 +215,7 @@ like).
Graph View (based on Flot)
Backend: Memory (local data)
Backend: ElasticSearch
+
Backend: DataProxy
Backend: Google Docs (Spreadsheet)
diff --git a/make b/make
index 02a3d5b9..4d60070a 100755
--- a/make
+++ b/make
@@ -1,5 +1,6 @@
#!/usr/bin/env python
import sys
+import shutil
import os
def cat():
@@ -12,6 +13,8 @@ def docs():
print("** Building docs")
cmd = 'docco src/model.js src/view.js src/view-grid.js src/view-flot-graph.js'
os.system(cmd)
+ if os.path.exists('/tmp/recline-docs'):
+ shutil.rmtree('/tmp/recline-docs')
os.makedirs('/tmp/recline-docs')
os.system('mkdir -p docs/backend')
files = '%s/src/backend/*.js' % os.getcwd()
diff --git a/recline.js b/recline.js
index 0add56e4..b52cb93e 100644
--- a/recline.js
+++ b/recline.js
@@ -166,7 +166,7 @@ my.Dataset = Backbone.Model.extend({
query: function(queryObj) {
this.trigger('query:start');
var self = this;
- this.queryState.set(queryObj, {silent: true});
+ this.queryState.set(queryObj);
var dfd = $.Deferred();
this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
var docs = _.map(rows, function(row) {
@@ -255,18 +255,6 @@ my.backends = {};
var util = function() {
var templates = {
transformActions: '
'
- , columnActions: ' \
-
\
-
\
-
\
-
\
-
\
- '
- , rowActions: '
'
- , rootActions: ' \
- {{#columns}} \
-
\
- {{/columns}}'
, cellEditor: ' \