/static/scripts/trackster.js
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});