PageRenderTime 52ms CodeModel.GetById 28ms app.highlight 19ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/trackster.js

https://bitbucket.org/ialbert/galaxy-genetrack
JavaScript | 367 lines | 333 code | 19 blank | 15 comment | 38 complexity | d3d6bf66bc343aa78cce220f50f3cfdd MD5 | raw file
  1var DENSITY = 1000;
  2
  3var View = function( chr, length, low, high ) {
  4    this.chr = chr;
  5    this.length = length;
  6    this.low = low;
  7    this.high = high;
  8};
  9$.extend( View.prototype, {
 10    move: function ( new_low, new_high ) {
 11        this.low = Math.max( 0, Math.floor( new_low ) );
 12        this.high = Math.min( this.length, Math.ceil( new_high ) );
 13    },
 14    zoom_in: function ( factor ) {
 15        var center = ( this.low + this.high ) / 2;
 16        var range = this.high - this.low;
 17        var diff = range / factor / 2;
 18        this.low = Math.floor( center - diff );
 19        this.high = Math.ceil( center + diff );
 20        if (this.high - this.low < 1 ) {
 21            this.high = this.low + 1;
 22        }
 23    },
 24    zoom_out: function ( factor ) {
 25        var center = ( this.low + this.high ) / 2;
 26        var range = this.high - this.low;
 27        var diff = range * factor / 2;
 28        this.low = Math.floor( Math.max( 0, center - diff ) );
 29        this.high = Math.ceil( Math.min( this.length, center + diff ) );
 30    },
 31    left: function( factor ) {
 32        var range = this.high - this.low;
 33        var diff = Math.floor( range / factor );
 34        if ( this.low - diff < 0 ) {
 35            this.low = 0;
 36            this.high = this.low + range;
 37        } else {
 38            this.low -= diff;
 39            this.high -= diff;
 40        }
 41    },
 42    right: function ( factor ) {
 43        var range = this.high - this.low;
 44        var diff = Math.floor( range / factor );
 45        if ( this.high + diff > this.length ) {
 46            this.high = this.length;
 47            this.low = this.high - range;
 48        } else {
 49            this.low += diff;
 50            this.high += diff;
 51        }
 52    }
 53});
 54
 55var Track = function ( name, view, parent_element ) {
 56    this.name = name;
 57    this.view = view;
 58    this.parent_element = parent_element;
 59    this.make_container();
 60};
 61$.extend( Track.prototype, {
 62    make_container : function () {
 63        this.header_div = $("<div class='track-header'>");
 64        this.header_div.text( this.name );
 65        this.content_div = $("<div class='track-content'>");
 66        this.container_div = $("<div class='track'></div>");
 67        this.container_div.append( this.header_div );
 68        this.container_div.append( this.content_div );
 69        this.parent_element.append( this.container_div );
 70    }
 71});
 72
 73var TiledTrack = function( name, view, parent_element ) {
 74    Track.call( this, name, view, parent_element );
 75    // For caching
 76    this.last_resolution = null;
 77    this.last_w_scale = null;
 78    this.tile_cache = {};
 79};
 80$.extend( TiledTrack.prototype, Track.prototype, {
 81    draw: function() {
 82        var low = this.view.low,
 83            high = this.view.high,
 84            range = high - low;
 85
 86        var resolution = Math.pow( 10, Math.ceil( Math.log( range / DENSITY ) / Math.log( 10 ) ) );
 87        resolution = Math.max( resolution, 1 );
 88        resolution = Math.min( resolution, 100000 );
 89
 90	var parent_element = $("<div style='position: relative;'></div>");
 91        this.content_div.children( ":first" ).remove();
 92        this.content_div.append( parent_element );
 93
 94        var w = this.content_div.width(),
 95            h = this.content_div.height(),
 96	    w_scale = w / range,
 97	    old_tiles = {},
 98            new_tiles = {};
 99
100        // If resolution and scale are unchanged, try to reuse tiles
101        if ( this.last_resolution == resolution && this.last_w_scale == w_scale ) {
102            old_tiles = this.tile_cache;
103        }
104
105        var tile_element;
106        // Index of first tile that overlaps visible region
107        var tile_index = Math.floor( low / resolution / DENSITY );
108        var max_height = 0;
109        while ( ( tile_index * 1000 * resolution ) < high ) {
110            // Check in cache
111            if ( tile_index in old_tiles ) {
112                // console.log( "tile from cache" );
113                tile_element = old_tiles[tile_index];
114                var tile_low = tile_index * DENSITY * resolution;
115                tile_element.css( {
116                    left: ( tile_low - this.view.low ) * w_scale
117                });
118                // Our responsibility to move the element to the new parent
119                parent_element.append( tile_element );
120            } else {
121                // console.log( "new tile" );
122                tile_element = this.draw_tile( resolution, tile_index, parent_element, w_scale, h );
123            }
124            if ( tile_element ) {
125                new_tiles[tile_index] = tile_element;
126                max_height = Math.max( max_height, tile_element.height() );
127            }
128            tile_index += 1;
129        }
130
131        parent_element.css( "height", max_height );
132
133        this.last_resolution = resolution;
134        this.last_w_scale = w_scale;
135        this.tile_cache = new_tiles;
136    }
137});
138
139var DataCache = function( type, track, view ) {
140    this.type = type;
141    this.track = track;
142    this.view = view;
143    this.cache = Object();
144};
145$.extend( DataCache.prototype, {
146    get: function( resolution, position ) {
147        var cache = this.cache;
148        if ( ! ( cache[resolution] && cache[resolution][position] ) ) {
149            if ( ! cache[resolution] ) {
150                cache[resolution] = Object();
151            }
152            var low = position * DENSITY * resolution;
153            var high = ( position + 1 ) * DENSITY * resolution;
154            cache[resolution][position] = { state: "loading" };
155	    // use closure to preserve this and parameters for getJSON
156	    var fetcher = function (ref) {
157	      return function () {
158		$.getJSON( TRACKSTER_DATA_URL + ref.type, { chrom: ref.view.chr, low: low, high: high, dataset_id: ref.track.dataset_id }, function ( data ) {
159		  if( data == "pending" ) {
160		    setTimeout( fetcher, 5000 );
161		  } else {
162		    cache[resolution][position] = { state: "loaded", values: data };
163		  }
164		  $(document).trigger( "redraw" );
165		});
166	      };
167	    }(this);
168	    fetcher();
169        }
170	return cache[resolution][position];
171    }
172});
173
174var LineTrack = function ( name, view, parent_element, dataset_id ) {
175    Track.call( this, name, view, parent_element );
176    this.container_div.addClass( "line-track" );
177    this.dataset_id = dataset_id;
178    this.cache = new DataCache( "", this, view );
179};
180$.extend( LineTrack.prototype, TiledTrack.prototype, {
181    make_container: function () {
182        Track.prototype.make_container.call( this );
183        this.content_div.css( "height", 100 );
184    },
185    draw_tile: function( resolution, tile_index, parent_element, w_scale, h_scale ) {
186        var tile_low = tile_index * DENSITY * resolution,
187            tile_high = ( tile_index + 1 ) * DENSITY * resolution,
188            tile_length = DENSITY * resolution;
189        var chunk = this.cache.get( resolution, tile_index );
190        var element;
191        if ( chunk.state == "loading" ) {
192            element = $("<div class='loading tile'></div>");
193        } else {
194            element = $("<canvas class='tile'></canvas>");
195        }
196        element.css( {
197            position: "absolute",
198            top: 0,
199            left: ( tile_low - this.view.low ) * w_scale,
200            width: Math.ceil( tile_length * w_scale ),
201            height: 100
202        });
203        parent_element.append( element );
204        // Chunk is still loading, do noting
205        if ( chunk.state == "loading" ) {
206            in_path = false;
207            return null;
208        }
209        var canvas = element;
210        canvas.get(0).width = canvas.width();
211        canvas.get(0).height = canvas.height();
212        var ctx = canvas.get(0).getContext("2d");
213        var in_path = false;
214        ctx.beginPath();
215        var data = chunk.values;
216        for ( var i = 0; i < data.length - 1; i++ ) {
217            var x1 = data[i][0] - tile_low;
218            var y1 = data[i][1];
219            var x2 = data[i+1][0] - tile_low;
220            var y2 = data[i+1][1];
221	    console.log( x1, y1, x2, y2 );
222            // Missing data causes us to stop drawing
223            if ( isNaN( y1 ) || isNaN( y2 ) ) {
224                in_path = false;
225            } else {
226                // Translate
227                x1 = x1 * w_scale;
228                x2 = x2 * w_scale;
229                y1 = h_scale - y1 * ( h_scale );
230                y2 = h_scale - y2 * ( h_scale );
231                if ( in_path ) {
232                    ctx.lineTo( x1, y1, x2, y2 );
233                } else {
234                    ctx.moveTo( x1, y1, x2, y2 );
235                    in_path = true;
236                }
237           }
238        }
239        ctx.stroke();
240        return element;
241    }
242});
243
244var LabelTrack = function ( view, parent_element ) {
245    Track.call( this, null, view, parent_element );
246    this.container_div.addClass( "label-track" );
247};
248$.extend( LabelTrack.prototype, Track.prototype, {
249    draw: function() {
250        var view = this.view,
251            range = view.high - view.low,
252            tickDistance = Math.floor( Math.pow( 10, Math.floor( Math.log( range ) / Math.log( 10 ) ) ) ),
253            position = Math.floor( view.low / tickDistance ) * tickDistance,
254	    width = this.content_div.width(),
255	    new_div = $("<div style='position: relative; height: 1.3em;'></div>");
256        while ( position < view.high ) {
257            var screenPosition = ( position - view.low ) / range * width;
258            new_div.append( $("<div class='label'>" + position + "</div>").css( {
259                position: "absolute",
260                // Reduce by one to account for border
261                left: screenPosition - 1
262            }) );
263            position += tickDistance;
264        }
265        this.content_div.children( ":first" ).remove();
266        this.content_div.append( new_div );
267    }
268});
269
270var itemHeight = 13,
271    itemPad = 3,
272    thinHeight = 7,
273    thinOffset = 3;
274
275var FeatureTrack = function ( name, view, parent_element,dataset_id ) {
276    Track.call( this, name, view, parent_element );
277    this.container_div.addClass( "feature-track" );
278    this.dataset_id = dataset_id;
279    this.cache = new DataCache( "", this, view );
280};
281$.extend( FeatureTrack.prototype, TiledTrack.prototype, {
282    get_data_async: function() {
283        var track = this;
284        $.getJSON( "data", { chr: this.view.chr, dataset_id: this.dataset_id }, function ( data ) {
285            track.values = data;
286            track.draw();
287        });
288    },
289    draw_tile: function( resolution, tile_index, parent_element, w_scale, h_scale ) {
290        var tile_low = tile_index * DENSITY * resolution,
291            tile_high = ( tile_index + 1 ) * DENSITY * resolution,
292            tile_length = DENSITY * resolution;
293
294        var view = this.view,
295            range = view.high - view.low,
296            width = this.content_div.width(),
297            slots = [],
298            new_div = $("<div class='tile' style='position: relative;'></div>");
299
300        var chunk = this.cache.get( resolution, tile_index );
301        if ( chunk.state == "loading" ) {
302	  parent_element.addClass("loading");
303          return null;
304        } else {
305	  parent_element.removeClass("loading");
306	}
307        var values = chunk.values;
308
309        for ( var index in values ) {
310            var value = values[index];
311            var start = value[1], end = value[2], strand = value[5];
312            // Determine slot based on entire feature and label
313            var screenStart = ( start - tile_low ) * w_scale;
314            var screenEnd = ( end - tile_low ) * w_scale;
315            var screenWidth = screenEnd - screenStart;
316            var screenStartWithLabel = screenStart;
317            // Determine slot
318            var slot = slots.length;
319            for ( i in slots ) {
320                if ( slots[i] < screenStartWithLabel ) {
321                    slot = i;
322                    break;
323                }
324            }
325            slots[slot] = Math.ceil( screenEnd );
326            var feature_div = $("<div class='feature'></div>").css( {
327                position: 'absolute',
328                left: screenStart,
329                top: (slot*(itemHeight+itemPad)),
330                height: itemHeight,
331                width: Math.max( screenWidth, 1 )
332            });
333            new_div.append( feature_div );
334        }
335        new_div.css( {
336            position: "absolute",
337            top: 0,
338            left: ( tile_low - this.view.low ) * w_scale,
339            width: Math.ceil( tile_length * w_scale ),
340            height: slots.length * ( itemHeight + itemPad ) + itemPad
341        });
342        parent_element.append( new_div );
343        return new_div;
344    }
345});
346
347var TrackLayout = function ( view ) {
348    this.view = view;
349    this.tracks = [];
350};
351$.extend( TrackLayout.prototype, {
352    add: function ( track ) {
353        this.tracks.push( track );
354    },
355    redraw : function () {
356        for ( var index in this.tracks ) {
357	  this.tracks[index].draw();
358        }
359        // Overview
360        $("#overview-box").css( {
361            left: ( this.view.low / this.view.length ) * $("#overview-viewport").width(),
362            width: Math.max( 1, ( ( this.view.high - this.view.low ) / this.view.length ) * $("#overview-viewport").width() )
363        }).show();
364        $("#low").text( this.view.low );
365        $("#high").text( this.view.high );
366    }
367});