commit/galaxy-central: jgoecks: Trackster: enhance VCF track to show sample data and summary data and use base coloring.
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/b9971c68e947/ Changeset: b9971c68e947 User: jgoecks Date: 2013-04-15 19:20:25 Summary: Trackster: enhance VCF track to show sample data and summary data and use base coloring. Affected #: 4 files diff -r 7e800d8223d95326cb956fbad0ee89e5a6e84f58 -r b9971c68e9472a021f5b7a6784ee1244a85d5a1f lib/galaxy/datatypes/tabular.py --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -552,7 +552,7 @@ return Tabular.make_html_table( self, dataset, column_names=self.column_names ) def get_track_type( self ): - return "VcfTrack", { "data": "tabix", "index": "summary_tree" } + return "VariantTrack", { "data": "tabix", "index": "summary_tree" } class Eland( Tabular ): """Support for the export.txt.gz file used by Illumina's ELANDv2e aligner""" diff -r 7e800d8223d95326cb956fbad0ee89e5a6e84f58 -r b9971c68e9472a021f5b7a6784ee1244a85d5a1f lib/galaxy/visualization/data_providers/genome.py --- a/lib/galaxy/visualization/data_providers/genome.py +++ b/lib/galaxy/visualization/data_providers/genome.py @@ -2,7 +2,7 @@ Data providers for genome visualizations. """ -import os, sys, operator +import os, sys, operator, re from math import ceil, log import pkg_resources pkg_resources.require( "bx-python" ) @@ -601,12 +601,22 @@ """ Abstract class that processes VCF data from native format to payload format. - Payload format: TODO + Payload format: An array of entries for each locus in the file. Each array + has the following entries: + 1. GUID (unused) + 2. location (0-based) + 3. reference base(s) + 4. alternative base(s) + 5. quality score + 6. whether variant passed filter + 7. sample genotypes -- a single string with samples separated by commas; empty string + denotes the reference genotype + 8-end: allele counts for each alternative """ col_name_data_attr_mapping = { 'Qual' : { 'index': 6 , 'name' : 'Qual' } } - dataset_type = 'bai' + dataset_type = 'variant' def process_data( self, iterator, start_val=0, max_vals=None, **kwargs ): """ @@ -621,7 +631,7 @@ message - error/informative message """ - rval = [] + data = [] message = None def get_mapping( ref, alt ): @@ -651,6 +661,7 @@ return ref_in_alt_index, alt[ ref_in_alt_index + 1: ], [ [ cig_ops.find( "I" ), alt_len - ref_len ] ] # Pack data. + genotype_re = re.compile( '/|\|' ) for count, line in enumerate( iterator ): if count < start_val: continue @@ -658,37 +669,61 @@ message = self.error_max_vals % ( max_vals, "features" ) break + # Split line and aggregate data. feature = line.split() - start = int( feature[1] ) - 1 - ref = feature[3] - alts = feature[4] + pos, c_id, ref, alt, qual, c_filter, info = feature[ 1:8 ] + format = feature[ 8 ] + samples_data = feature [ 9: ] + # VCF is 1-based. + pos = int( pos ) - 1 + + # FIXME: OK to skip? + if alt == '.': + count -= 1 + continue - # HACK? alts == '.' --> monomorphism. - if alts == '.': - alts = ref + # Count number of samples matching each allele. + allele_counts = [ 0 for i in range ( alt.count( ',' ) + 1 ) ] - # Pack variants. - for alt in alts.split(","): - offset, new_seq, cigar = get_mapping( ref, alt ) - start += offset - end = start + len( new_seq ) + # Process and pack sample genotype. + sample_gts = [] + alleles_seen = {} + has_alleles = False - # Pack line. - payload = [ - hash( line ), - start, - end, - # ID: - feature[2], - cigar, - # TODO? VCF does not have strand, so default to positive. - "+", - new_seq, - None if feature[5] == '.' else float( feature[5] ) - ] - rval.append(payload) + for i, sample in enumerate( samples_data ): + # Parse and count alleles. + genotype = sample.split( ':' )[ 0 ] + has_alleles = False + alleles_seen.clear() + for allele in genotype_re.split( genotype ): + allele = int( allele ) + # Only count allele if it hasn't been seen yet. + if allele != 0 and allele not in alleles_seen: + allele_counts[ allele - 1 ] += 1 + alleles_seen[ allele ] = True + has_alleles = True + + # If no alleles, use empty string as proxy. + if not has_alleles: + genotype = '' - return { 'data': rval, 'message': message } + sample_gts.append( genotype ) + + # Add locus data. + locus_data = [ + -1, + pos, + c_id, + ref, + alt, + qual, + c_filter, + ','.join( sample_gts ) + ] + locus_data.extend( allele_counts ) + data.append( locus_data ) + + return { 'data': data, 'message': message } def write_data_to_file( self, regions, filename ): out = open( filename, "w" ) @@ -707,7 +742,8 @@ """ Provides data from a VCF file indexed via tabix. """ - pass + + dataset_type = 'variant' class RawVcfDataProvider( VcfDataProvider ): """ diff -r 7e800d8223d95326cb956fbad0ee89e5a6e84f58 -r b9971c68e9472a021f5b7a6784ee1244a85d5a1f static/scripts/viz/trackster/painters.js --- a/static/scripts/viz/trackster/painters.js +++ b/static/scripts/viz/trackster/painters.js @@ -149,7 +149,7 @@ // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases // start. However, height of each rectangle is relative to required_height; hence, the // max rectangle is required_height. - base_y = height; + base_y = height, delta_x_px = Math.ceil(this.data.delta * w_scale); ctx.save(); @@ -1285,7 +1285,7 @@ drawDownwardEquilateralTriangle(ctx, data[0], data[1], data[2]); } } - }, + } }); var ArcLinkedFeaturePainter = function(data, view_start, view_end, prefs, mode, alpha_scaler, height_scaler) { @@ -1544,6 +1544,150 @@ ctx.restore(); }; +/** + * Paints variant data onto canvas. + */ +var VariantPainter = function(data, view_start, view_end, prefs, mode, base_color_fn) { + Painter.call(this, data, view_start, view_end, prefs, mode); + this.base_color_fn = base_color_fn; + this.divider_height = 1; +}; + +extend(VariantPainter.prototype, Painter.prototype, { + /** + * Height of a single row, depends on mode + */ + get_row_height: function() { + var mode = this.mode, height; + if (mode === "Dense") { + height = DENSE_TRACK_HEIGHT; + } + else if (mode === "Squish") { + height = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + height = PACK_TRACK_HEIGHT; + } + return height; + }, + + get_required_height: function(rows_required, width) { + var height = this.prefs.summary_height; + if (this.prefs.show_sample_data) { + height += this.divider_height; + if (this.data.length !== 0) { + // Sample data is separated by commas, so this computes # of samples: + height += (this.data[0][7].match(/,/g).length + 1) * this.get_row_height(); + } + } + return height; + }, + + /** + * Draw on the context using a rectangle of width x height. w_scale is + * needed because it cannot be computed from width and view size alone + * as a left_offset may be present. + */ + draw: function(ctx, width, height, w_scale) { + var locus_data, + pos, + id, + ref, + alt, + qual, + filter, + sample_gts, + allele_counts, + variant, + draw_x_start, + char_x_start, + draw_y_start, + genotype, + // Always draw variants at least 1 pixel wide. + base_px = Math.max(1, Math.floor(w_scale)), + row_height = (this.mode === 'Squish' ? SQUISH_TRACK_HEIGHT : PACK_TRACK_HEIGHT), + // If zoomed out, fill the whole row with feature to make it easier to read; + // when zoomed in, use feature height so that there are gaps in sample rows. + feature_height = (w_scale < 0.1 ? + row_height : + (this.mode === 'Squish' ? SQUISH_FEATURE_HEIGHT : PACK_FEATURE_HEIGHT) + ), + j; + + // Draw divider between summary and samples. + if (this.prefs.show_sample_data) { + ctx.fillStyle = '#DDDDDD'; + ctx.globalAlpha = 1; + ctx.fillRect(0, this.prefs.summary_height - this.divider_height, width, this.divider_height); + } + + // Draw variants. + ctx.textAlign = "center"; + for (var i = 0; i < this.data.length; i++) { + // Get locus data. + locus_data = this.data[i]; + pos = locus_data[1]; + alt = locus_data[4].split(','); + sample_gts = locus_data[7].split(','); + allele_counts = locus_data.slice(8); + + // Compute start for drawing variants marker, text. + draw_x_start = Math.floor( Math.max(-0.5 * w_scale, (pos - this.view_start - 0.5) * w_scale) ); + char_x_start = Math.floor( Math.max(0, (pos - this.view_start) * w_scale) ); + + // Draw summary. + ctx.fillStyle = '#AAAAAA'; + // Draw background for summary. + ctx.fillRect(draw_x_start, 0, base_px, this.prefs.summary_height); + draw_y_start = this.prefs.summary_height; + // Draw allele fractions onto summary. + for (j = 0; j < alt.length; j++) { + ctx.fillStyle = this.base_color_fn(alt[j]); + allele_frac = allele_counts / sample_gts.length; + draw_height = Math.ceil(this.prefs.summary_height * allele_frac); + ctx.fillRect(draw_x_start, draw_y_start - draw_height, base_px, draw_height); + draw_y_start -= draw_height; + } + + // Done drawing if not showing samples data. + if (!this.prefs.show_sample_data) { continue; } + + // Draw sample genotypes. + draw_y_start = this.prefs.summary_height + this.divider_height; + for (j = 0; j < sample_gts.length; j++, draw_y_start += row_height) { + genotype = (sample_gts[j] ? sample_gts[j].split(/\/|\|/) : ['0', '0']); + + // Get variant to draw and set drawing properties. + variant = null; + if (genotype[0] === genotype[1]) { + if (genotype[0] !== '0') { + // Homozygous for variant. + variant = alt[ parseInt(genotype[0], 10) - 1 ]; + ctx.globalAlpha = 1; + } + // else reference + } + else { // Heterozygous for variant. + variant = (genotype[0] !== '0' ? genotype[0] : genotype[1]); + variant = alt[ parseInt(variant, 10) - 1 ]; + ctx.globalAlpha = 0.4; + } + + // If there's a variant, draw it. + if (variant) { + ctx.fillStyle = this.base_color_fn(variant); + if (this.mode === 'Squish' || w_scale < ctx.canvas.manager.char_width_px) { + ctx.fillRect(draw_x_start, draw_y_start + 1, base_px, feature_height); + } + else { + ctx.fillText(variant, char_x_start, draw_y_start + row_height); + } + } + } + } + } +}); + return { Scaler: Scaler, SummaryTreePainter: SummaryTreePainter, @@ -1552,7 +1696,8 @@ ReadPainter: ReadPainter, RefBasedReadPainter: RefBasedReadPainter, ArcLinkedFeaturePainter: ArcLinkedFeaturePainter, - DiagonalHeatmapPainter: DiagonalHeatmapPainter + DiagonalHeatmapPainter: DiagonalHeatmapPainter, + VariantPainter: VariantPainter }; diff -r 7e800d8223d95326cb956fbad0ee89e5a6e84f58 -r b9971c68e9472a021f5b7a6784ee1244a85d5a1f static/scripts/viz/trackster/tracks.js --- a/static/scripts/viz/trackster/tracks.js +++ b/static/scripts/viz/trackster/tracks.js @@ -1712,7 +1712,7 @@ tool.run( // URL params. { - target_dataset_id: this.track.original_dataset_id, + target_dataset_id: this.track.dataset_id, action: 'rerun', tool_id: tool.id }, @@ -1741,7 +1741,7 @@ }), url_params = { - target_dataset_id: this.track.original_dataset_id, + target_dataset_id: this.track.dataset_id, action: 'rerun', tool_id: this.id, regions: [ @@ -2236,7 +2236,7 @@ * ----> ReferenceTrack * ----> FeatureTrack * -------> ReadTrack - * -------> VcfTrack + * ----> VariantTrack */ var Track = function(view, container, obj_dict) { // For now, track's container is always view. @@ -2251,7 +2251,9 @@ this.dataset = new data.Dataset({ id: obj_dict.dataset_id, hda_ldda: obj_dict.hda_ldda - }); + }); + this.dataset_id = this.dataset.get('id'); + this.hda_ldda = this.dataset.get('hda_ldda'); this.dataset_check_type = 'converted_datasets_state'; this.data_url_extra_params = {}; this.data_query_wait = ('data_query_wait' in obj_dict ? obj_dict.data_query_wait : DEFAULT_DATA_QUERY_WAIT); @@ -2548,8 +2550,8 @@ else if (this instanceof ReadTrack) { return "ReadTrack"; } - else if (this instanceof VcfTrack) { - return "VcfTrack"; + else if (this instanceof VariantTrack) { + return "VariantTrack"; } else if (this instanceof CompositeTrack) { return "CompositeTrack"; @@ -2679,6 +2681,7 @@ this.filters_available = false; this.tool = ('tool' in obj_dict && obj_dict.tool ? new Tool(this, obj_dict.tool, obj_dict.tool_state) : null); this.tile_cache = new visualization.Cache(TILE_CACHE_SIZE); + this.left_offset = 0; if (this.header_div) { // @@ -2917,8 +2920,9 @@ tile.html_elt.css("padding-top", ERROR_PADDING); } }); - } + } }, + /** * Retrieves from cache, draws, or sets up drawing for a single tile. Returns either a Tile object or a * jQuery.Deferred object that is fulfilled when tile can be drawn again. @@ -2967,7 +2971,7 @@ // HACK: this is FeatureTrack-specific. // If track mode is Auto, determine mode and update. var mode = track.mode; - if (mode === "Auto") { + if (mode === "Auto" && track.get_mode) { mode = track.get_mode(tile_data); track.update_auto_mode(mode); } @@ -3014,6 +3018,15 @@ }, /** + * Draw a track tile for summary tree data. + */ + _draw_summary_tree_tile: function(result, ctx, region, resolution, w_scale) { + var painter = new painters.SummaryTreePainter(result, region.get('start'), region.get('end'), this.prefs); + painter.draw(ctx, ctx.canvas.width, ctx.canvas.height, w_scale); + return new SummaryTreeTile(this, region, resolution, ctx.canvas, result.data, result.max); + }, + + /** * Draw a track tile. * @param result result from server * @param ctx canvas context to draw on @@ -3198,7 +3211,6 @@ // Init drawables; each drawable is a copy so that config/preferences // are independent of each other. Also init left offset. this.drawables = []; - this.left_offset = 0; if ('drawables' in obj_dict) { var drawable; for (var i = 0; i < obj_dict.drawables.length; i++) { @@ -3543,11 +3555,6 @@ this.mode = "Histogram"; TiledTrack.call(this, view, container, obj_dict); - this.hda_ldda = obj_dict.hda_ldda; - this.dataset_id = obj_dict.dataset_id; - this.original_dataset_id = this.dataset_id; - this.left_offset = 0; - // Define track configuration this.config = new DrawableConfig( { track: this, @@ -3690,12 +3697,6 @@ this.mode = "Heatmap"; TiledTrack.call(this, view, container, obj_dict); - // This all seems to be duplicated - this.hda_ldda = obj_dict.hda_ldda; - this.dataset_id = obj_dict.dataset_id; - this.original_dataset_id = this.dataset_id; - this.left_offset = 0; - // Define track configuration this.config = new DrawableConfig( { track: this, @@ -3746,12 +3747,11 @@ }, /** - * Draw LineTrack tile. + * Draw tile. */ draw_tile: function(result, ctx, mode, resolution, region, w_scale) { // Paint onto canvas. - var - canvas = ctx.canvas, + var canvas = ctx.canvas, painter = new painters.DiagonalHeatmapPainter(result.data, region.get('start'), region.get('end'), this.prefs, mode); painter.draw(ctx, canvas.width, canvas.height, w_scale); @@ -3806,9 +3806,6 @@ this.visible_height_px = this.config.values.height; this.container_div.addClass( "feature-track" ); - this.hda_ldda = obj_dict.hda_ldda; - this.dataset_id = obj_dict.dataset_id; - this.original_dataset_id = obj_dict.dataset_id; this.show_labels_scale = 0.001; this.showing_details = false; this.summary_draw_height = 30; @@ -3834,6 +3831,7 @@ this.painter = painters.LinkedFeaturePainter; } }, + /** * Actions to be taken before drawing. */ @@ -4053,10 +4051,7 @@ // Drawing the summary tree. if (mode === "summary_tree" || mode === "Coverage") { - // Paint summary tree into canvas - var painter = new painters.SummaryTreePainter(result, tile_low, tile_high, this.prefs); - painter.draw(ctx, canvas.width, canvas.height, w_scale); - return new SummaryTreeTile(track, region, resolution, canvas, result.data, result.max); + return this._draw_summary_tree_tile(result, ctx, region, resolution, w_scale); } // Handle row-by-row tracks @@ -4146,17 +4141,19 @@ } }); -var VcfTrack = function(view, container, obj_dict) { - FeatureTrack.call(this, view, container, obj_dict); +/** + * Track for displaying variant data. + */ +var VariantTrack = function(view, container, obj_dict) { + this.display_modes = ["Auto", "Coverage", "Dense", "Squish", "Pack"]; + TiledTrack.call(this, view, container, obj_dict); this.config = new DrawableConfig( { track: this, params: [ { key: 'name', label: 'Name', type: 'text', default_value: this.name }, - { key: 'block_color', label: 'Block color', type: 'color', default_value: util.get_random_color() }, - { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, - { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false }, - { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, + { key: 'show_sample_data', label: 'Show sample data', type: 'bool', default_value: true }, + { key: 'summary_height', label: 'Locus summary height', type: 'float', default_value: 20 }, { key: 'mode', type: 'string', default_value: this.mode, hidden: true } ], saved_values: obj_dict.prefs, @@ -4168,10 +4165,54 @@ }); this.prefs = this.config.values; - this.painter = painters.ReadPainter; + this.painter = painters.VariantPainter; + this.summary_draw_height = 30; + + // Maximum resolution is ~45 pixels/base, so use this size left offset to ensure that full + // variant is drawn when variant is at start of tile. + this.left_offset = 30; }; -extend(VcfTrack.prototype, Drawable.prototype, TiledTrack.prototype, FeatureTrack.prototype); +extend(VariantTrack.prototype, Drawable.prototype, TiledTrack.prototype, { + /** + * Draw tile. + */ + draw_tile: function(result, ctx, mode, resolution, region, w_scale) { + // Data could be summary tree data or variant data. + if (result.dataset_type === 'summary_tree') { + return this._draw_summary_tree_tile(result, ctx, region, resolution, w_scale); + } + else { // result.dataset_type === 'variant' + var view = this.view, + painter = new (this.painter)(result.data, region.get('start'), region.get('end'), this.prefs, mode, + function(b) { return view.get_base_color(b); }); + painter.draw(ctx, ctx.canvas.width, ctx.canvas.height, w_scale); + return new Tile(this, region, resolution, ctx.canvas, result.data); + } + }, + + /** + * Returns canvas height needed to display data; return value is an integer that denotes the + * number of pixels required. + */ + get_canvas_height: function(result, mode, w_scale, canvas_width) { + if (result.dataset_type === 'summary_tree') { + return this.summary_draw_height; + } + else { + var dummy_painter = new (this.painter)(result.data, null, null, this.prefs, mode); + return dummy_painter.get_required_height(); + } + }, + + /** + * Actions to be taken before drawing. + */ + before_draw: function() { + // Clear because this is set when drawing. + this.max_height_px = 0; + } +}); /** * Track that displays mapped reads. Track expects position data in 1-based, closed format, i.e. SAM/BAM format. @@ -4214,14 +4255,16 @@ /** * Objects that can be added to a view. */ -var addable_objects = { +var addable_objects = { + "CompositeTrack": CompositeTrack, + "DrawableGroup": DrawableGroup, + "DiagonalHeatmapTrack": DiagonalHeatmapTrack, + "FeatureTrack": FeatureTrack, "LineTrack": LineTrack, - "FeatureTrack": FeatureTrack, - "VcfTrack": VcfTrack, "ReadTrack": ReadTrack, - "DiagonalHeatmapTrack": DiagonalHeatmapTrack, - "CompositeTrack": CompositeTrack, - "DrawableGroup": DrawableGroup + "VariantTrack": VariantTrack, + // For backward compatibility, map vcf track to variant. + "VcfTrack": VariantTrack }; /** @@ -4252,7 +4295,7 @@ FeatureTrack: FeatureTrack, DiagonalHeatmapTrack: DiagonalHeatmapTrack, ReadTrack: ReadTrack, - VcfTrack: VcfTrack, + VariantTrack: VariantTrack, CompositeTrack: CompositeTrack, object_from_template: object_from_template }; 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)
-
commits-noreply@bitbucket.org