diff --git a/lib/galaxy/datatypes/interval.py b/lib/galaxy/datatypes/interval.py index 0bfc5949a18..69c77a553db 100644 --- a/lib/galaxy/datatypes/interval.py +++ b/lib/galaxy/datatypes/interval.py @@ -1298,7 +1298,32 @@ class ENCODEPeak( Interval ): return False def get_track_type( self ): - return "FeatureTrack", {"data": "tabix", "index": "summary_tree"} + return "FeatureTrack", {"data": "tabix", "index": "summary_tree"} + +class ChromatinInteractions( Interval ): + ''' + Chromatin interactions obtained from 3C/5C/Hi-C experiments. + ''' + + file_ext = "chrint" + + column_names = [ 'Chrom', 'Start1', 'End1', 'Start2', 'End2', 'Value' ] + + """Add metadata elements""" + MetadataElement( name="chromCol", default=1, desc="Chrom column", param=metadata.ColumnParameter ) + MetadataElement( name="start1Col", default=2, desc="Start1 column", param=metadata.ColumnParameter ) + MetadataElement( name="end1Col", default=3, desc="End1 column", param=metadata.ColumnParameter ) + MetadataElement( name="start2Col", default=2, desc="Start2 column", param=metadata.ColumnParameter ) + MetadataElement( name="end2Col", default=3, desc="End2 column", param=metadata.ColumnParameter ) + MetadataElement( name="columns", default=3, desc="Number of columns", readonly=True, visible=False ) + + def sniff( self, filename ): + return False + + def get_track_type( self ): + return "DiagonalHeatmapTrack", {"data": "tabix", "index": "summary_tree"} + + if __name__ == '__main__': import doctest, sys diff --git a/lib/galaxy/visualization/tracks/data_providers.py b/lib/galaxy/visualization/tracks/data_providers.py index 65804e5edb5..0f2f52aad33 100644 --- a/lib/galaxy/visualization/tracks/data_providers.py +++ b/lib/galaxy/visualization/tracks/data_providers.py @@ -19,7 +19,7 @@ from galaxy.util.lrucache import LRUCache from galaxy.visualization.tracks.summary import * import galaxy_utils.sequence.vcf from galaxy.datatypes.tabular import Vcf -from galaxy.datatypes.interval import Interval, Bed, Gff, Gtf, ENCODEPeak +from galaxy.datatypes.interval import Interval, Bed, Gff, Gtf, ENCODEPeak, ChromatinInteractions from pysam import csamtools, ctabix @@ -146,6 +146,9 @@ class TracksDataProvider( object ): { 'name' : attrs[ 'name' ], 'type' : column_types[viz_col_index], \ 'index' : attrs[ 'index' ] } ) return filters + + def get_default_max_vals( self ): + return 5000; # # -- Base mixins and providers -- @@ -1268,6 +1271,67 @@ class ENCODEPeakTabixDataProvider( TabixDataProvider, ENCODEPeakDataProvider ): 'tool_id': 'Filter1', 'tool_exp_name': 'c9' } ) return filters + +# +# -- ChromatinInteraction data providers -- +# +class ChromatinInteractionsDataProvider( TracksDataProvider ): + def process_data( self, iterator, start_val=0, max_vals=None, **kwargs ): + """ + Provides + """ + + rval = [] + message = None + for count, line in enumerate( iterator ): + if count < start_val: + continue + if max_vals and count-start_val >= max_vals: + message = ERROR_MAX_VALS % ( max_vals, "interactions" ) + break + + feature = line.split() + length = len( feature ) + + s1 = int( feature[1] ), + e1 = int( feature[2] ), + c = feature[3], + s2 = int( feature[4] ), + e2 = int( feature[5] ), + v = float( feature[6] ) + + # Feature initialization. + payload = [ + # GUID is just a hash of the line + hash( line ), + # Add start1, end1, chr2, start2, end2, value. + s1, e1, c, s2, e2, v + ] + + rval.append( payload ) + + return { 'data': rval, 'message': message } + + def get_default_max_vals( self ): + return 50000; + +class ChromatinInteractionsTabixDataProvider( TabixDataProvider, ChromatinInteractionsDataProvider ): + def get_iterator( self, chrom, start, end ): + """ + """ + # Modify start as needed to get earlier interactions with start region. + start = max( 0, int( start) - 1000000 ) + def filter( iter ): + for line in iter: + feature = line.split() + s1 = int( feature[1] ), + e1 = int( feature[2] ), + c = feature[3] + s2 = int( feature[4] ), + e2 = int( feature[5] ), + if ( ( c == chrom ) and ( s1 < end and e1 > start ) and ( s2 < end and e2 > start ) ): + yield line + return filter( TabixDataProvider.get_iterator( self, chrom, start, end ) ) # # -- Helper methods. -- @@ -1283,7 +1347,9 @@ dataset_type_name_to_data_provider = { Gtf: GtfTabixDataProvider, ENCODEPeak: ENCODEPeakTabixDataProvider, Interval: IntervalTabixDataProvider, - "default" : TabixDataProvider }, + ChromatinInteractions: ChromatinInteractionsTabixDataProvider, + "default" : TabixDataProvider + }, "interval_index": IntervalIndexDataProvider, "bai": BamDataProvider, "bam": SamDataProvider, diff --git a/lib/galaxy/web/controllers/tracks.py b/lib/galaxy/web/controllers/tracks.py index 98944d14ea4..b6a89d10227 100644 --- a/lib/galaxy/web/controllers/tracks.py +++ b/lib/galaxy/web/controllers/tracks.py @@ -345,7 +345,7 @@ class TracksController( BaseUIController, UsesVisualizationMixin, UsesHistoryDat return { "status": messages.DATA, "valid_chroms": valid_chroms } @web.json - def data( self, trans, hda_ldda, dataset_id, chrom, low, high, start_val=0, max_vals=5000, **kwargs ): + def data( self, trans, hda_ldda, dataset_id, chrom, low, high, start_val=0, max_vals=None, **kwargs ): """ Provides a block of data from a dataset. """ @@ -401,6 +401,10 @@ class TracksController( BaseUIController, UsesVisualizationMixin, UsesHistoryDat deps = dataset.get_converted_dataset_deps( trans, tracks_dataset_type ) data_provider = data_provider_class( converted_dataset=converted_dataset, original_dataset=dataset, dependencies=deps ) + # Allow max_vals top be data provider set if not passed + if max_vals is None: + max_vals = data_provider.get_default_max_vals() + # Get and return data from data_provider. result = data_provider.get_data( chrom, low, high, int( start_val ), int( max_vals ), **kwargs ) result.update( { 'dataset_type': tracks_dataset_type, 'extra_info': extra_info } ) diff --git a/static/scripts/viz/trackster.js b/static/scripts/viz/trackster.js index afcefea9dd7..f9caeda81f1 100644 --- a/static/scripts/viz/trackster.js +++ b/static/scripts/viz/trackster.js @@ -4346,6 +4346,84 @@ extend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, { } }); +var DiagonalHeatmapTrack = function (view, container, obj_dict) { + var track = this; + this.display_modes = ["Heatmap"]; + 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, + params: [ + { key: 'name', label: 'Name', type: 'text', default_value: this.name }, + { key: 'pos_color', label: 'Positive Color', type: 'color', default_value: "4169E1" }, + { key: 'negative_color', label: 'Negative Color', type: 'color', default_value: "FF8C00" }, + { key: 'min_value', label: 'Min Value', type: 'float', default_value: 0 }, + { key: 'max_value', label: 'Max Value', type: 'float', default_value: 1 }, + { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, + { key: 'height', type: 'int', default_value: 500, hidden: true } + ], + saved_values: obj_dict.prefs, + onchange: function() { + track.set_name(track.prefs.name); + track.vertical_range = track.prefs.max_value - track.prefs.min_value; + track.set_min_value(track.prefs.min_value); + track.set_max_value(track.prefs.max_value); + } + }); + + this.prefs = this.config.values; + this.visible_height_px = this.config.values.height; + this.vertical_range = this.config.values.max_value - this.config.values.min_value; +}; +extend(DiagonalHeatmapTrack.prototype, Drawable.prototype, TiledTrack.prototype, { + /** + * Action to take during resize. + */ + on_resize: function() { + this.request_draw(true); + }, + /** + * Set track minimum value. + */ + set_min_value: function(new_val) { + this.prefs.min_value = new_val; + this.tile_cache.clear(); + this.request_draw(); + }, + /** + * Set track maximum value. + */ + set_max_value: function(new_val) { + this.prefs.max_value = new_val; + this.tile_cache.clear(); + this.request_draw(); + }, + + /** + * Draw LineTrack tile. + */ + draw_tile: function(result, ctx, mode, resolution, tile_index, w_scale) { + // Paint onto canvas. + var + canvas = ctx.canvas, + tile_bounds = this._get_tile_bounds(tile_index, resolution), + tile_low = tile_bounds[0], + tile_high = tile_bounds[1], + painter = new painters.DiagonalHeatmapPainter(result.data, tile_low, tile_high, this.prefs, mode); + painter.draw(ctx, canvas.width, canvas.height, w_scale); + + return new Tile(this, tile_index, resolution, canvas, result.data); + } +}); + var FeatureTrack = function(view, container, obj_dict) { // // Preinitialization: do things that need to be done before calling Track and TiledTrack @@ -4878,6 +4956,7 @@ exports.View = View; exports.DrawableGroup = DrawableGroup; exports.LineTrack = LineTrack; exports.FeatureTrack = FeatureTrack; +exports.DiagonalHeatmapTrack = DiagonalHeatmapTrack; exports.ReadTrack = ReadTrack; exports.VcfTrack = VcfTrack; exports.CompositeTrack = CompositeTrack; @@ -6041,12 +6120,224 @@ extend(ArcLinkedFeaturePainter.prototype, FeaturePainter.prototype, LinkedFeatur } }); +// Color stuff from less.js + +var Color = function (rgb, a) { + /** + * The end goal here, is to parse the arguments + * into an integer triplet, such as `128, 255, 0` + * + * This facilitates operations and conversions. + */ + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +Color.prototype = { + eval: function () { return this }, + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + toCSS: function () { + if (this.alpha < 1.0) { + return "rgba(" + this.rgb.map(function (c) { + return Math.round(c); + }).concat(this.alpha).join(', ') + ")"; + } else { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + }, + + mix: function (color2, weight) { + color1 = this; + + var p = weight; // .value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new Color(rgb, alpha); + } +}; + + +// End colors from less.js + +var LinearRamp = function( start_color, end_color, start_value, end_value ) { + /** + * Simple linear gradient + */ + this.start_color = new Color( start_color ); + this.end_color = new Color( end_color ); + this.start_value = start_value; + this.end_value = end_value; + this.value_range = end_value - start_value; +} +LinearRamp.prototype.map_value = function( value ) { + value = Math.max( value, this.start_value ); + value = Math.min( value, this.end_value ); + value = ( value - this.start_value ) / this.value_range; + // HACK: just red for now + // return "hsl(0,100%," + (value * 100) + "%)" + return this.start_color.mix( this.end_color, 1 - value ).toCSS(); +} + +var SplitRamp = function( start_color, middle_color, end_color, start_value, end_value ) { + /** + * Two gradients split away from 0 + */ + this.positive_ramp = new LinearRamp( middle_color, end_color, 0, end_value ); + this.negative_ramp = new LinearRamp( middle_color, start_color, 0, -start_value ); + this.start_value = start_value; + this.end_value = end_value; +} +SplitRamp.prototype.map_value = function( value ) { + value = Math.max( value, this.start_value ); + value = Math.min( value, this.end_value ); + if ( value >= 0 ) { + return this.positive_ramp.map_value( value ) + } else { + return this.negative_ramp.map_value( -value ) + } +} + +var DiagonalHeatmapPainter = function(data, view_start, view_end, prefs, mode) { + Painter.call( this, data, view_start, view_end, prefs, mode ); + if ( this.prefs.min_value === undefined ) { + var min_value = Infinity; + for (var i = 0, len = this.data.length; i < len; i++) { + min_value = Math.min( min_value, this.data[i][5] ); + } + this.prefs.min_value = min_value; + } + if ( this.prefs.max_value === undefined ) { + var max_value = -Infinity; + for (var i = 0, len = this.data.length; i < len; i++) { + max_value = Math.max( max_value, this.data[i][5] ); + } + this.prefs.max_value = max_value; + } +}; + +DiagonalHeatmapPainter.prototype.default_prefs = { + min_value: undefined, + max_value: undefined, + mode: "Heatmap", + pos_color: "4169E1", + neg_color: "FF8C00" +}; + +DiagonalHeatmapPainter.prototype.draw = function(ctx, width, height, w_scale) { + var + min_value = this.prefs.min_value, + max_value = this.prefs.max_value, + value_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + mode = this.mode, + data = this.data, + invsqrt2 = 1 / Math.sqrt(2); + + var ramp = ( new SplitRamp( this.prefs.neg_color, "FFFFFF", this.prefs.pos_color, min_value, max_value ) ); + + var d, s1, e1, s2, e2, value; + + var scale = function( p ) { return ( p - view_start ) * w_scale }; + + ctx.save(); + + // Draw into triangle, then rotate and scale + ctx.rotate(-45 * Math.PI / 180); + ctx.scale( invsqrt2, invsqrt2 ); + + // Paint track. + for (var i = 0, len = data.length; i < len; i++) { + + d = data[i]; + + // Ensure the cell is visible + // if ( ) + + s1 = scale( d[1] ); + e1 = scale( d[2] ); + s2 = scale( d[4] ); + e2 = scale( d[5] ); + value = d[6]; + + ctx.fillStyle = ( ramp.map_value( value ) ) + + ctx.fillRect( s1, s2, ( e1 - s1 ), ( e2 - s2 ) ); + } + + ctx.restore(); +}; + exports.Scaler = Scaler; exports.SummaryTreePainter = SummaryTreePainter; exports.LinePainter = LinePainter; exports.LinkedFeaturePainter = LinkedFeaturePainter; exports.ReadPainter = ReadPainter; exports.ArcLinkedFeaturePainter = ArcLinkedFeaturePainter; +exports.DiagonalHeatmapPainter = DiagonalHeatmapPainter; // End painters_module encapsulation }; diff --git a/static/scripts/viz/trackster_ui.js b/static/scripts/viz/trackster_ui.js index a41be387d44..6cbd13a1580 100644 --- a/static/scripts/viz/trackster_ui.js +++ b/static/scripts/viz/trackster_ui.js @@ -67,6 +67,7 @@ var addable_objects = { "FeatureTrack": FeatureTrack, "VcfTrack": VcfTrack, "ReadTrack": ReadTrack, + // "DiagonalHeatmapTrack": DiagonalHeatmapTrack, "CompositeTrack": CompositeTrack, "DrawableGroup": DrawableGroup };