PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/viz/visualization.js

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