commit/galaxy-central: carlfeberhard: scatterplot.js: fixes after testing; utils/LazyDataLoader.js: basic lazy ajax loading object;
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/changeset/d9e8a7eee7aa/ changeset: d9e8a7eee7aa user: carlfeberhard date: 2012-10-11 23:25:51 summary: scatterplot.js: fixes after testing; utils/LazyDataLoader.js: basic lazy ajax loading object; affected #: 3 files diff -r 419d87dd65149285d6c9f0043ea898b365d2dbde -r d9e8a7eee7aac7e0ed15fce27592eb0182144889 static/scripts/utils/LazyDataLoader.js --- /dev/null +++ b/static/scripts/utils/LazyDataLoader.js @@ -0,0 +1,183 @@ +/* +TODO: + ?? superclass dataloader, subclass lazydataloader?? + +*/ +//============================================================================== +/** + * Object to progressively load JSON data from a REST url, delaying some time between loading chunks + * + * Data from ajax loading is aggregated in a list, with one element for each ajax response. + * It's up to the calling code to combine the results in a meaningful, correct way + * + * example: + * var loader = new scatterplot.LazyDataLoader({ + * //logger : console, + * url : ( apiDatasetsURL + '/' + hda.id + '?data_type=raw_data' + * + '&columns=[10,14]' ), + * total : hda.metadata_data_lines, + * size : 500, + * + * initialize : function( config ){ + * // ... do some stuff + * }, + * + * buildUrl : function( start, size ){ + * // change the formation of start, size in query string + * return loader.url + '&' + jQuery.param({ + * start_val: start, + * max_vals: size + * }); + * }, + * + * loadedPartialEvent : 'loader.partial', + * loadedAllEvent : 'loader.all', + * }); + * $( loader ).bind( 'loader.partial', function( event, data ){ + * console.info( 'partial load complete:', event, data ); + * // ... do stuff with new data + * }); + * $( loader ).bind( 'loader.all', function( event, data ){ + * console.info( 'final load complete:', event, data ); + * // ... do stuff with all data + * }); + * + * loader.load( function( dataArray ){ console.debug( 'FINISHED!', x, y, z ); } ); + */ +function LazyDataLoader( config ){ + // for now assume: + // get, async, and params sent via url query string + // we want json + // we know the size of the data on the server beforehand + var loader = this; + + jQuery.extend( loader, LoggableMixin ); + jQuery.extend( loader, { + + //NOTE: the next two need to be sent in config (required) + // total size of data on server + total : undefined, + // url of service to get the data + url : undefined, + + // holds the interval id for the current load delay + currentIntervalId : undefined, + + // optional events to trigger when partial, last data are loaded + // loadedPartialEvent will be sent: the ajax response data, start value, and size + loadedPartialEvent : undefined, + // loadedAllEvent will be sent: the final loader's data array and the total + loadedAllEvent : undefined, + + // each load call will add an element to this array + // it's the responsibility of the code using this to combine them properly + data : [], + // ms btwn recursive loads + delay : 500, + // starting line, element, whatever + start : 0, + // size to fetch per load + size : 1000, + + // loader init func: extends loader with config and calls config.init if there + //@param {object} config : object containing variables to override (or additional) + initialize : function( config ){ + jQuery.extend( loader, config ); + + // call the custom initialize function if any + // only dangerous if the user tries LazyDataLoader.prototype.init + if( config.hasOwnProperty( 'initialize' ) ){ + config.initialize.call( loader, config ); + } + + // ensure necessary stuff + if( !loader.total ){ throw( loader + ' requires a total (total size of the data)' ); } + if( !loader.url ){ throw( loader + ' requires a url' ); } + + this.log( this + ' initialized:', loader ); + }, + + // returns query string formatted start and size (for the next fetch) appended to the loader.url + //OVERRIDE: to change how params are passed, param names, etc. + //@param {int} start : the line/row/datum indicating where in the dataset the next load begins + //@param {int} size : the number of lines/rows/data to get on the next load + buildUrl : function( start, size ){ + // currently VERY SPECIFIC to using data_providers.py start_val, max_vals params + return loader.url + '&' + jQuery.param({ + start_val: start, + max_vals: size + }); + }, + + //OVERRIDE: to handle ajax errors differently + ajaxErrorFn : function( xhr, status, error ){ + alert( loader + ' ERROR:' + status + '\n' + error ); + }, + + // interface to begin load (and first recursive call) + //@param {Function} callback : function to execute when all data is loaded. callback is passed loader.data + load : function( callback ){ + + // subsequent recursive calls + function loadHelper( start, size ){ + loader.log( loader + '.loadHelper, start:', start, 'size:', size ); + var url = loader.buildUrl( start, size ); + loader.log( '\t url:', url ); + + jQuery.ajax({ + url : loader.buildUrl( start, size ), + dataType : 'json', + error : function( xhr, status, error ){ + loader.log( '\t ajax error, status:', status, 'error:', error ); + if( loader.currentIntervalId ){ + clearInterval( loader.currentIntervalId ); + } + loader.ajaxErrorFn( xhr, status, error ); + }, + + success : function( response ){ + var next = start + size, + remainder = Math.min( loader.total - next, loader.size ); + loader.log( '\t ajax success, next:', next, 'remainder:', remainder ); + + // store the response as is in a new element + //TODO:?? store start, size as well? + loader.data.push( response ); + + // fire the partial load event + if( loader.loadedPartialEvent ){ + loader.log( '\t firing:', loader.loadedPartialEvent ); + $( loader ).trigger( loader.loadedPartialEvent, response, start, size ); + } + + // if we haven't gotten everything yet, + // set up for next recursive call and set the timer + if( remainder > 0 ){ + loader.currentIntervalId = setTimeout( + function(){ loadHelper( next, remainder ); }, + loader.delay + ); + loader.log( '\t currentIntervalId:', loader.currentIntervalId ); + + // otherwise (base-case), don't do anything + } else { + loader.log( loader + '.loadHelper, has finished:', loader.data ); + if( loader.loadedAllEvent ){ + loader.log( '\t firing:', loader.loadedAllEvent ); + $( loader ).trigger( loader.loadedAllEvent, loader.data, loader.total ); + } + if( callback ){ callback( loader.data ); } + } + } + }); + } + loadHelper( loader.start, Math.min( loader.total, loader.size ) ); + }, + + toString : function(){ return 'LazyDataLoader'; } + }); + + loader.initialize( config ); + return loader; +} + diff -r 419d87dd65149285d6c9f0043ea898b365d2dbde -r d9e8a7eee7aac7e0ed15fce27592eb0182144889 static/scripts/viz/scatterplot.js --- a/static/scripts/viz/scatterplot.js +++ b/static/scripts/viz/scatterplot.js @@ -1,6 +1,8 @@ define([ "../libs/underscore", + "../mvc/base-mvc", + "../utils/LazyDataLoader", "../templates/compiled/template-visualization-scatterplotControlForm", "../templates/compiled/template-visualization-statsTable", "../templates/compiled/template-visualization-chartSettings", @@ -14,7 +16,7 @@ /* ============================================================================= todo: outside this: - BUG: visualization menu doesn't disappear + BUG: setting width, height in plot controls doesn't re-interpolate data locations!! BUG?: get metadata_column_names (from datatype if necessary) BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations' @@ -78,7 +80,7 @@ PADDING = 8, X_LABEL_TOO_LONG_AT = 5; - //this.debugging = true; + this.debugging = true; this.log = function(){ if( this.debugging && console && console.debug ){ var args = Array.prototype.slice.call( arguments ); @@ -102,8 +104,8 @@ yNumTicks : 10, xAxisLabelBumpY : 40, yAxisLabelBumpX : -35, - width : 320, - height : 320, + width : 500, + height : 500, //TODO: anyway to make this a sub-obj? marginTop : 50, marginRight : 50, @@ -120,7 +122,7 @@ }; this.config = _.extend( {}, this.defaults, config ); - this.updateConfig = function( newConfig ){ + this.updateConfig = function( newConfig, rerender ){ _.extend( this.config, newConfig ); }; @@ -148,9 +150,10 @@ this.yAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' ); this.yAxisLabel = this.yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' ); - this.log( 'built svg:', d3.selectAll( 'svg' ) ); + //this.log( 'built svg:', d3.selectAll( 'svg' ) ); this.adjustChartDimensions = function( top, right, bottom, left ){ + //this.log( this + '.adjustChartDimensions', arguments ); top = top || 0; right = right || 0; bottom = bottom || 0; @@ -171,6 +174,7 @@ // ........................................................ data and scales this.preprocessData = function( data, min, max ){ + //this.log( this + '.preprocessData', arguments ); //TODO: filter by min, max if set // set a cap on the data, limit to first n points @@ -178,7 +182,7 @@ }; this.setUpDomains = function( xCol, yCol, meta ){ - this.log( 'setUpDomains' ); + //this.log( this + '.setUpDomains', arguments ); // configuration takes priority, otherwise meta (from the server) if passed, last-resort: compute it here this.xMin = this.config.xMin || ( meta )?( meta[0].min ):( d3.min( xCol ) ); this.xMax = this.config.xMax || ( meta )?( meta[0].max ):( d3.max( xCol ) ); @@ -187,6 +191,7 @@ }; this.setUpScales = function(){ + //this.log( this + '.setUpScales', arguments ); // Interpolation for x, y based on data domains this.xScale = d3.scale.linear() .domain([ this.xMin, this.xMax ]) @@ -198,6 +203,7 @@ // ........................................................ axis and ticks this.setUpXAxis = function(){ + //this.log( this + '.setUpXAxis', arguments ); // origin: bottom, left //TODO: incoporate top, right this.xAxisFn = d3.svg.axis() @@ -228,13 +234,14 @@ }; this.setUpYAxis = function(){ + //this.log( this + '.setUpYAxis', arguments ); this.yAxisFn = d3.svg.axis() .scale( this.yScale ) .ticks( this.config.yNumTicks ) .orient( 'left' ); this.yAxis// = content.select( 'g#y-axis' ) .call( this.yAxisFn ); - this.log( 'yAxis:', this.yAxis ); + //this.log( 'yAxis:', this.yAxis ); // get the tick labels for the y axis var yTickLabels = this.yAxis.selectAll( 'text' ).filter( function( e, i ){ return i !== 0; } ); @@ -259,7 +266,7 @@ if( this.config.marginLeft < neededY ){ var adjusting = ( neededY ) - this.config.marginLeft; adjusting = ( adjusting < 0 )?( 0 ):( adjusting ); - this.log( 'adjusting:', adjusting ); + //this.log( 'adjusting:', adjusting ); // update dimensions, translations this.adjustChartDimensions( 0, 0, 0, adjusting ); @@ -278,6 +285,7 @@ // ........................................................ grid lines this.renderGrid = function(){ + //this.log( this + '.renderGrid', arguments ); // VERTICAL // select existing this.vGridLines = this.content.selectAll( 'line.v-grid-line' ) @@ -328,51 +336,70 @@ }; + // initial render or complete re-render (REPLACE datapoints) this.renderDatapoints = function( xCol, yCol, ids ){ - // initial render, complete re-render (REPLACE datapoints) + this.log( this + '.renderDatapoints', arguments ); + var count = 0; this.datapoints = this.addDatapoints( xCol, yCol, ids, ".glyph" ); // glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM this.datapoints.exit() + .each( function(){ count += 1; } ) .transition().duration( this.config.entryAnimDuration ) .attr( "cy", this.config.height ) .attr( "r", 0 ) .remove(); + this.log( count, ' glyphs removed' ); - this.log( this.datapoints, 'glyphs rendered' ); + //this.log( this.datapoints.length, ' glyphs in the graph' ); }; + // adding points to existing this.addDatapoints = function( newXCol, newYCol, ids, selectorForExisting ){ + this.log( this + '.addDatapoints', arguments ); // ADD datapoints to plot that's already rendered // if selectorForExisting === undefined (as in not passed), addDatapoints won't update existing // pass in the class ( '.glyph' ) to update exising datapoints var plot = this, + count = 0, xPosFn = function( d, i ){ + //if( d ){ this.log( 'x.data:', newXCol[ i ], 'plotted:', plot.xScale( newXCol[ i ] ) ); } return plot.xScale( newXCol[ i ] ); }, yPosFn = function( d, i ){ + //if( d ){ this.log( 'y.data:', newYCol[ i ], 'plotted:', plot.yScale( newYCol[ i ] ) ); } return plot.yScale( newYCol[ i ] ); }; // select all existing glyphs and compare to incoming data // enter() will yield those glyphs that need to be added - var newDatapoints = this.content.selectAll( selectorForExisting ).data( newXCol ); + var newDatapoints = this.content.selectAll( selectorForExisting ); + this.log( 'existing datapoints:', newDatapoints ); + newDatapoints = newDatapoints.data( newXCol ); // enter - new data to be added as glyphs: give them a 'entry' position and style + count = 0; newDatapoints.enter() - .append( "svg:circle" ) + .append( 'svg:circle' ) + .each( function(){ count += 1; } ) .classed( "glyph", true ) - // start all bubbles small... .attr( "cx", xPosFn ) .attr( "cy", yPosFn ) + // start all bubbles small... .attr( "r", 0 ); + this.log( count, ' new glyphs created' ); // for all existing glyphs and those that need to be added: transition anim to final state + count = 0; newDatapoints // ...animate to final position .transition().duration( this.config.entryAnimDuration ) - .attr( "r", this.config.datapointSize ); + .each( function(){ count += 1; } ) + .attr( "cx", xPosFn ) + .attr( "cy", yPosFn ) + .attr( "r", plot.config.datapointSize ); + this.log( count, ' existing glyphs transitioned' ); // attach ids if( ids ){ @@ -419,21 +446,22 @@ }; this.render = function( columnData, meta ){ + this.log( this + '.render', arguments ); //pre: columns passed are numeric //pre: at least two columns are passed //assume: first column is x, second column is y, any remaining aren't used var xCol = columnData[0], yCol = columnData[1], ids = ( columnData.length > 2 )?( columnData[2] ):( undefined ); - this.log( 'renderScatterplot', xCol.length, yCol.length, this.config ); + //this.log( this + '.render', xCol.length, yCol.length, this.config ); //pre: xCol.len == yCol.len xCol = this.preprocessData( xCol ); yCol = this.preprocessData( yCol ); - //this.log( 'xCol len', xCol.length, 'yCol len', yCol.length ); + this.log( 'xCol len', xCol.length, 'yCol len', yCol.length ); this.setUpDomains( xCol, yCol, meta ); - this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax ); + //this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax ); this.setUpScales(); this.adjustChartDimensions(); @@ -515,7 +543,7 @@ this.$chartSettingsPanel = this._render_chartSettings(); this.$statsPanel = this.$el.find( '.tab-pane#chart-stats' ); - this.$el.find( 'ul.nav' ).find( 'a[href="#chart-settings"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-settings"]' ).tab( 'show' ); return this; }, @@ -523,6 +551,7 @@ var chartControl = this, $chartSettingsPanel = this.$el.find( '.tab-pane#chart-settings' ), // limits for controls (by control/chartConfig id) + //TODO: move into TwoVarScatterplot controlRanges = { 'maxDataPoints' : { min: 1000, max: 30000, step: 100 }, 'datapointSize' : { min: 2, max: 10, step: 1 }, @@ -559,7 +588,6 @@ }); //TODO: anim checkbox - //TODO: labels -> renderPlot return $chartSettingsPanel; }, @@ -639,7 +667,7 @@ columns = []; this.log( 'columnSelections:', columnSelections ); - //TODO: ?? could be moved into getColumnVals; + //TODO: move this data/chart settings form crap out this.log( columnSelections.X.val, columnSelections.Y.val ); this.xColIndex = columnSelections.X.colIndex; this.yColIndex = columnSelections.Y.colIndex; @@ -650,15 +678,18 @@ columns.push( columnSelections.ID.colIndex ); } - this.log( columnSelections.X.colName, columnSelections.Y.colName ); - this.plot.xLabel = this.chartConfig.xLabel = columnSelections.X.colName; - this.plot.xLabel = this.chartConfig.yLabel = columnSelections.Y.colName; + // update labels using chartSettings inputs (if not at defaults), otherwise the selects' colName + var chartSettingsXLabel = this.$chartSettingsPanel.find( 'input#X-axis-label' ).val(), + chartSettingsYLabel = this.$chartSettingsPanel.find( 'input#Y-axis-label' ).val(); + this.chartConfig.xLabel = ( chartSettingsXLabel === 'X' )? + ( columnSelections.X.colName ):( chartSettingsXLabel ); + this.chartConfig.yLabel = ( chartSettingsYLabel === 'Y' )? + ( columnSelections.Y.colName ):( chartSettingsYLabel ); + //this.log( 'this.chartConfig:', this.chartConfig ); + view.plot.updateConfig( this.chartConfig, false ); - //TODO: alter directly - view.plot.updateConfig( this.chartConfig, false ); //TODO: validate columns - minimally: we can assume either set by selectors or via a good query string //TODO: other vals: max, start, page - //TODO: chart config // fetch the data, sending chosen columns to the server var params = { @@ -705,6 +736,7 @@ //============================================================================== return { + LazyDataLoader : LazyDataLoader, TwoVarScatterplot : TwoVarScatterplot, ScatterplotControlForm : ScatterplotControlForm };}); diff -r 419d87dd65149285d6c9f0043ea898b365d2dbde -r d9e8a7eee7aac7e0ed15fce27592eb0182144889 templates/visualization/scatterplot.mako --- a/templates/visualization/scatterplot.mako +++ b/templates/visualization/scatterplot.mako @@ -209,7 +209,7 @@ var hda = ${h.to_json_string( hda )}, historyID = '${historyID}' apiDatasetsURL = "${h.url_for( controller='/api/datasets' )}"; - + var settingsForm = new scatterplot.ScatterplotControlForm({ dataset : hda, el : $( '#chart-settings-form' ), @@ -243,3 +243,4 @@ <div id="test"></div></%def> + Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
Bitbucket