/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

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