1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/changeset/ae57c837ab03/ changeset: ae57c837ab03 user: carlfeberhard date: 2012-09-21 22:42:29 summary: fixed(?) weird permissions on jquery-ui; scatterplot: major adjustments; affected #: 8 files diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf lib/galaxy/visualization/data_providers/basic.py --- a/lib/galaxy/visualization/data_providers/basic.py +++ b/lib/galaxy/visualization/data_providers/basic.py @@ -45,31 +45,64 @@ """ iterator = self.get_iterator( chrom, start, end ) return self.process_data( iterator, start_val, max_vals, **kwargs ) + def write_data_to_file( self, filename, **kwargs ): """ Write data in region defined by chrom, start, and end to a file. """ raise Exception( "Unimplemented Function" ) + class ColumnDataProvider( BaseDataProvider ): """ Data provider for columnar data """ + MAX_LINES_RETURNED = 30000 - def __init__( self, original_dataset ): + def __init__( self, original_dataset, max_lines_returned=MAX_LINES_RETURNED ): # Compatibility check. if not isinstance( original_dataset.datatype, Tabular ): raise Exception( "Data provider can only be used with tabular data" ) # Attribute init. self.original_dataset = original_dataset + # allow throttling + self.max_lines_returned = max_lines_returned - def get_data( self, cols, start_val=0, max_vals=sys.maxint, **kwargs ): + def get_data( self, columns, start_val=0, max_vals=None, skip_comments=True, **kwargs ): """ Returns data from specified columns in dataset. Format is list of lists where each list is a line of data. """ - - cols = from_json_string( cols ) + #TODO: validate kwargs + try: + max_vals = int( max_vals ) + max_vals = min([ max_vals, self.max_lines_returned ]) + except ( ValueError, TypeError ): + max_vals = self.max_lines_returned + try: + start_val = int( start_val ) + start_val = max([ start_val, 0 ]) + except ( ValueError, TypeError ): + start_val = 0 + + # skip comment lines (if any/avail) + # pre: should have original_dataset and + if( skip_comments + and start_val == 0 + and self.original_dataset.metadata.comment_lines ): + start_val = self.original_dataset.metadata.comment_lines + + response = {} + response[ 'data' ] = data = [] + + #TODO bail if columns None, not parsable, not within meta.columns + # columns is an array of ints for now (should handle column names later) + columns = from_json_string( columns ) + assert( all([ column < self.original_dataset.metadata.columns for column in columns ]) ),( + "column index (%d) must be less" % ( column ) + + " than the number of columns: %d" % ( self.original_dataset.metadata.columns ) ) + + # alter meta by column_selectors (if any) def cast_val( val, type ): """ Cast value based on type. """ if type == 'int': @@ -80,23 +113,23 @@ except: pass return val - data = [] f = open( self.original_dataset.file_name ) + #TODO: add f.seek if given fptr in kwargs for count, line in enumerate( f ): if count < start_val: continue - if max_vals and count-start_val >= max_vals: - message = self.error_max_vals % ( max_vals, "features" ) + + if ( count - start_val ) >= max_vals: break fields = line.split() - #pre: column indeces should be avail in fields - for col_index in cols: - assert col_index < len( fields ), ( - "column index (%d) must be less than fields length: %d" % ( col_index, len( fields ) ) ) - - data.append( [ cast_val( fields[c], self.original_dataset.metadata.column_types[c] ) for c in cols ] ) + fields_len = len( fields ) + #TODO: this will return the wrong number of columns for abberrant lines + data.append([ cast_val( fields[c], self.original_dataset.metadata.column_types[c] ) + for c in columns if ( c < fields_len ) ]) + response[ 'endpoint' ] = dict( last_line=( count - 1 ), file_ptr=f.tell() ) f.close() - - return data + + return response + diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf lib/galaxy/web/api/datasets.py --- a/lib/galaxy/web/api/datasets.py +++ b/lib/galaxy/web/api/datasets.py @@ -38,26 +38,27 @@ return str( e ) # Use data type to return particular type of data. - if data_type == 'state': - rval = self._dataset_state( trans, dataset ) - elif data_type == 'converted_datasets_state': - rval = self._converted_datasets_state( trans, dataset, kwd.get( 'chrom', None ) ) - elif data_type == 'data': - rval = self._data( trans, dataset, **kwd ) - elif data_type == 'features': - rval = self._search_features( trans, dataset, kwd.get( 'query ' ) ) - elif data_type == 'raw_data': - rval = self._raw_data( trans, dataset, **kwd ) - elif data_type == 'track_config': - rval = self.get_new_track_config( trans, dataset ) - else: - # Default: return dataset as API value. - try: + try: + if data_type == 'state': + rval = self._dataset_state( trans, dataset ) + elif data_type == 'converted_datasets_state': + rval = self._converted_datasets_state( trans, dataset, kwd.get( 'chrom', None ) ) + elif data_type == 'data': + rval = self._data( trans, dataset, **kwd ) + elif data_type == 'features': + rval = self._search_features( trans, dataset, kwd.get( 'query ' ) ) + elif data_type == 'raw_data': + rval = self._raw_data( trans, dataset, **kwd ) + elif data_type == 'track_config': + rval = self.get_new_track_config( trans, dataset ) + else: + # Default: return dataset as API value. rval = dataset.get_api_value() - except Exception, e: - rval = "Error in dataset API at listing contents" - log.error( rval + ": %s" % str(e) ) - trans.response.status = 500 + + except Exception, e: + rval = "Error in dataset API at listing contents" + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 return rval def _dataset_state( self, trans, dataset, **kwargs ): diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf lib/galaxy/web/controllers/visualization.py --- a/lib/galaxy/web/controllers/visualization.py +++ b/lib/galaxy/web/controllers/visualization.py @@ -800,26 +800,26 @@ return self.get_visualization( trans, id ) @web.expose - def scatterplot( self, trans, dataset_id, cols ): + def scatterplot( self, trans, dataset_id, **kwargs ): # Get HDA. hda = self.get_dataset( trans, dataset_id, check_ownership=False, check_accessible=True ) - - # get some metadata for the page hda_dict = hda.get_api_value() - #title = "Scatter plot of {name}:".format( **hda_dict ) - #subtitle = "{misc_info}".format( **hda_dict ) + hda_dict[ 'id' ] = dataset_id + if( hda_dict[ 'metadata_column_names' ] == None + and hasattr( hda.datatype, 'column_names' ) ): + hda_dict[ 'metadata_column_names' ] = hda.datatype.column_names + history_id = trans.security.encode_id( hda.history.id ) #TODO: add column data # Read data. - data_provider = ColumnDataProvider( original_dataset=hda ) - data = data_provider.get_data( cols ) + #data_provider = ColumnDataProvider( original_dataset=hda, **kwargs ) + #data = data_provider.get_data() # Return plot. return trans.fill_template_mako( "visualization/scatterplot.mako", - title=hda.name, subtitle=hda.info, - hda=hda, - data=data ) - + hda=hda_dict, + historyID=history_id, + kwargs=kwargs ) @web.json def bookmarks_from_dataset( self, trans, hda_id=None, ldda_id=None ): diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf static/scripts/viz/scatterplot.js --- /dev/null +++ b/static/scripts/viz/scatterplot.js @@ -0,0 +1,466 @@ +/* ============================================================================= +todo: + outside this: + BUG: visualization menu doesn't disappear + BUG?: get column_names (from datatype if necessary) + BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations' + + ??: maybe better to do this with a canvas... + + move renderScatter plot to obj, possibly view? + + fix x axis - adjust ticks when tick labels are long - move odds down and extend tick line + + provide callback in view to load data incrementally - for large sets + + paginate + handle rerender + use endpoint (here and on the server (fileptr)) + fetch (new?) data + handle rerender + + selectable list of preset column comparisons (rnaseq etc.) + multiple plots on one page (small multiples) + + where are bad dataprovider params handled? api/datasets? + + config changes to the graph + render stats on the data (max, min, count) + render warning on long data (> maxDataPoints) + adjust endpoint + + loading indicator + + download svg -> base64 encode + dress up ui + + incorporate glyphs, glyph state renderers + + validate columns selection (here or server) + + ?? ensure svg styles thru d3 or css? + d3: configable (easily) + css: standard - better maintenance + ? override at config + +============================================================================= */ +/** + * Two Variable scatterplot visualization using d3 + * Uses semi transparent circles to show density of data in x, y grid + * usage : + * var plot = new TwoVarScatterplot({ containerSelector : 'div#my-plot', ... }) + * plot.render( xColumnData, yColumnData ); + * + * depends on: d3, underscore + */ +function TwoVarScatterplot( config ){ + var plot = this, + GUESS_AT_SVG_CHAR_WIDTH = 10, + GUESS_AT_SVG_CHAR_HEIGHT = 12, + PADDING = 8, + X_LABEL_TOO_LONG_AT = 5; + + //this.debugging = true; + this.log = function(){ + if( this.debugging && console && console.debug ){ + var args = Array.prototype.slice.call( arguments ); + args.unshift( this.toString() ); + console.debug.apply( null, args ); + } + }; + this.log( 'new TwoVarScatterplot:', config ); + + // ........................................................ set up chart config + // config will default to these values when not passed in + //NOTE: called on new + this.defaults = { + id : 'TwoVarScatterplot', + containerSelector : 'body', + maxDataPoints : 30000, + bubbleRadius : 4, + entryAnimDuration : 500, + //TODO: no effect? + xNumTicks : 10, + yNumTicks : 10, + xAxisLabelBumpY : 40, + yAxisLabelBumpX : -35, + width : 500, + height : 500, + //TODO: anyway to make this a sub-obj? + marginTop : 50, + marginRight : 50, + marginBottom : 50, + marginLeft : 50, + xMin : null, + xMax : null, + yMin : null, + yMax : null, + xLabel : "X", + yLabel : "Y" + }; + this.config = _.extend( {}, this.defaults, config ); + + this.updateConfig = function( newConfig ){ + _.extend( this.config, newConfig ); + }; + + // ........................................................ helpers + this.toString = function(){ + return this.config.id; + }; + this.translateStr = function( x, y ){ + return 'translate(' + x + ',' + y + ')'; + }; + this.rotateStr = function( d, x, y ){ + return 'rotate(' + d + ',' + x + ',' + y + ')'; + }; + + // ........................................................ initial element creation + //NOTE: called on new + this.svg = d3.select( this.config.containerSelector ) + .append( "svg:svg" ).attr( "class", "chart" ).style( 'display', 'none' ); + this.content = this.svg.append( "svg:g" ).attr( "class", "content" ); + + this.xAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' ); + this.xAxisLabel = this.xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' ); + + 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.adjustChartDimensions = function(){ + this.svg + .attr( "width", this.config.width + ( this.config.marginRight + this.config.marginLeft ) ) + .attr( "height", this.config.height + ( this.config.marginTop + this.config.marginBottom ) ) + // initial is hidden - show it + .style( 'display', 'block' ); + + // move content group away from margins + this.content = this.svg.select( "g.content" ) + .attr( "transform", this.translateStr( this.config.marginLeft, this.config.marginTop ) ); + }; + + // ........................................................ data and scales + this.preprocessData = function( data ){ + // set a cap on the data, limit to first n points + return data.slice( 0, this.config.maxDataPoints ); + }; + + this.setUpDomains = function( xCol, yCol ){ + this.xMin = this.config.xMin || d3.min( xCol ); + this.xMax = this.config.xMax || d3.max( xCol ); + this.yMin = this.config.yMin || d3.min( yCol ); + this.yMax = this.config.yMax || d3.max( yCol ); + }; + + this.setUpScales = function(){ + // Interpolation for x, y based on data domains + this.xScale = d3.scale.linear() + .domain([ this.xMin, this.xMax ]) + .range([ 0, this.config.width ]), + this.yScale = d3.scale.linear() + .domain([ this.yMin, this.yMax ]) + .range([ this.config.height, 0 ]); + }; + + // ........................................................ axis and ticks + this.setUpXAxis = function(){ + // origin: bottom, left + //TODO: incoporate top, right + this.xAxisFn = d3.svg.axis() + .scale( this.xScale ) + .ticks( this.config.xNumTicks ) + .orient( 'bottom' ); + this.xAxis// = content.select( 'g#x-axis' ) + .attr( 'transform', this.translateStr( 0, this.config.height ) ) + .call( this.xAxisFn ); + this.log( 'xAxis:', this.xAxis ); + + //TODO: this isn't reliable with -/+ ranges - better to go thru each tick + this.xLongestLabel = d3.max( _.map( [ this.xMin, this.xMax ], + function( number ){ return ( String( number ) ).length; } ) ); + this.log( 'xLongestLabel:', this.xLongestLabel ); + + if( this.xLongestLabel >= X_LABEL_TOO_LONG_AT ){ + //TODO: adjust ticks when tick labels are long - move odds down and extend tick line + // (for now) hide them + this.xAxis.selectAll( 'g' ).filter( ':nth-child(odd)' ).style( 'display', 'none' ); + } + + this.xAxisLabel// = xAxis.select( 'text#x-axis-label' ) + .attr( 'x', this.config.width / 2 ) + .attr( 'y', this.config.xAxisLabelBumpY ) + .attr( 'text-anchor', 'middle' ) + .text( this.config.xLabel ); + this.log( 'xAxisLabel:', this.xAxisLabel ); + }; + + this.setUpYAxis = function(){ + 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.yLongestLabel = d3.max( _.map( [ this.yMin, this.yMax ], + function( number ){ return ( String( number ) ).length; } ) ); + this.log( 'yLongestLabel:', this.yLongestLabel ); + + //TODO: ugh ... so clumsy + //TODO: this isn't reliable with -/+ ranges - better to go thru each tick + var neededY = this.yLongestLabel * GUESS_AT_SVG_CHAR_WIDTH + ( PADDING ); + + // increase width for xLongerStr, increase margin for y + //TODO??: (or transform each number: 2k) + if( this.config.yAxisLabelBumpX > -( neededY ) ){ + this.config.yAxisLabelBumpX = -( neededY ); + } + if( this.config.marginLeft < neededY ){ + this.config.marginLeft = neededY + GUESS_AT_SVG_CHAR_HEIGHT; + // update dimensions, translations + this.adjustChartDimensions(); + } + this.log( 'this.config.yAxisLableBumpx, this.config.marginLeft:', + this.config.yAxisLabelBumpX, this.config.marginLeft ); + + this.yAxisLabel// = yAxis.select( 'text#y-axis-label' ) + .attr( 'x', this.config.yAxisLabelBumpX ) + .attr( 'y', this.config.height / 2 ) + .attr( 'text-anchor', 'middle' ) + .attr( 'transform', this.rotateStr( -90, this.config.yAxisLabelBumpX, this.config.height / 2 ) ) + .text( this.config.yLabel ); + this.log( 'yAxisLabel:', this.yAxisLabel ); + }; + + // ........................................................ grid lines + this.renderGrid = function(){ + // VERTICAL + // select existing + this.vGridLines = this.content.selectAll( 'line.v-grid-line' ) + .data( this.xScale.ticks( this.xAxisFn.ticks()[0] ) ); + + // append any extra lines needed (more ticks) + this.vGridLines.enter().append( 'svg:line' ) + .classed( 'grid-line v-grid-line', true ); + + // update the attributes of existing and appended + this.vGridLines + .attr( 'x1', this.xScale ) + .attr( 'y1', 0 ) + .attr( 'x2', this.xScale ) + .attr( 'y2', this.config.height ); + + // remove unneeded (less ticks) + this.vGridLines.exit().remove(); + this.log( 'vGridLines:', this.vGridLines ); + + // HORIZONTAL + this.hGridLines = this.content.selectAll( 'line.h-grid-line' ) + .data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) ); + + this.hGridLines.enter().append( 'svg:line' ) + .classed( 'grid-line h-grid-line', true ); + + this.hGridLines + .attr( 'x1', 0 ) + .attr( 'y1', this.yScale ) + .attr( 'x2', this.config.width ) + .attr( 'y2', this.yScale ); + + this.hGridLines.exit().remove(); + this.log( 'hGridLines:', this.hGridLines ); + }; + + // ........................................................ data points + //TODO: these to config ...somehow + //TODO: use these in renderDatapoints ...somehow + this.glyphEnterState = function( d3Elem ){ + + }; + this.glyphFinalState = function( d3Elem ){ + + }; + this.glyphExitState = function( d3Elem ){ + + }; + + this.renderDatapoints = function( xCol, yCol ){ + + var xPosFn = function( d, i ){ + return plot.xScale( xCol[ i ] ); + }; + var yPosFn = function( d, i ){ + return plot.yScale( yCol[ i ] ); + }; + + // select all existing glyphs and compare to incoming data + // enter() will yield those glyphs that need to be added + // exit() will yield existing glyphs that need to be removed + this.datapoints = this.content.selectAll( ".glyph" ) + .data( xCol ); + + // enter - new data to be added as glyphs: give them a 'entry' position and style + this.datapoints.enter() + .append( "svg:circle" ).attr( "class", "glyph" ) + // start all bubbles at corner... + .attr( "cx", xPosFn ) + .attr( "cy", 0 ) + .attr( "r", 0 ); + + // for all existing glyphs and those that need to be added: transition anim to final state + this.datapoints + // ...animate to final position + .transition().duration( this.config.entryAnimDuration ) + .attr( "cx", xPosFn ) + .attr( "cy", yPosFn ) + .attr( "r", this.config.bubbleRadius ); + + // glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM + this.datapoints.exit() + .transition().duration( this.config.entryAnimDuration ) + .attr( "cy", this.config.height ) + .attr( "r", 0 ) + .style( "fill-opacity", 0 ) + .remove(); + + this.log( this.datapoints, 'glyphs rendered' ); + }; + + this.render = function( xCol, yCol ){ + //pre: columns passed are numeric + this.log( 'renderScatterplot', xCol.length, yCol.length, this.config ); + + //pre: xCol.len == yCol.len + //TODO: ^^ isn't necessarily true with current ColumnDataProvider + xCol = this.preprocessData( xCol ); + yCol = this.preprocessData( yCol ); + this.log( 'xCol len', xCol.length, 'yCol len', yCol.length ); + + //TODO: compute min, max on server. + this.setUpDomains( xCol, yCol ); + this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax ); + + this.setUpScales(); + this.adjustChartDimensions(); + + this.setUpXAxis(); + this.setUpYAxis(); + + this.renderGrid(); + this.renderDatapoints( xCol, yCol ); + //TODO: on hover red line to axes, display values + }; +} + +//// ugh...this seems like the wrong way to use models +//var ScatterplotModel = BaseModel.extend( LoggableMixin ).extend({ +// logger : console +//}); + +var ScatterplotView = BaseView.extend( LoggableMixin ).extend({ + //logger : console, + tagName : 'form', + className : 'scatterplot-settings-form', + + events : { + 'click #render-button' : 'renderScatterplot' + }, + + initialize : function( attributes ){ + if( !attributes.dataset ){ + throw( "ScatterplotView requires a dataset" ); + } else { + this.dataset = attributes.dataset; + } + this.apiDatasetsURL = attributes.apiDatasetsURL; + this.chartConfig = attributes.chartConfig || {}; + this.log( 'this.chartConfig:', this.chartConfig ); + + this.plot = new TwoVarScatterplot( this.chartConfig ); + }, + + render : function(){ + //TODO: to template + var view = this, + html = '', + columnHtml = ''; + // build column select controls for each x, y (based on name if available) + // ugh...hafta preprocess + this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' ); + _.each( this.dataset.metadata_column_types, function( type, index ){ + // use only numeric columns + if( type === 'int' || type === 'float' ){ + var name = 'column ' + index; + // label with the name if available + if( view.dataset.metadata_column_names ){ + name = view.dataset.metadata_column_names[ index ]; + } + columnHtml += '<option value="' + index + '">' + name + '</column>'; + } + }); + + // column selector containers + html += '<div id="x-column-input">'; + html += '<label for="">Data column for X: </label><select name="x-column">' + columnHtml + '</select>'; + html += '</div>'; + + html += '<div id="y-column-input">'; + html += '<label for="">Data column for Y: </label><select name="y-column">' + columnHtml + '</select>'; + html += '</div>'; + + html += '<input id="render-button" type="button" value="Draw" />'; + html += '<div class="clear"></div>'; + + //TODO: other vals: max_vals, start_val, pagination + + this.$el.append( html ); + this.$el.find( '#render-button' ); + return this; + }, + + renderScatterplot : function(){ + var view = this, + url = this.apiDatasetsURL + '/' + this.dataset.id + '?data_type=raw_data&'; + xSelector = this.$el.find( '[name="x-column"]' ), + xVal = xSelector.val(), + xName = xSelector.children( '[value="' + xVal + '"]' ).text(), + ySelector = this.$el.find( '[name="y-column"]' ), + yVal = ySelector.val(), + yName = ySelector.children( '[value="' + yVal + '"]' ).text(); + //TODO + this.log( xName, yName ); + this.chartConfig.xLabel = xName; + this.chartConfig.yLabel = yName; + //TODO: alter directly + view.plot.updateConfig( this.chartConfig ); + + //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 + + url += jQuery.param({ + columns : '[' + [ xVal, yVal ] + ']' + }); + this.log( 'url:', url ); + + jQuery.ajax({ + url : url, + dataType : 'json', + success : function( response ){ + view.endpoint = response.endpoint; + view.plot.render( + // pull apart first two regardless of number of columns + _.map( response.data, function( columns ){ return columns[0]; } ), + _.map( response.data, function( columns ){ return columns[1]; } ) + ); + }, + error : function( xhr, status, error ){ + alert( 'ERROR:' + status + '\n' + error ); + } + }); + } +}); + diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf templates/root/history.mako --- a/templates/root/history.mako +++ b/templates/root/history.mako @@ -194,6 +194,17 @@ }; }; +function create_scatterplot_action_fn( url, params ){ + action = function() { + var galaxy_main = $( window.parent.document ).find( 'iframe#galaxy_main' ), + final_url = url + '/scatterplot?' + $.param(params); + galaxy_main.attr( 'src', final_url ); + $( 'div.popmenu-wrapper' ).remove(); + return false; + }; + return action; +} + /** * Create popup menu for visualization icon. */ @@ -208,14 +219,20 @@ // Create visualization action. create_viz_action = function(visualization) { var action; - if (visualization === 'trackster') { - action = create_trackster_action_fn(vis_url, params, dbkey); - } - else { - action = function() { - window.parent.location = vis_url + '/' + visualization + '?' + - $.param(params); - }; + switch( visualization ){ + + case 'trackster': + action = create_trackster_action_fn(vis_url, params, dbkey); + break; + + case 'scatterplot': + action = create_scatterplot_action_fn( vis_url, params ); + break; + + default: + action = function(){ + window.parent.location = vis_url + '/' + visualization + '?' + $.param(params); + } } return action; }, @@ -234,6 +251,7 @@ // Set up action or menu. if (visualizations.length === 1) { // No need for popup menu because there's a single visualization. + icon.attr( 'title', visualizations[0] ); icon.click(create_viz_action(visualizations[0])); } else { diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf templates/root/history_common.mako --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -230,10 +230,10 @@ ## information--URL base, dataset id, dbkey, visualizations--in anchor. <% visualizations = data.get_visualizations() - ## HACK: if there are visualizations, only provide trackster for now since others - ## are not ready. + ## HACK: if there are visualizations, only provide trackster for now + ## since others are not ready. - comment out to see all WIP visualizations if visualizations: - visualizations = [ 'trackster' ] + visualizations = [ vis for vis in visualizations if vis in [ 'trackster' ] ] %> %if visualizations: <a href="${h.url_for( controller='visualization' )}" diff -r 63a8fb1d8e2b8a8da71a5b618ef65981ff5c4ac3 -r ae57c837ab039ded7f5056252c2e630988080faf templates/visualization/scatterplot.mako --- a/templates/visualization/scatterplot.mako +++ b/templates/visualization/scatterplot.mako @@ -2,23 +2,61 @@ <%def name="stylesheets()"> ${parent.stylesheets()} -${h.css( "history", "autocomplete_tagging", "trackster", "overcast/jquery-ui-1.8.5.custom", "library" )} +##${h.css( "history", "autocomplete_tagging", "trackster", "overcast/jquery-ui-1.8.5.custom", "library" )} <style type="text/css"> * { margin: 0px, padding: 0px; } +.title { + margin: 0px; + padding: 8px; + background-color: #ebd9b2; +} + .subtitle { - margin-left: 1em; - margin-top: -1em; + margin: 0px; + padding: 0px, 8px, 8px, 16px; + background-color: #ebd9b2; color: grey; font-size: small; } -.chart { +#chart-settings-form { + /*from width + margin of chart?*/ + float: right; + width: 100%; + background-color: #ebd9b2; + padding-top: 1em; +} + +#chart-settings-form > * { + margin: 8px; +} + +#chart-settings-form input, #chart-settings-form select { + width: 30%; + max-width: 256px; +} + +#chart-settings-form [value=Draw] { + float: right; +} + +#chart-holder { + overflow: auto; +} + +.clear { + clear: both; + margin: 0px; +} + + +svg .chart { /*shape-rendering: crispEdges;*/ } -.grid-line { +svg .grid-line { fill: none; stroke: lightgrey; stroke-opacity: 0.5; @@ -26,18 +64,18 @@ stroke-dasharray: 3, 3; } -.axis path, .axis line { +svg .axis path, svg .axis line { fill: none; stroke: black; shape-rendering: crispEdges; } -.axis text { +svg .axis text { font-family: sans-serif; font-size: 12px; } -circle.bubble { +svg .glyph { stroke: none; fill: black; fill-opacity: 0.2; @@ -49,156 +87,39 @@ <%def name="javascripts()"> ${parent.javascripts()} -${h.js( "libs/d3" )} +${h.js( + "libs/underscore", + "libs/backbone/backbone", + "libs/backbone/backbone-relational", + "libs/d3", + "mvc/base-mvc", + "viz/scatterplot" +)} <script type="text/javascript"> -/* ============================================================================= -todo: - validate columns (here or server) - send: type, column title/name in JSON - - move to obj, possibly view? - fetch (new?) data - config changes to the graph - download svg (png?) - -============================================================================= */ -function translateStr( x, y ){ - return 'translate(' + x + ',' + y + ')'; -} -function rotateStr( d, x, y ){ - return 'rotate(' + d + ',' + x + ',' + y + ')'; -} - $(function() { - // Constants - var data = ${data}, - MAX_DATA_POINTS = 30000, - BUBBLE_RADIUS = 5, - ENTRY_ANIM_DURATION = 500, - X_TICKS = 10, Y_TICKS = 10, - X_AXIS_LABEL_BUMP_Y = 40, - Y_AXIS_LABEL_BUMP_X = -35, - WIDTH = 300, - HEIGHT = 300, - MARGIN= 50, - xLabel = "Magnitude", - yLabel = "Depth"; - - // set a cap on the data, limit to first n points - data = data.slice( 0, MAX_DATA_POINTS ); + var hda = ${h.to_json_string( hda )}, + historyID = '${historyID}' + apiDatasetsURL = "${h.url_for( controller='/api/datasets' )}"; + //?? hmmmm + //kwargs = ${h.to_json_string( kwargs )}; - // split the data into columns - //TODO: compute min, max on server. - var col1_data = data.map( function(e) { return e[0] }), - col2_data = data.map( function(e) { return e[1] }), - xMin = d3.min( col1_data ), - xMax = d3.max( col1_data ), - yMin = d3.min( col2_data ), - yMax = d3.max( col2_data ); - //console.log( 'col1_data:', col1_data ); - //console.log( 'col2_data:', col2_data ); - //console.log( 'xMin, xMax, yMin, yMax:', xMin, xMax, yMin, yMax ); - - // Set up. - d3.select( "body" ).append( "svg:svg" ) - .attr( "width", WIDTH + ( MARGIN * 2 ) ) - .attr( "height", HEIGHT + ( MARGIN * 2 ) ) - .attr( "class", "chart" ); - - // Scale for x, y based on data domains - // origin: bottom, left - var x_scale = d3.scale.linear() - .domain([ xMin, xMax ]) - .range([ 0, WIDTH ]), - y_scale = d3.scale.linear() - .domain([ yMin, yMax ]) - .range([ HEIGHT, 0 ]); - - // Selection of SVG, append group (will group our entire chart), give attributes - // apply a group and transform all coords away from margins - var chart = d3.select( ".chart" ).append( "svg:g" ) - .attr( "class", "content" ) - .attr( "transform", translateStr( MARGIN, MARGIN ) ); - - // axes - var xAxisFn = d3.svg.axis() - .scale( x_scale ) - .ticks( X_TICKS ) - .orient( 'bottom' ); - var xAxis = chart.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' ) - .attr( 'transform', translateStr( 0, HEIGHT ) ) - .call( xAxisFn ) - //console.debug( 'xAxis:', xAxis ); window.xAxis = xAxis, window.xAxisFn = xAxisFn; - - var xAxisLabel = xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' ) - .attr( 'x', WIDTH / 2 ) - .attr( 'y', X_AXIS_LABEL_BUMP_Y ) - .attr( 'text-anchor', 'middle' ) - .text( xLabel ); - //console.debug( 'xAxisLabel:', xAxisLabel ); window.xAxisLabel = xAxisLabel; - - var yAxisFn = d3.svg.axis() - .scale( y_scale ) - .ticks( Y_TICKS ) - .orient( 'left' ); - var yAxis = chart.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' ) - .call( yAxisFn ); - //console.debug( 'yAxis:', yAxis ); window.yAxis = yAxis, window.yAxisFn = yAxisFn; - - var yAxisLabel = yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' ) - .attr( 'x', Y_AXIS_LABEL_BUMP_X ) - .attr( 'y', HEIGHT / 2 ) - .attr( 'text-anchor', 'middle' ) - .attr( 'transform', rotateStr( -90, Y_AXIS_LABEL_BUMP_X, HEIGHT / 2 ) ) - .text( yLabel ); - //console.debug( 'yAxisLabel:', yAxisLabel ); window.yAxisLabel = yAxisLabel; - - // grid lines - var hGridLines = chart.selectAll( '.h-grid-line' ) - .data( x_scale.ticks( xAxisFn.ticks()[0] ) ) - .enter().append( 'svg:line' ) - .classed( 'grid-line h-grid-line', true ) - .attr( 'x1', x_scale ).attr( 'y1', 0 ) - .attr( 'x2', x_scale ).attr( 'y2', HEIGHT ) - //console.debug( 'hGridLines:', hGridLines ); window.hGridLines = hGridLines; - - var vGridLines = chart.selectAll( '.v-grid-line' ) - .data( y_scale.ticks( yAxisFn.ticks()[0] ) ) - .enter().append( 'svg:line' ) - .classed( 'grid-line v-grid-line', true ) - .attr( 'x1', 0 ) .attr( 'y1', y_scale ) - .attr( 'x2', WIDTH ).attr( 'y2', y_scale ) - //console.debug( 'vGridLines:', vGridLines ); window.vGridLines = vGridLines; - - // Functions used to render plot. - var xPosFn = function( d, i ){ - return x_scale( col1_data[ i ] ); - }; - var yPosFn = function( d, i ){ - return y_scale( col2_data[ i ] ); - }; - - // Create bubbles for each data point. - chart.selectAll( "circle.bubble" ) - .data(data).enter() - .append( "svg:circle" ).attr( "class", "bubble" ) - // start all bubbles at corner... - .attr( "r", 0 ) - .attr( "fill", "white" ) - // ...animate to final position - .transition().duration( ENTRY_ANIM_DURATION ) - .attr("cx", xPosFn ) - .attr("cy", yPosFn ) - .attr("r", BUBBLE_RADIUS); - - //TODO: on hover red line to axes, display values + var settingsForm = new ScatterplotView({ + dataset : hda, + el : $( '#chart-settings-form' ), + apiDatasetsURL : apiDatasetsURL, + chartConfig : { + containerSelector : '#chart-holder', + marginTop : 20, + } + }).render(); }); </script></%def><%def name="body()"> - <h1 class="title">Scatterplot of '${title}':</h1> - <h2 class="subtitle">${subtitle}</h2> - + <h2 class="title">Scatterplot of '${hda['name']}'</h2> + <p class="subtitle">${hda['misc_info']}</p> + <div id="chart-holder"></div> + <div id="chart-settings-form"></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.