PageRenderTime 54ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/viz/visualization.js

https://bitbucket.org/jmchilton/galaxy-central-reports-config-enhancements
JavaScript | 766 lines | 507 code | 80 blank | 179 comment | 48 complexity | 613a34683d988e9c229e422c79d38347 MD5 | raw file
  1. define( ["mvc/data", "viz/trackster/util" ], function(data, util) {
  2. /**
  3. * Model, view, and controller objects for Galaxy visualization framework.
  4. *
  5. * Required libraries: Backbone, jQuery
  6. *
  7. * Models have no references to views, instead using events to indicate state
  8. * changes; this is advantageous because multiple views can use the same object
  9. * and models can be used without views.
  10. */
  11. /**
  12. * Helper to determine if object is jQuery deferred.
  13. */
  14. var is_deferred = function ( d ) {
  15. return ( 'isResolved' in d );
  16. };
  17. // --------- Models ---------
  18. /**
  19. * Canvas manager is used to create canvases, for browsers, this deals with
  20. * backward comparibility using excanvas, as well as providing a pattern cache
  21. */
  22. var CanvasManager = function(default_font) {
  23. this.default_font = default_font !== undefined ? default_font : "9px Monaco, Lucida Console, monospace";
  24. this.dummy_canvas = this.new_canvas();
  25. this.dummy_context = this.dummy_canvas.getContext('2d');
  26. this.dummy_context.font = this.default_font;
  27. this.char_width_px = this.dummy_context.measureText("A").width;
  28. this.patterns = {};
  29. // FIXME: move somewhere to make this more general
  30. this.load_pattern( 'right_strand', "/visualization/strand_right.png" );
  31. this.load_pattern( 'left_strand', "/visualization/strand_left.png" );
  32. this.load_pattern( 'right_strand_inv', "/visualization/strand_right_inv.png" );
  33. this.load_pattern( 'left_strand_inv', "/visualization/strand_left_inv.png" );
  34. };
  35. _.extend( CanvasManager.prototype, {
  36. load_pattern: function( key, path ) {
  37. var patterns = this.patterns,
  38. dummy_context = this.dummy_context,
  39. image = new Image();
  40. image.src = galaxy_paths.attributes.image_path + path;
  41. image.onload = function() {
  42. patterns[key] = dummy_context.createPattern( image, "repeat" );
  43. };
  44. },
  45. get_pattern: function( key ) {
  46. return this.patterns[key];
  47. },
  48. new_canvas: function() {
  49. var canvas = $("<canvas/>")[0];
  50. // If using excanvas in IE, we need to explicately attach the canvas
  51. // methods to the DOM element
  52. if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); }
  53. // Keep a reference back to the manager
  54. canvas.manager = this;
  55. return canvas;
  56. }
  57. });
  58. /**
  59. * Generic cache that handles key/value pairs.
  60. */
  61. var Cache = Backbone.Model.extend({
  62. defaults: {
  63. num_elements: 20,
  64. obj_cache: null,
  65. key_ary: null
  66. },
  67. initialize: function(options) {
  68. this.clear();
  69. },
  70. get_elt: function(key) {
  71. var obj_cache = this.attributes.obj_cache,
  72. key_ary = this.attributes.key_ary,
  73. index = key_ary.indexOf(key);
  74. if (index !== -1) {
  75. if (obj_cache[key].stale) {
  76. // Object is stale, so remove key and object.
  77. key_ary.splice(index, 1);
  78. delete obj_cache[key];
  79. }
  80. else {
  81. this.move_key_to_end(key, index);
  82. }
  83. }
  84. return obj_cache[key];
  85. },
  86. set_elt: function(key, value) {
  87. var obj_cache = this.attributes.obj_cache,
  88. key_ary = this.attributes.key_ary,
  89. num_elements = this.attributes.num_elements;
  90. if (!obj_cache[key]) {
  91. if (key_ary.length >= num_elements) {
  92. // Remove first element
  93. var deleted_key = key_ary.shift();
  94. delete obj_cache[deleted_key];
  95. }
  96. key_ary.push(key);
  97. }
  98. obj_cache[key] = value;
  99. return value;
  100. },
  101. // Move key to end of cache. Keys are removed from the front, so moving a key to the end
  102. // delays the key's removal.
  103. move_key_to_end: function(key, index) {
  104. this.attributes.key_ary.splice(index, 1);
  105. this.attributes.key_ary.push(key);
  106. },
  107. clear: function() {
  108. this.attributes.obj_cache = {};
  109. this.attributes.key_ary = [];
  110. },
  111. // Returns the number of elements in the cache.
  112. size: function() {
  113. return this.attributes.key_ary.length;
  114. }
  115. });
  116. /**
  117. * Data manager for genomic data. Data is connected to and queryable by genomic regions.
  118. */
  119. var GenomeDataManager = Cache.extend({
  120. defaults: _.extend({}, Cache.prototype.defaults, {
  121. dataset: null,
  122. filters_manager: null,
  123. data_type: "data",
  124. genome_wide_summary_data: null,
  125. data_mode_compatible: function(entry, mode) { return true; },
  126. can_subset: function(entry) { return false; }
  127. }),
  128. /**
  129. * Returns deferred that resolves to true when dataset is ready (or false if dataset
  130. * cannot be used).
  131. */
  132. data_is_ready: function() {
  133. var dataset = this.get('dataset'),
  134. ready_deferred = $.Deferred(),
  135. ss_deferred = new util.ServerStateDeferred({
  136. ajax_settings: {
  137. url: this.get('dataset').url(),
  138. data: {
  139. hda_ldda: dataset.get('hda_ldda'),
  140. data_type: 'state'
  141. },
  142. dataType: "json"
  143. },
  144. interval: 5000,
  145. success_fn: function(response) { return response !== "pending"; }
  146. });
  147. $.when(ss_deferred.go()).then(function(response) {
  148. ready_deferred.resolve(response === "ok" || response === "data" );
  149. });
  150. return ready_deferred;
  151. },
  152. /**
  153. * Perform a feature search from server; returns Deferred object that resolves when data is available.
  154. */
  155. search_features: function(query) {
  156. var dataset = this.get('dataset'),
  157. params = {
  158. query: query,
  159. dataset_id: dataset.id,
  160. hda_ldda: dataset.get('hda_ldda'),
  161. data_type: 'features'
  162. };
  163. return $.getJSON(dataset.url(), params);
  164. },
  165. /**
  166. * Load data from server; returns Deferred object that resolves when data is available.
  167. */
  168. load_data: function(region, mode, resolution, extra_params) {
  169. // Setup data request params.
  170. var params = {
  171. "data_type": this.get('data_type'),
  172. "chrom": region.get('chrom'),
  173. "low": region.get('start'),
  174. "high": region.get('end'),
  175. "mode": mode,
  176. "resolution": resolution
  177. },
  178. dataset = this.get('dataset');
  179. // ReferenceDataManager does not have dataset.
  180. if (dataset) {
  181. params.dataset_id = dataset.id;
  182. params.hda_ldda = dataset.get('hda_ldda');
  183. }
  184. $.extend(params, extra_params);
  185. // Add track filters to params.
  186. var filters_manager = this.get('filters_manager');
  187. if (filters_manager) {
  188. var filter_names = [];
  189. var filters = filters_manager.filters;
  190. for (var i = 0; i < filters.length; i++) {
  191. filter_names.push(filters[i].name);
  192. }
  193. params.filter_cols = JSON.stringify(filter_names);
  194. }
  195. // Do request.
  196. var manager = this;
  197. return $.getJSON(dataset.url(), params, function (result) {
  198. manager.set_data(region, result);
  199. });
  200. },
  201. /**
  202. * Get data from dataset.
  203. */
  204. get_data: function(region, mode, resolution, extra_params) {
  205. // Debugging:
  206. //console.log("get_data", low, high, mode);
  207. /*
  208. console.log("cache contents:")
  209. for (var i = 0; i < this.key_ary.length; i++) {
  210. console.log("\t", this.key_ary[i], this.obj_cache[this.key_ary[i]]);
  211. }
  212. */
  213. // Look for entry and return if it's a deferred or if data available is compatible with mode.
  214. var entry = this.get_elt(region);
  215. if ( entry &&
  216. ( is_deferred(entry) || this.get('data_mode_compatible')(entry, mode) ) ) {
  217. return entry;
  218. }
  219. //
  220. // Look in cache for data that can be used. Data can be reused if it
  221. // has the requested data and is not summary tree and has details.
  222. // TODO: this logic could be improved if the visualization knew whether
  223. // the data was "index" or "data."
  224. //
  225. var key_ary = this.get('key_ary'),
  226. obj_cache = this.get('obj_cache'),
  227. key, entry_region;
  228. for (var i = 0; i < key_ary.length; i++) {
  229. key = key_ary[i];
  230. entry_region = new GenomeRegion({from_str: key});
  231. if (entry_region.contains(region)) {
  232. // This entry has data in the requested range. Return if data
  233. // is compatible and can be subsetted.
  234. entry = obj_cache[key];
  235. if ( is_deferred(entry) ||
  236. ( this.get('data_mode_compatible')(entry, mode) && this.get('can_subset')(entry) ) ) {
  237. this.move_key_to_end(key, i);
  238. return entry;
  239. }
  240. }
  241. }
  242. // Load data from server. The deferred is immediately saved until the
  243. // data is ready, it then replaces itself with the actual data.
  244. entry = this.load_data(region, mode, resolution, extra_params);
  245. this.set_data(region, entry);
  246. return entry;
  247. },
  248. /**
  249. * Alias for set_elt for readbility.
  250. */
  251. set_data: function(region, entry) {
  252. this.set_elt(region, entry);
  253. },
  254. /** "Deep" data request; used as a parameter for DataManager.get_more_data() */
  255. DEEP_DATA_REQ: "deep",
  256. /** "Broad" data request; used as a parameter for DataManager.get_more_data() */
  257. BROAD_DATA_REQ: "breadth",
  258. /**
  259. * Gets more data for a region using either a depth-first or a breadth-first approach.
  260. */
  261. get_more_data: function(region, mode, resolution, extra_params, req_type) {
  262. //
  263. // Get current data from cache and mark as stale.
  264. //
  265. var cur_data = this.get_elt(region);
  266. if ( !(cur_data && this.get('data_mode_compatible')(cur_data, mode)) ) {
  267. console.log("ERROR: no current data for: ", dataset, region.toString(), mode, resolution, extra_params);
  268. return;
  269. }
  270. cur_data.stale = true;
  271. //
  272. // Set parameters based on request type.
  273. //
  274. var query_low = region.get('start');
  275. if (req_type === this.DEEP_DATA_REQ) {
  276. // Use same interval but set start_val to skip data that's already in cur_data.
  277. $.extend(extra_params, {start_val: cur_data.data.length + 1});
  278. }
  279. else if (req_type === this.BROAD_DATA_REQ) {
  280. // To get past an area of extreme feature depth, set query low to be after either
  281. // (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
  282. query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
  283. }
  284. var query_region = region.copy().set('start', query_low);
  285. //
  286. // Get additional data, append to current data, and set new data. Use a custom deferred object
  287. // to signal when new data is available.
  288. //
  289. var
  290. data_manager = this,
  291. new_data_request = this.load_data(query_region, mode, resolution, extra_params),
  292. new_data_available = $.Deferred();
  293. // load_data sets cache to new_data_request, but use custom deferred object so that signal and data
  294. // is all data, not just new data.
  295. this.set_data(region, new_data_available);
  296. $.when(new_data_request).then(function(result) {
  297. // Update data and message.
  298. if (result.data) {
  299. result.data = cur_data.data.concat(result.data);
  300. if (result.max_low) {
  301. result.max_low = cur_data.max_low;
  302. }
  303. if (result.message) {
  304. // HACK: replace number in message with current data length. Works but is ugly.
  305. result.message = result.message.replace(/[0-9]+/, result.data.length);
  306. }
  307. }
  308. data_manager.set_data(region, result);
  309. new_data_available.resolve(result);
  310. });
  311. return new_data_available;
  312. },
  313. /**
  314. * Get data from the cache.
  315. */
  316. get_elt: function(region) {
  317. return Cache.prototype.get_elt.call(this, region.toString());
  318. },
  319. /**
  320. * Sets data in the cache.
  321. */
  322. set_elt: function(region, result) {
  323. return Cache.prototype.set_elt.call(this, region.toString(), result);
  324. }
  325. });
  326. var ReferenceTrackDataManager = GenomeDataManager.extend({
  327. load_data: function(low, high, mode, resolution, extra_params) {
  328. if (resolution > 1) {
  329. // Now that data is pre-fetched before draw, we don't load reference tracks
  330. // unless it's at the bottom level.
  331. return { data: null };
  332. }
  333. return GenomeDataManager.prototype.load_data.call(this, low, high, mode, resolution, extra_params);
  334. }
  335. });
  336. /**
  337. * A genome build.
  338. */
  339. var Genome = Backbone.Model.extend({
  340. defaults: {
  341. name: null,
  342. key: null,
  343. chroms_info: null
  344. },
  345. get_chroms_info: function() {
  346. return this.attributes.chroms_info.chrom_info;
  347. }
  348. });
  349. /**
  350. * A genomic region.
  351. */
  352. var GenomeRegion = Backbone.RelationalModel.extend({
  353. defaults: {
  354. chrom: null,
  355. start: 0,
  356. end: 0,
  357. DIF_CHROMS: 1000,
  358. BEFORE: 1001,
  359. CONTAINS: 1002,
  360. OVERLAP_START: 1003,
  361. OVERLAP_END: 1004,
  362. CONTAINED_BY: 1005,
  363. AFTER: 1006
  364. },
  365. /**
  366. * If from_str specified, use it to initialize attributes.
  367. */
  368. initialize: function(options) {
  369. if (options.from_str) {
  370. var pieces = options.from_str.split(':'),
  371. chrom = pieces[0],
  372. start_end = pieces[1].split('-');
  373. this.set({
  374. chrom: chrom,
  375. start: parseInt(start_end[0], 10),
  376. end: parseInt(start_end[1], 10)
  377. });
  378. }
  379. },
  380. copy: function() {
  381. return new GenomeRegion({
  382. chrom: this.get('chrom'),
  383. start: this.get('start'),
  384. end: this.get('end')
  385. });
  386. },
  387. length: function() {
  388. return this.get('end') - this.get('start');
  389. },
  390. /** Returns region in canonical form chrom:start-end */
  391. toString: function() {
  392. return this.get('chrom') + ":" + this.get('start') + "-" + this.get('end');
  393. },
  394. toJSON: function() {
  395. return {
  396. chrom: this.get('chrom'),
  397. start: this.get('start'),
  398. end: this.get('end')
  399. };
  400. },
  401. /**
  402. * Compute the type of overlap between this region and another region. The overlap is computed relative to the given/second region;
  403. * hence, OVERLAP_START indicates that the first region overlaps the start (but not the end) of the second region.
  404. */
  405. compute_overlap: function(a_region) {
  406. var first_chrom = this.get('chrom'), second_chrom = a_region.get('chrom'),
  407. first_start = this.get('start'), second_start = a_region.get('start'),
  408. first_end = this.get('end'), second_end = a_region.get('end'),
  409. overlap;
  410. // Look at chroms.
  411. if (first_chrom && second_chrom && first_chrom !== second_chrom) {
  412. return this.get('DIF_CHROMS');
  413. }
  414. // Look at regions.
  415. if (first_start < second_start) {
  416. if (first_end < second_start) {
  417. overlap = this.get('BEFORE');
  418. }
  419. else if (first_end <= second_end) {
  420. overlap = this.get('OVERLAP_START');
  421. }
  422. else { // first_end > second_end
  423. overlap = this.get('CONTAINS');
  424. }
  425. }
  426. else { // first_start >= second_start
  427. if (first_start > second_end) {
  428. overlap = this.get('AFTER');
  429. }
  430. else if (first_end <= second_end) {
  431. overlap = this.get('CONTAINED_BY');
  432. }
  433. else {
  434. overlap = this.get('OVERLAP_END');
  435. }
  436. }
  437. return overlap;
  438. },
  439. /**
  440. * Returns true if this region contains a given region.
  441. */
  442. contains: function(a_region) {
  443. return this.compute_overlap(a_region) === this.get('CONTAINS');
  444. },
  445. /**
  446. * Returns true if regions overlap.
  447. */
  448. overlaps: function(a_region) {
  449. return _.intersection( [this.compute_overlap(a_region)],
  450. [this.get('DIF_CHROMS'), this.get('BEFORE'), this.get('AFTER')] ).length === 0;
  451. }
  452. });
  453. var GenomeRegionCollection = Backbone.Collection.extend({
  454. model: GenomeRegion
  455. });
  456. /**
  457. * A genome browser bookmark.
  458. */
  459. var BrowserBookmark = Backbone.RelationalModel.extend({
  460. defaults: {
  461. region: null,
  462. note: ''
  463. },
  464. relations: [
  465. {
  466. type: Backbone.HasOne,
  467. key: 'region',
  468. relatedModel: GenomeRegion
  469. }
  470. ]
  471. });
  472. /**
  473. * Bookmarks collection.
  474. */
  475. var BrowserBookmarkCollection = Backbone.Collection.extend({
  476. model: BrowserBookmark
  477. });
  478. var GenomeWideBigWigData = Backbone.Model.extend({
  479. defaults: {
  480. data: null,
  481. min: 0,
  482. max: 0
  483. },
  484. initialize: function(options) {
  485. // Set max across dataset by extracting all values, flattening them into a
  486. // single array, and getting the min and max.
  487. var values = _.flatten( _.map(this.get('data'), function(d) {
  488. if (d.data.length !== 0) {
  489. // Each data point has the form [position, value], so return all values.
  490. return _.map(d.data, function(p) {
  491. return p[1];
  492. });
  493. }
  494. else {
  495. return 0;
  496. }
  497. }) );
  498. this.set('max', _.max(values));
  499. this.set('min', _.min(values));
  500. }
  501. });
  502. /**
  503. * Genome-wide summary tree dataset.
  504. */
  505. var GenomeWideSummaryTreeData = Backbone.RelationalModel.extend({
  506. defaults: {
  507. data: null,
  508. min: 0,
  509. max: 0
  510. },
  511. initialize: function(options) {
  512. // Set max across dataset.
  513. var max_data = _.max(this.get('data'), function(d) {
  514. if (!d || typeof d === 'string') { return 0; }
  515. return d[1];
  516. });
  517. this.attributes.max = (max_data && typeof max_data !== 'string' ? max_data[1] : 0);
  518. }
  519. });
  520. /**
  521. * A track of data in a genome visualization.
  522. */
  523. // TODO: rename to Track and merge with Trackster's Track object.
  524. var BackboneTrack = data.Dataset.extend({
  525. initialize: function(options) {
  526. // Dataset id is unique ID for now.
  527. this.set('id', options.dataset_id);
  528. // Create genome-wide dataset if available.
  529. var genome_wide_data = this.get('genome_wide_data');
  530. if (genome_wide_data) {
  531. var gwd_class = (this.get('track_type') === 'LineTrack' ?
  532. GenomeWideBigWigData : GenomeWideSummaryTreeData);
  533. this.set('genome_wide_data', new gwd_class(genome_wide_data));
  534. }
  535. }
  536. });
  537. /**
  538. * A visualization.
  539. */
  540. var Visualization = Backbone.RelationalModel.extend({
  541. defaults: {
  542. id: '',
  543. title: '',
  544. type: '',
  545. dbkey: '',
  546. tracks: null
  547. },
  548. relations: [
  549. {
  550. type: Backbone.HasMany,
  551. key: 'tracks',
  552. relatedModel: BackboneTrack
  553. }
  554. ],
  555. // Use function because visualization_url changes depending on viz.
  556. // FIXME: all visualizations should save to the same URL (and hence
  557. // this function won't be needed).
  558. url: function() {
  559. return galaxy_paths.get("visualization_url");
  560. },
  561. /**
  562. * POSTs visualization's JSON to its URL using the parameter 'vis_json'
  563. * Note: This is necessary because (a) Galaxy requires keyword args and
  564. * (b) Galaxy does not handle PUT now.
  565. */
  566. save: function() {
  567. return $.ajax({
  568. url: this.url(),
  569. type: "POST",
  570. dataType: "json",
  571. data: {
  572. vis_json: JSON.stringify(this)
  573. }
  574. });
  575. }
  576. });
  577. /**
  578. * A Genome space visualization.
  579. */
  580. var GenomeVisualization = Visualization.extend({
  581. defaults: _.extend({}, Visualization.prototype.defaults, {
  582. bookmarks: null,
  583. viewport: null
  584. })
  585. });
  586. /**
  587. * Configuration data for a Trackster track.
  588. */
  589. var TrackConfig = Backbone.Model.extend({
  590. });
  591. /**
  592. * -- Routers --
  593. */
  594. /**
  595. * Router for track browser.
  596. */
  597. var TrackBrowserRouter = Backbone.Router.extend({
  598. initialize: function(options) {
  599. this.view = options.view;
  600. // Can't put regular expression in routes dictionary.
  601. // NOTE: parentheses are used to denote parameters returned to callback.
  602. this.route(/([\w]+)$/, 'change_location');
  603. this.route(/([\w]+\:[\d,]+-[\d,]+)$/, 'change_location');
  604. // Handle navigate events from view.
  605. var self = this;
  606. self.view.on("navigate", function(new_loc) {
  607. self.navigate(new_loc);
  608. });
  609. },
  610. change_location: function(new_loc) {
  611. this.view.go_to(new_loc);
  612. }
  613. });
  614. /**
  615. * -- Helper functions.
  616. */
  617. /**
  618. * Use a popup grid to add more datasets.
  619. */
  620. var add_datasets = function(dataset_url, add_track_async_url, success_fn) {
  621. $.ajax({
  622. url: dataset_url,
  623. data: { "f-dbkey": view.dbkey },
  624. error: function() { alert( "Grid failed" ); },
  625. success: function(table_html) {
  626. show_modal(
  627. "Select datasets for new tracks",
  628. table_html, {
  629. "Cancel": function() {
  630. hide_modal();
  631. },
  632. "Add": function() {
  633. var requests = [];
  634. $('input[name=id]:checked,input[name=ldda_ids]:checked').each(function() {
  635. var data = {
  636. data_type: 'track_config',
  637. 'hda_ldda': 'hda'
  638. },
  639. id = $(this).val();
  640. if ($(this).attr("name") !== "id") {
  641. data['hda_ldda'] = 'ldda';
  642. }
  643. requests[requests.length] = $.ajax({
  644. url: add_track_async_url + "/" + id,
  645. data: data,
  646. dataType: "json"
  647. });
  648. });
  649. // To preserve order, wait until there are definitions for all tracks and then add
  650. // them sequentially.
  651. $.when.apply($, requests).then(function() {
  652. // jQuery always returns an Array for arguments, so need to look at first element
  653. // to determine whether multiple requests were made and consequently how to
  654. // map arguments to track definitions.
  655. var track_defs = (arguments[0] instanceof Array ?
  656. $.map(arguments, function(arg) { return arg[0]; }) :
  657. [ arguments[0] ]
  658. );
  659. success_fn(track_defs);
  660. });
  661. hide_modal();
  662. }
  663. }
  664. );
  665. }
  666. });
  667. };
  668. return {
  669. BrowserBookmark: BrowserBookmark,
  670. BrowserBookmarkCollection: BrowserBookmarkCollection,
  671. Cache: Cache,
  672. CanvasManager: CanvasManager,
  673. Genome: Genome,
  674. GenomeDataManager: GenomeDataManager,
  675. GenomeRegion: GenomeRegion,
  676. GenomeRegionCollection: GenomeRegionCollection,
  677. GenomeVisualization: GenomeVisualization,
  678. GenomeWideBigWigData: GenomeWideBigWigData,
  679. GenomeWideSummaryTreeData: GenomeWideSummaryTreeData,
  680. ReferenceTrackDataManager: ReferenceTrackDataManager,
  681. TrackBrowserRouter: TrackBrowserRouter,
  682. TrackConfig: TrackConfig,
  683. Visualization: Visualization,
  684. add_datasets: add_datasets
  685. };
  686. });