diff --git a/eggs.ini b/eggs.ini index 7bec53d8b6f..06dca8b1bac 100644 --- a/eggs.ini +++ b/eggs.ini @@ -12,7 +12,7 @@ repository = http://eggs.g2.bx.psu.edu no_auto = pbs_python DRMAA_python [eggs:platform] -bx_python = 0.7.0 +bx_python = 0.7.1 Cheetah = 2.2.2 ctypes = 1.0.2 DRMAA_python = 0.2 @@ -67,7 +67,7 @@ Whoosh = 0.3.18 psycopg2 = _8.4.2_static pysqlite = _3.6.17_static MySQL_python = _5.1.41_static -bx_python = _494c2d1d68b3_rebuild1 +bx_python = _7b95ff194725 GeneTrack = _dev_48da9e998f0caf01c5be731e926f4b0481f658f0 SQLAlchemy = _dev_r6498 pysam = _kanwei_b10f6e722e9a diff --git a/lib/galaxy/datatypes/converters/interval_to_summary_tree_converter.py b/lib/galaxy/datatypes/converters/interval_to_summary_tree_converter.py index 2fb73a6e18d..f7f89dbf407 100644 --- a/lib/galaxy/datatypes/converters/interval_to_summary_tree_converter.py +++ b/lib/galaxy/datatypes/converters/interval_to_summary_tree_converter.py @@ -50,4 +50,4 @@ def main(): st.write(out_fname) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/lib/galaxy/visualization/tracks/data_providers.py b/lib/galaxy/visualization/tracks/data_providers.py index 29cb08dece9..30792835d2d 100644 --- a/lib/galaxy/visualization/tracks/data_providers.py +++ b/lib/galaxy/visualization/tracks/data_providers.py @@ -2,14 +2,15 @@ Data providers for tracks visualizations. """ -import sys -from math import ceil, log +import sys, time +from math import ceil, log, sqrt import pkg_resources pkg_resources.require( "bx-python" ) if sys.version_info[:2] == (2, 4): pkg_resources.require( "ctypes" ) pkg_resources.require( "pysam" ) pkg_resources.require( "numpy" ) +import numpy from galaxy.datatypes.util.gff_util import * from galaxy.util.json import from_json_string from bx.interval_index_file import Indexes @@ -740,40 +741,73 @@ class BBIDataProvider( TracksDataProvider ): # Bigwig has the possibility of it being a standalone bigwig file, in which case we use # original_dataset, or coming from wig->bigwig conversion in which we use converted_dataset f, bbi = self._get_dataset() - + + # If the stats kwarg was provide, we compute overall summary data for + # the entire chromosome, but no reduced data -- currently only + # providing values used by trackster to determine the default range if 'stats' in kwargs: - all_dat = bbi.query(chrom, 0, 2147483647, 1) + # FIXME: use actual chromosome size + summary = bbi.summarize( chrom, 0, 214783647, 1 ) f.close() - if all_dat is None: + if summary is None: return None - - all_dat = all_dat[0] # only 1 summary - return { 'data' : { 'max': float( all_dat['max'] ), \ - 'min': float( all_dat['min'] ), \ - 'total_frequency': float( all_dat['coverage'] ) } \ - } - + else: + # Does the summary contain any defined values? + valid_count = summary.valid_count[0] + if summary.valid_count < 1: + return None + + # Compute $\mu \pm 2\sigma$ to provide an estimate for upper and lower + # bounds that contain ~95% of the data. + mean = summary.sum_data[0] / valid_count + var = summary.sum_squares[0] - mean + if valid_count > 1: + var /= valid_count - 1 + sd = numpy.sqrt( var ) + + return dict( data=dict( min=summary.min_val[0], max=summary.max_val[0], mean=mean, sd=sd ) ) + start = int(start) end = int(end) + + # The following seems not to work very well, for example it will only return one + # data point if the tile is 1280px wide. Not sure what the intent is. + # The first zoom level for BBI files is 640. If too much is requested, it will look at each block instead # of summaries. The calculation done is: zoom <> (end-start)/num_points/2. # Thus, the optimal number of points is (end-start)/num_points/2 = 640 # num_points = (end-start) / 1280 - num_points = (end-start) / 1280 - if num_points < 1: - num_points = end - start - else: - num_points = min(num_points, 500) + #num_points = (end-start) / 1280 + #if num_points < 1: + # num_points = end - start + #else: + # num_points = min(num_points, 500) - data = bbi.query(chrom, start, end, num_points) + # For now, we'll do 1000 data points by default However, the summaries + # don't seem to work when a summary pixel corresponds to less than one + # datapoint, so we prevent that. + # FIXME: need to switch over to using the full data at high levels of + # detail. + num_points = min( 1000, end - start ) + + summary = bbi.summarize( chrom, start, end, num_points ) f.close() - - pos = start - step_size = (end - start) / num_points + result = [] - if data: - for dat_dict in data: - result.append( (pos, float_nan(dat_dict['mean']) ) ) + + if summary: + mean = summary.sum_data / summary.valid_count + + ## Standard deviation by bin, not yet used + ## var = summary.sum_squares - mean + ## var /= minimum( valid_count - 1, 1 ) + ## sd = sqrt( var ) + + pos = start + step_size = (end - start) / num_points + + for i in range( num_points ): + result.append( (pos, float_nan( mean[i] ) ) ) pos += step_size return { 'data': result } diff --git a/lib/galaxy/web/controllers/tracks.py b/lib/galaxy/web/controllers/tracks.py index 68397d6a3be..3ae6cea5063 100644 --- a/lib/galaxy/web/controllers/tracks.py +++ b/lib/galaxy/web/controllers/tracks.py @@ -528,6 +528,7 @@ class TracksController( BaseUIController, UsesVisualization, UsesHistoryDatasetA if not standalone_provider.has_data( chrom ): return messages.NO_DATA valid_chroms = standalone_provider.valid_chroms() + # Have data if we get here return { "status": messages.DATA, "valid_chroms": valid_chroms } @@ -1041,4 +1042,4 @@ def _get_highest_priority_msg( message_list ): return_message = message elif return_message == None and message == messages.PENDING: return_message = message - return return_message \ No newline at end of file + return return_message diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index 998165bb0d7..d53be3086af 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -231,6 +231,10 @@ div.form-row-error-message { padding: 3px 0 0 1em; } +.form-row .help { + color: #666; +} + select, input, textarea { font: inherit; } diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index a6caed06955..70bc7fede74 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -43,6 +43,7 @@ div.form-row label.inline{display:inline;} div.form-row-input{float:left;} div.form-row-input label{font-weight:normal;display:inline;} div.form-row-error-message{width:300px;float:left;color:red;font-weight:bold;padding:3px 0 0 1em;} +.form-row .help{color:#666;} select,input,textarea{font:inherit;} select,textarea,input[type="text"],input[type="file"],input[type="password"]{-webkit-box-sizing:border-box;max-width:300px;} .errormessagelarge,.warningmessagelarge,.donemessagelarge,.infomessagelarge{padding:10px;padding-left:52px;min-height:32px;border:1px solid #AA6666;background-color:#FFCCCC;background-image:url(error_message_icon.png);background-repeat:no-repeat;background-position:10px 10px;} diff --git a/static/june_2007_style/blue/panel_layout.css b/static/june_2007_style/blue/panel_layout.css index 298ee40af41..5a3e2cf51d7 100644 --- a/static/june_2007_style/blue/panel_layout.css +++ b/static/june_2007_style/blue/panel_layout.css @@ -23,9 +23,11 @@ div.unified-panel-body{position:absolute;top:2em;bottom:0;width:100%;margin-top: .panel-header-button:hover{color:black;background-color:#ccc;} .panel-header-button:active{color:white;background-color:#aaaaaa;} #overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:20000;} +#overlay.modal #overlay-background{background:rgba(0,0,0,0.5);} .dialog-box-container{position:relative;margin-top:80px;margin-right:auto;margin-left:auto;} .dialog-box-wrapper{position:relative;padding:1em;background-color:rgba(0,0,0,0.5);-moz-border-radius:1em;-webkit-border-radius:1em;} .dialog-box{border:solid #999 1px;background:white;z-index:80000;} +#overlay.modal .dialog-box .body{min-width:600px;} .dialog-box .body{padding:5px;overflow:auto;max-height:500px;min-width:300px;} .dialog-box .buttons{padding:5px;} .panel-error-message,.panel-warning-message,.panel-done-message,.panel-info-message{height:24px;line-height:24px;color:#303030;padding:0px;padding-left:26px;background-color:#FFCCCC;background-image:url(error_small.png);background-repeat:no-repeat;background-position:6px 50%;} diff --git a/static/june_2007_style/blue/trackster.css b/static/june_2007_style/blue/trackster.css index e59b26677a9..a9ba82ae395 100644 --- a/static/june_2007_style/blue/trackster.css +++ b/static/june_2007_style/blue/trackster.css @@ -3,30 +3,9 @@ .content{font:10px verdana;} .nav-controls{text-align:center;padding:1px 0;} .nav-controls input{margin:0 5px;} -.menu-button{padding: 0px 4px 0px 4px;} #zoom-in,#zoom-out{display:inline-block;height:16px;width:16px;margin-bottom:-3px;cursor:pointer;} #zoom-out{background:transparent url(../images/fugue/magnifier-zoom-out.png) center center no-repeat;} #zoom-in{margin-left:10px;background:transparent url(../images/fugue/magnifier-zoom.png) center center no-repeat;} -.overview-icon{background:transparent url(../images/fugue/application-dock-270-bw.png) no-repeat;} -.overview-icon:hover{background:transparent url(../images/fugue/application-dock-270.png) no-repeat;} -.settings-icon{background:transparent url(../images/fugue/gear-bw.png) no-repeat;} -.settings-icon:hover{background:transparent url(../images/fugue/gear.png) no-repeat;} -.tools-icon{background:transparent url(../images/fugue/toolbox-bw.png) no-repeat;} -.tools-icon:hover{background:transparent url(../images/fugue/toolbox.png) no-repeat;} -.filters-icon{background:transparent url(../images/fugue/ui-slider-050-bw.png) no-repeat;} -.filters-icon:hover{background:transparent url(../images/fugue/ui-slider-050.png) no-repeat;} -.remove-icon,.overview-close{background:transparent url(../images/fugue/cross-small-bw.png) no-repeat;} -.remove-icon:hover,.overview-close:hover{background:transparent url(../images/fugue/cross-circle.png) no-repeat;} -#close-icon{background:transparent url(../images/fugue/cross-circle-bw.png) no-repeat;margin-right:0px;} -#close-icon:hover{background:transparent url(../images/fugue/cross-circle.png) no-repeat;} -#add-tracks-icon{background:transparent url(../images/fugue/plus-button-bw.png) no-repeat;} -#add-tracks-icon:hover{background:transparent url(../images/fugue/plus-button.png) no-repeat;} -#add-group-icon{background:transparent url(../images/fugue/block--plus-bw.png) no-repeat;} -#add-group-icon:hover{background:transparent url(../images/fugue/block--plus.png) no-repeat;} -#bookmarks-icon{background:transparent url(../images/fugue/bookmarks-bw.png) no-repeat;} -#bookmarks-icon:hover{background:transparent url(../images/fugue/bookmarks.png) no-repeat;} -#save-icon{background:transparent url(../images/fugue/disk--arrow-bw.png) no-repeat;} -#save-icon:hover{background:transparent url(../images/fugue/disk--arrow.png) no-repeat;} .nav-input{font-size:12px;width:30em;z-index:1000;} .location{display:inline-block;width:15em;margin:0 10px;} .draghandle{margin-top:2px;cursor:move;float:left;background:transparent url(../images/visualization/draggable_horizontal.png) center center no-repeat;width:10px;height:12px;} @@ -65,16 +44,39 @@ input{font:10px verdana;} .dynamic-tool,.filters{margin-left:0.25em;padding-bottom:0.5em;} .dynamic-tool{width:410px;} -.filters>.sliders,.display-controls{float:left;margin:1em;} +.filters > .sliders,.display-controls{float:left;margin:1em;} .sliders{width:410px;} -.display-controls{border-left:solid 2px #DDDDDD;padding-left:1em} -.filter-row{margin-top:0.4em;} +.display-controls{border-left:solid 2px #DDDDDD;padding-left:1em +} +.filter-row { + margin-top:0.4em;} .slider-row{margin-left:1em;} .elt-label{float:left;font-weight:bold;margin-right:1em;} .slider{float:right;width:200px;position:relative;} .tool-name{font-size:110%;font-weight:bold;} .param-row{margin-top:0.2em;margin-left:1em;} .param-label{float:left;font-weight:bold;padding-top:0.2em;} +.menu-button{padding:0px 4px 0px 4px;} +.settings-icon{background:transparent url(../images/fugue/gear-bw.png) no-repeat;} +.settings-icon:hover{background:transparent url(../images/fugue/gear.png) no-repeat;} +.overview-icon{background:transparent url(../images/fugue/application-dock-270-bw.png) no-repeat;} +.overview-icon:hover{background:transparent url(../images/fugue/application-dock-270.png) no-repeat;} +.tools-icon{background:transparent url(../images/fugue/toolbox-bw.png) no-repeat;} +.tools-icon:hover{background:transparent url(../images/fugue/toolbox.png) no-repeat;} +.filters-icon{background:transparent url(../images/fugue/ui-slider-050-bw.png) no-repeat;} +.filters-icon:hover{background:transparent url(../images/fugue/ui-slider-050.png) no-repeat;} +.remove-icon,.overview-close{background:transparent url(../images/fugue/cross-small-bw.png) no-repeat;} +.remove-icon:hover,.overview-close:hover{background:transparent url(../images/fugue/cross-circle.png) no-repeat;} +#close-icon{background:transparent url(../images/fugue/cross-circle-bw.png) no-repeat;margin-right:0px;} +#close-icon:hover{background:transparent url(../images/fugue/cross-circle.png) no-repeat;} +#add-tracks-icon{background:transparent url(../images/fugue/plus-button-bw.png) no-repeat;} +#add-tracks-icon:hover{background:transparent url(../images/fugue/plus-button.png) no-repeat;} +#add-group-icon{background:transparent url(../images/fugue/block--plus-bw.png) no-repeat;} +#add-group-icon:hover{background:transparent url(../images/fugue/block--plus.png) no-repeat;} +#bookmarks-icon{background:transparent url(../images/fugue/bookmarks-bw.png) no-repeat;} +#bookmarks-icon:hover{background:transparent url(../images/fugue/bookmarks.png) no-repeat;} +#save-icon{background:transparent url(../images/fugue/disk--arrow-bw.png) no-repeat;} +#save-icon:hover{background:transparent url(../images/fugue/disk--arrow.png) no-repeat;} .child-track-icon{background:url('../images/fugue/arrow-000-small-bw.png') no-repeat;width:30px;cursor:move;} .track-resize{background:white url('../images/visualization/draggable_vertical.png') no-repeat top center;position:absolute;right:3px;bottom:-4px;width:14px;height:7px;border:solid #999 1px;z-index:100;} .bookmark{background:white;border:solid #999 1px;border-right:none;margin:0.5em;margin-right:0;padding:0.5em;} @@ -85,4 +87,5 @@ input{font:10px verdana;} .icon.more-across{background:url('../images/fugue/arrow-transition-bw.png') no-repeat 0px 0px;} .intro{padding:1em;} .intro > .action-button{background-color:#CCC;padding:1em;} -.feature-popup{background-color:#DDD;position:absolute;z-index:1000} +.feature-popup{position:absolute;z-index:1000;padding:5px;font-size:10px;filter:alpha(opacity=80);background-repeat:no-repeat;background-image:url(../images/tipsy.gif);background-position:top center;} +.feature-popup-inner{padding:5px 8px 4px 8px;background-color:black;color:white;} diff --git a/static/june_2007_style/panel_layout.css.tmpl b/static/june_2007_style/panel_layout.css.tmpl index 614079a91a4..292501dda9c 100644 --- a/static/june_2007_style/panel_layout.css.tmpl +++ b/static/june_2007_style/panel_layout.css.tmpl @@ -161,6 +161,10 @@ div.unified-panel-body { z-index: 20000; } +#overlay.modal #overlay-background { + background: rgba(0,0,0,0.5); +} + .dialog-box-container { position: relative; margin-top: 80px; @@ -182,6 +186,10 @@ div.unified-panel-body { z-index: 80000; } +#overlay.modal .dialog-box .body { + min-width: 600px; +} + .dialog-box .body { padding: 5px; overflow: auto; diff --git a/static/june_2007_style/trackster.css.tmpl b/static/june_2007_style/trackster.css.tmpl index 16e92729a7c..a902ec965d0 100644 --- a/static/june_2007_style/trackster.css.tmpl +++ b/static/june_2007_style/trackster.css.tmpl @@ -405,8 +405,19 @@ input { background-color: #CCC; padding: 1em; } -.feature-popup{ - background-color: #DDD; + +.feature-popup { position: absolute; - z-index: 1000 + z-index: 1000; + padding: 5px; + font-size: 10px; + filter: alpha(opacity=80); + background-repeat: no-repeat; + background-image: url(../images/tipsy.gif); + background-position: top center; +} +.feature-popup-inner { + padding: 5px 8px 4px 8px; + background-color: black; + color: white; } diff --git a/static/scripts/galaxy.panels.js b/static/scripts/galaxy.panels.js index be187d8fb57..37adce6b7e2 100644 --- a/static/scripts/galaxy.panels.js +++ b/static/scripts/galaxy.panels.js @@ -163,13 +163,23 @@ function make_right_panel( panel_el, center_el, border_el ) { // Modal dialog boxes function hide_modal() { - $(".dialog-box-container" ).fadeOut( function() { + $(".dialog-box-container" ).hide( 0, function() { $("#overlay").hide(); + $("#overlay").removeClass( "modal" ); $( ".dialog-box" ).find( ".body" ).children().remove(); } ); }; -function show_modal( title, body, buttons, extra_buttons, init_fn ) { +function show_modal() { + $("#overlay").addClass( "modal" ); + _show_modal.apply( this, arguments ); +} + +function show_message() { + _show_modal.apply( this, arguments ); +} + +function _show_modal( title, body, buttons, extra_buttons, init_fn ) { if ( title ) { $( ".dialog-box" ).find( ".title" ).html( title ); $( ".dialog-box" ).find( ".unified-panel-header" ).show(); @@ -205,7 +215,7 @@ function show_modal( title, body, buttons, extra_buttons, init_fn ) { $( ".dialog-box" ).find( ".body" ).html( body ); if ( ! $(".dialog-box-container").is( ":visible" ) ) { $("#overlay").show(); - $(".dialog-box-container").fadeIn(); + $(".dialog-box-container").show(); } // Fix min-width so that modal cannot shrink considerably if // new content is loaded. diff --git a/static/scripts/trackster.js b/static/scripts/trackster.js index bf57da53f95..5875baee4e9 100644 --- a/static/scripts/trackster.js +++ b/static/scripts/trackster.js @@ -701,7 +701,7 @@ extend(Drawable.prototype, { remove: function() { this.container.remove_drawable(this); - this.container_div.fadeOut('slow', function() { + this.container_div.hide(0, function() { $(this).remove(); // HACK: is there a better way to update the view? view.update_intro_div(); @@ -1027,6 +1027,7 @@ extend( View.prototype, DrawableCollection.prototype, { // Only act on x axis scrolling if we see if, y will be i // handled by the browser when the event bubbles up if ( dx ) { + dx *= 50; var delta_chrom = Math.round( - dx / view.viewport_container.width() * (view.high - view.low) ); view.move_delta( delta_chrom ); } @@ -1255,7 +1256,7 @@ extend( View.prototype, DrawableCollection.prototype, { DrawableCollection.prototype.remove_drawable.call(this, drawable); if (hide) { var view = this; - drawable.container_div.fadeOut('slow', function() { + drawable.container_div.hide(0, function() { $(this).remove(); view.update_intro_div(); }); @@ -2114,8 +2115,14 @@ extend(DrawableConfig.prototype, { build_form: function() { var track_config = this; var container = $("
"); - $.each( this.params, function( index, param ) { - if ( ! param.hidden ) { + var param; + // Function to process parameters recursively + function handle_params( params, container ) { + for ( var index = 0; index < params.length; index++ ) { + param = params[index]; + // Hidden params have no representation in the form + if ( param.hidden ) { continue; } + // Build row for param var id = 'param_' + index; var value = track_config.values[ param.key ]; var row = $("
").appendTo( container ); @@ -2127,18 +2134,19 @@ extend(DrawableConfig.prototype, { } else if ( param.type === 'color' ) { var input = $('').attr("id", id ).attr("name", id ).val( value ); // Color picker in tool tip style float - var tip = $( "
" ).hide(); + var tip = $( "
" ).hide(); // Inner div for padding purposes var tip_inner = $("
").appendTo(tip); var farb_container = $("
") .appendTo(tip_inner) .farbtastic( { width: 100, height: 100, callback: input, color: value }); - // Outer div container input and tip for hover to work $("
").append( input ).append( tip ).appendTo( row ).bind( "click", function ( e ) { tip.css( { - left: $(this).position().left + ( $(input).width() / 2 ) - 60, - top: $(this).position().top + $(this.height) + // left: $(this).position().left + ( $(input).width() / 2 ) - 60, + // top: $(this).position().top + $(this.height) + left: $(this).position().left + $(input).width() + 5, + top: $(this).position().top - ( $(tip).height() / 2 ) + ( $(input).height() / 2 ) } ).show(); $(document).bind( "click.color-picker", function() { tip.hide(); @@ -2150,8 +2158,15 @@ extend(DrawableConfig.prototype, { else { row.append( $('').attr("id", id ).attr("name", id ).val( value ) ); } + // Help text + if ( param.help ) { + row.append( $("
").text( param.help ) ); + } } - }); + } + // Handle top level parameters in order + handle_params( this.params, container ); + // Return element containing constructed form return container; }, update_from_form: function( container ) { @@ -2229,7 +2244,16 @@ FeatureTrackTile.prototype.predisplay_actions = function() { // Only show popups in Pack mode. if (tile.mode !== "Pack") { return; } - $(this.canvas).mousemove(function (e) { + $(this.canvas).hover( function() { + this.hovered = true; + $(this).mousemove(); + }, function() { + this.hovered = false; + // Clear popup if it is still hanging around (this is probably not needed) + $(this).siblings(".feature-popup").remove(); + } ).mousemove(function (e) { + // Use the hover plugin to get a delay before showing popup + if ( !this.hovered ) { return; } // Get feature data for position. var this_offset = $(this).offset(), @@ -2270,8 +2294,8 @@ FeatureTrackTile.prototype.predisplay_actions = function() { // Build popup. var popup = $("
").attr("id", feature_uid).addClass("feature-popup"), - key, value, - table = $("").appendTo(popup), row; + table = $("
"), + key, value, row; for (key in feature_dict) { value = feature_dict[key]; row = $("").appendTo(table); @@ -2279,6 +2303,7 @@ FeatureTrackTile.prototype.predisplay_actions = function() { $("
").attr("align", "left").appendTo(row) .text(typeof(value) == 'number' ? round(value, 2) : value); } + popup.append( $("
").append( table ) ); popups[feature_uid] = popup; } @@ -2289,7 +2314,7 @@ FeatureTrackTile.prototype.predisplay_actions = function() { // parseInt strips "px" from left, top measurements. +7 so that mouse pointer does not // overlap popup. var - popupX = offsetX + parseInt( tile.canvas.css("left") ) + 7, + popupX = offsetX + parseInt( tile.canvas.css("left") ) - popup.width() / 2, popupY = offsetY + parseInt( tile.canvas.css("top") ) + 7; popup.css("left", popupX + "px").css("top", popupY + "px") } @@ -2331,6 +2356,12 @@ var Track = function(name, view, container, show_header, prefs, data_url, data_q this.data_url_extra_params = {} this.data_query_wait = (data_query_wait ? data_query_wait : DEFAULT_DATA_QUERY_WAIT); this.dataset_check_url = converted_datasets_state_url; + + // FIXME: this should be a saved setting + this.content_visible = true; + + if (!Track.id_counter) { Track.id_counter = 0; } + this.id = Track.id_counter++; // // Create HTML element structure for track. @@ -2346,6 +2377,9 @@ var Track = function(name, view, container, show_header, prefs, data_url, data_q this.icons_div = $("
").css("float", "left").appendTo(this.header_div).hide(); // Track icons. + this.toggle_icon = $("").attr("href", "javascript:void(0);").attr("title", "Hide/show track content") + .addClass("icon-button toggle").tipsy( {gravity: 's'} ) + .appendTo(this.icons_div); this.settings_icon = $("").attr("href", "javascript:void(0);").attr("title", "Edit settings") .addClass("icon-button settings-icon").tipsy( {gravity: 's'} ) .appendTo(this.icons_div); @@ -2365,6 +2399,21 @@ var Track = function(name, view, container, show_header, prefs, data_url, data_q // Suppress double clicks in header so that they do not impact viz. this.header_div.dblclick( function(e) { e.stopPropagation(); } ); + + // Toggle icon hides or shows the track content + this.toggle_icon.click( function() { + if ( track.content_visible ) { + track.toggle_icon.addClass("toggle-expand").removeClass("toggle"); + track.hide_contents(); + track.mode_div.hide(); + track.content_visible = false; + } else { + track.toggle_icon.addClass("toggle").removeClass("toggle-expand"); + track.content_visible = true; + track.mode_div.show(); + track.show_contents(); + } + }); // Clicking on settings icon opens track config. this.settings_icon.click( function() { @@ -2551,6 +2600,26 @@ extend(Track.prototype, Drawable.prototype, { this.update_icons(); }, + /** + * Hide any elements that are part of the tracks contents area. Should + * remove as approprite, the track will be redrawn by show_contents. + */ + hide_contents : function () { + // Clear contents by removing any elements that are contained in + // the tracks content_div + this.content_div.children().remove(); + // Hide the content div + this.content_div.hide(); + // And any y axis labels (common to several track types) + this.container_div.find(".yaxislabel, .track-resize").hide() + }, + show_contents : function() { + // Show the contents div and labels (if present) + this.content_div.show(); + this.container_div.find(".yaxislabel, .track-resize").show() + // Request a redraw of the content + this.request_draw(); + }, /** * Additional initialization required before drawing track for the first time. */ @@ -2682,6 +2751,11 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, { */ _draw: function(force, clear_after) { if (!this.enabled) { return; } + + // TODO: There should probably be a general way to disable content drawing + // for all drawables. However the button to toggle this is currently + // only present for Track instances. + if (!this.content_visible) { return; } // HACK: ReferenceTrack can draw without dataset ID, but other tracks cannot. if ( !(this instanceof ReferenceTrack) && (!this.dataset_id) ) { return; } @@ -2694,7 +2768,7 @@ extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, { w_scale = width / range, resolution = this.view.resolution, parent_element = $("
"); - + // For overview, adjust high, low, resolution, and w_scale. if (this.is_overview) { low = this.view.max_low; @@ -3028,7 +3102,8 @@ var LineTrack = function (name, view, container, hda_ldda, dataset_id, prefs) { this.min_height_px = 16; this.max_height_px = 400; - this.height_px = 80; + // Default height for new tracks, should be a defined constant? + this.height_px = 32; this.hda_ldda = hda_ldda; this.dataset_id = dataset_id; this.original_dataset_id = dataset_id; @@ -3076,8 +3151,10 @@ extend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, { var drag_control = $( "
" ) // Control shows on hover over track, stays while dragging $(track.container_div).hover( function() { - in_handle = true; - drag_control.show(); + if ( track.content_visible ) { + in_handle = true; + drag_control.show(); + } }, function() { in_handle = false; if ( ! in_drag ) { drag_control.hide(); } @@ -3108,9 +3185,20 @@ extend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, { track.container_div.addClass( "line-track" ); var data = result.data; if ( isNaN(parseFloat(track.prefs.min_value)) || isNaN(parseFloat(track.prefs.max_value)) ) { - track.prefs.min_value = data.min; - track.prefs.max_value = data.max; + // Compute default minimum and maximum values + var min_value = data.min + var max_value = data.max + // If mean and sd are present, use them to compute a ~95% window + // but only if it would shrink the range on one side + min_value = Math.floor( Math.min( 0, Math.max( min_value, data.mean - 2 * data.sd ) ) ) + max_value = Math.ceil( Math.max( 0, Math.min( max_value, data.mean + 2 * data.sd ) ) ) + // Update the prefs + track.prefs.min_value = min_value; + track.prefs.max_value = max_value; // Update the config + // FIXME: we should probably only save this when the user explicately sets it + // since we lose the ability to compute it on the fly (when changing + // chromosomes for example). $('#track_' + track.dataset_id + '_minval').val(track.prefs.min_value); $('#track_' + track.dataset_id + '_maxval').val(track.prefs.max_value); } @@ -3179,7 +3267,8 @@ var FeatureTrack = function(name, view, container, hda_ldda, dataset_id, prefs, { key: 'name', label: 'Name', type: 'text', default_value: name }, { key: 'block_color', label: 'Block color', type: 'color', default_value: get_random_color() }, { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, - { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, + { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true, + help: 'Show the number of items in each bin when drawing summary histogram' }, { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, ], saved_values: prefs, @@ -3527,7 +3616,7 @@ extend(FeatureTrack.prototype, Drawable.prototype, TiledTrack.prototype, { var filter_height_scaler = (this.filters_manager.height_filter ? new FilterScaler(this.filters_manager.height_filter) : null); // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument var painter = new (this.painter)(filtered, tile_low, tile_high, this.prefs, mode, filter_alpha_scaler, filter_height_scaler, ref_seq); - var required_height = Math.max(MIN_TRACK_HEIGHT, painter.get_required_height(slots_required)); + var required_height = Math.max(MIN_TRACK_HEIGHT, painter.get_required_height(slots_required,width)); var canvas = this.view.canvas_manager.new_canvas(); var feature_mapper = null; @@ -4057,6 +4146,7 @@ var FeaturePositionMapper = function(slot_height) { this.feature_positions = {}; this.slot_height = slot_height; this.translation = 0; + this.y_translation = 0; }; /** @@ -4078,7 +4168,7 @@ FeaturePositionMapper.prototype.map_feature_data = function(feature_data, slot, */ FeaturePositionMapper.prototype.get_feature_data = function(x, y) { // Find slot using Y. - var slot = Math.floor( y/this.slot_height ), + var slot = Math.floor( (y-this.y_translation)/this.slot_height ), feature_dict; // May not be over a slot due to padding, margin, etc. @@ -4108,15 +4198,23 @@ var FeaturePainter = function(data, view_start, view_end, prefs, mode, alpha_sca FeaturePainter.prototype.default_prefs = { block_color: "#FFF", connector_color: "#FFF" }; extend(FeaturePainter.prototype, { - get_required_height: function(rows_required) { + get_required_height: function(rows_required, width) { // y_scale is the height per row var required_height = y_scale = this.get_row_height(), mode = this.mode; // If using a packing mode, need to multiply by the number of slots used if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { required_height = rows_required * y_scale; } + return required_height + this.get_top_padding(width) + this.get_bottom_padding(width); + }, + /** Extra padding before first row of features */ + get_top_padding: function(width) { + return 0; + }, + /** Extra padding after last row of features */ + get_bottom_padding: function(width) { // Pad bottom by half a row, at least 5 px - return required_height + Math.max( Math.round( y_scale / 2 ), 5 ); + return Math.max( Math.round( this.get_row_height() / 2 ), 5 ) }, /** * Draw data on ctx using slots and within the rectangle defined by width and height. Returns @@ -4153,6 +4251,7 @@ extend(FeaturePainter.prototype, { } ctx.restore(); + feature_mapper.y_translation = this.get_top_padding(width); return feature_mapper; }, /** @@ -4179,6 +4278,10 @@ var DENSE_TRACK_HEIGHT = 10, var LinkedFeaturePainter = function(data, view_start, view_end, prefs, mode, alpha_scaler, height_scaler) { FeaturePainter.call(this, data, view_start, view_end, prefs, mode, alpha_scaler, height_scaler); + // Whether to draw a single connector in the background that spans the entire feature (the intron fishbone) + this.draw_background_connector = true; + // Whether to call draw_connector for every pair of blocks + this.draw_individual_connectors = false; }; extend(LinkedFeaturePainter.prototype, FeaturePainter.prototype, { @@ -4216,7 +4319,7 @@ extend(LinkedFeaturePainter.prototype, FeaturePainter.prototype, { f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), draw_start = f_start, draw_end = f_end, - y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale + this.get_top_padding(width), thickness, y_start, thick_start = null, thick_end = null, // TODO: is there any reason why block, label color cannot be set at the Painter level? block_color = this.prefs.block_color, @@ -4279,38 +4382,51 @@ extend(LinkedFeaturePainter.prototype, FeaturePainter.prototype, { // needed. This ensures that whole feature, regardless of whether it starts with // a block, is visible. // - - // Draw whole feature as connector/intron. + + // Compute y axis center position and height var cur_y_center, cur_height; if (mode === "Squish" || mode === "Dense") { - ctx.fillStyle = CONNECTOR_COLOR; cur_y_center = y_center + Math.floor(SQUISH_FEATURE_HEIGHT/2) + 1; cur_height = 1; } else { // mode === "Pack" if (feature_strand) { - var cur_y_center = y_center; - var cur_height = thick_height; - if (feature_strand === "+") { - ctx.fillStyle = ctx.canvas.manager.get_pattern( 'right_strand' ); - } else if (feature_strand === "-") { - ctx.fillStyle = ctx.canvas.manager.get_pattern( 'left_strand' ); - } + cur_y_center = y_center; + cur_height = thick_height; } else { - ctx.fillStyle = CONNECTOR_COLOR; cur_y_center += (SQUISH_FEATURE_HEIGHT/2) + 1; cur_height = 1; } } - ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); + + // Draw whole feature as connector/intron. + if ( this.draw_background_connector ) { + if (mode === "Squish" || mode === "Dense") { + ctx.fillStyle = CONNECTOR_COLOR; + } + else { // mode === "Pack" + if (feature_strand) { + if (feature_strand === "+") { + ctx.fillStyle = ctx.canvas.manager.get_pattern( 'right_strand' ); + } else if (feature_strand === "-") { + ctx.fillStyle = ctx.canvas.manager.get_pattern( 'left_strand' ); + } + } + else { + ctx.fillStyle = CONNECTOR_COLOR; + } + } + ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); + } // Draw blocks. var start_and_height; for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { var block = feature_blocks[k], block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), - block_end = Math.ceil( Math.min(width, Math.max((block[1] - tile_low) * w_scale)) ); + block_end = Math.ceil( Math.min(width, Math.max((block[1] - tile_low) * w_scale)) ), + last_block_start, last_block_end; // Skip drawing if block not on tile. if (block_start > block_end) { continue; } @@ -4341,6 +4457,12 @@ extend(LinkedFeaturePainter.prototype, FeaturePainter.prototype, { ctx.fillRect(block_thick_start, y_center + 1, block_thick_end - block_thick_start, thick_height ); } } + // Draw individual connectors if required + if ( this.draw_individual_connectors && last_block_start ) { + this.draw_connector( ctx, last_block_start, last_block_end, block_start, block_end, y_center ); + } + last_block_start = block_start; + last_block_end = block_end; } // FIXME: Height scaling only works in Pack mode right now. @@ -4673,11 +4795,54 @@ extend(ReadPainter.prototype, FeaturePainter.prototype, { } }); +var ArcLinkedFeaturePainter = function(data, view_start, view_end, prefs, mode, alpha_scaler, height_scaler) { + LinkedFeaturePainter.call(this, data, view_start, view_end, prefs, mode, alpha_scaler, height_scaler); + // Need to know the longest feature length for adding spacing + this.longest_feature_length = this.calculate_longest_feature_length(); + this.draw_background_connector = false; + this.draw_individual_connectors = true; +}; + +extend(ArcLinkedFeaturePainter.prototype, FeaturePainter.prototype, LinkedFeaturePainter.prototype, { + + calculate_longest_feature_length: function () { + var longest_feature_length = 0; + for (var i = 0, len = this.data.length; i < len; i++) { + var feature = this.data[i], feature_start = feature[1], feature_end = feature[2]; + longest_feature_length = Math.max( longest_feature_length, feature_end - feature_start ); + } + return longest_feature_length; + }, + + get_top_padding: function( width ) { + var view_range = this.view_end - this.view_start, + w_scale = width / view_range; + return Math.min( 128, Math.ceil( ( this.longest_feature_length / 2 ) * w_scale ) ); + }, + + draw_connector: function( ctx, block1_start, block1_end, block2_start, block2_end, y_center ) { + // Arc drawing -- from closest endpoints + var x_center = ( block1_end + block2_start ) / 2, + radius = block2_start - x_center; + // For full half circles + var angle1 = Math.PI, angle2 = 0; + if ( radius > 0 ) { + ctx.beginPath(); + ctx.arc( x_center, y_center, block2_start - x_center, Math.PI, 0 ); + ctx.stroke(); + } + } +}); + + + + exports.Scaler = Scaler; exports.SummaryTreePainter = SummaryTreePainter; exports.LinePainter = LinePainter; exports.LinkedFeaturePainter = LinkedFeaturePainter; exports.ReadPainter = ReadPainter; +exports.ArcLinkedFeaturePainter = ArcLinkedFeaturePainter; // End painters_module encapsulation }; @@ -4708,4 +4873,4 @@ exports.ReadPainter = ReadPainter; for ( key in modules.trackster ) { target[key] = modules.trackster[key]; } -})(window); \ No newline at end of file +})(window); diff --git a/templates/tracks/browser.mako b/templates/tracks/browser.mako index 7c929625742..8b9b7069714 100644 --- a/templates/tracks/browser.mako +++ b/templates/tracks/browser.mako @@ -42,7 +42,7 @@ ${parent.javascripts()} -${h.js( "galaxy.base", "galaxy.panels", "json2", "jquery", "jstorage", "jquery.event.drag", "jquery.mousewheel", "jquery.autocomplete", "trackster", "trackster_ui", "jquery.ui.sortable.slider", "farbtastic", "jquery.tipsy" )} +${h.js( "galaxy.base", "galaxy.panels", "json2", "jquery", "jstorage", "jquery.event.drag", "jquery.event.hover","jquery.mousewheel", "jquery.autocomplete", "trackster", "trackster_ui", "jquery.ui.sortable.slider", "farbtastic", "jquery.tipsy" )}