PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/data.js

https://bitbucket.org/kellrott/galaxy-central
JavaScript | 670 lines | 429 code | 101 blank | 140 comment | 66 complexity | e74ef2c7ce6eb69b84a640a6f3963bdc MD5 | raw file
Possible License(s): CC-BY-3.0
  1. // Additional dependencies: jQuery, underscore.
  2. define(['mvc/ui/ui-modal', 'mvc/ui/ui-frames'], function(Modal, Frames) {
  3. /**
  4. * Dataset metedata.
  5. */
  6. var DatasetMetadata = Backbone.Model.extend({});
  7. /**
  8. * A dataset. In Galaxy, datasets are associated with a history, so
  9. * this object is also known as a HistoryDatasetAssociation.
  10. */
  11. var Dataset = Backbone.Model.extend({
  12. defaults: {
  13. id: '',
  14. type: '',
  15. name: '',
  16. hda_ldda: 'hda',
  17. metadata: null
  18. },
  19. initialize: function() {
  20. this._set_metadata();
  21. // Update metadata on change.
  22. this.on('change', this._set_metadata, this);
  23. },
  24. _set_metadata: function() {
  25. var metadata = new DatasetMetadata();
  26. // Move metadata from dataset attributes to metadata object.
  27. _.each(_.keys(this.attributes), function(k) {
  28. if (k.indexOf('metadata_') === 0) {
  29. // Found metadata.
  30. var new_key = k.split('metadata_')[1];
  31. metadata.set(new_key, this.attributes[k]);
  32. delete this.attributes[k];
  33. }
  34. }, this);
  35. // Because this is an internal change, silence it.
  36. this.set('metadata', metadata, { 'silent': true });
  37. },
  38. /**
  39. * Returns dataset metadata for a given attribute.
  40. */
  41. get_metadata: function(attribute) {
  42. return this.attributes.metadata.get(attribute);
  43. },
  44. urlRoot: galaxy_config.root + "api/datasets"
  45. });
  46. /**
  47. * A tabular dataset. This object extends dataset to provide incremental chunked data.
  48. */
  49. var TabularDataset = Dataset.extend({
  50. defaults: _.extend({}, Dataset.prototype.defaults, {
  51. chunk_url: null,
  52. first_data_chunk: null,
  53. chunk_index: -1,
  54. at_eof: false
  55. }),
  56. initialize: function(options) {
  57. Dataset.prototype.initialize.call(this);
  58. // If first data chunk is available, next chunk is 1.
  59. this.attributes.chunk_index = (this.attributes.first_data_chunk ? 1 : 0);
  60. this.attributes.chunk_url = galaxy_config.root + 'dataset/display?dataset_id=' + this.id;
  61. this.attributes.url_viz = galaxy_config.root + 'visualization';
  62. },
  63. /**
  64. * Returns a jQuery Deferred object that resolves to the next data chunk or null if at EOF.
  65. */
  66. get_next_chunk: function() {
  67. // If already at end of file, do nothing.
  68. if (this.attributes.at_eof) {
  69. return null;
  70. }
  71. // Get next chunk.
  72. var self = this,
  73. next_chunk = $.Deferred();
  74. $.getJSON(this.attributes.chunk_url, {
  75. chunk: self.attributes.chunk_index++
  76. }).success(function(chunk) {
  77. var rval;
  78. if (chunk.ck_data !== '') {
  79. // Found chunk.
  80. rval = chunk;
  81. }
  82. else {
  83. // At EOF.
  84. self.attributes.at_eof = true;
  85. rval = null;
  86. }
  87. next_chunk.resolve(rval);
  88. });
  89. return next_chunk;
  90. }
  91. });
  92. var DatasetCollection = Backbone.Collection.extend({
  93. model: Dataset
  94. });
  95. /**
  96. * Provides a base for table-based, dynamic view of a tabular dataset.
  97. * Do not instantiate directly; use either TopLevelTabularDatasetChunkedView
  98. * or EmbeddedTabularDatasetChunkedView.
  99. */
  100. var TabularDatasetChunkedView = Backbone.View.extend({
  101. /**
  102. * Initialize view and, importantly, set a scroll element.
  103. */
  104. initialize: function(options) {
  105. // Row count for rendering.
  106. this.row_count = 0;
  107. this.loading_chunk = false;
  108. // CSS colors used in table.
  109. this.header_color = '#AAA';
  110. this.dark_row_color = '#DDD';
  111. // load trackster button
  112. new TabularButtonTracksterView({
  113. model : options.model,
  114. $el : this.$el
  115. });
  116. },
  117. expand_to_container: function(){
  118. if (this.$el.height() < this.scroll_elt.height()){
  119. this.attempt_to_fetch();
  120. }
  121. },
  122. attempt_to_fetch: function( func ){
  123. var self = this;
  124. if ( !this.loading_chunk && this.scrolled_to_bottom() ) {
  125. this.loading_chunk = true;
  126. this.loading_indicator.show();
  127. $.when(self.model.get_next_chunk()).then(function(result) {
  128. if (result) {
  129. self._renderChunk(result);
  130. self.loading_chunk = false;
  131. }
  132. self.loading_indicator.hide();
  133. self.expand_to_container();
  134. });
  135. }
  136. },
  137. render: function() {
  138. // Add loading indicator.
  139. this.loading_indicator = $('<div/>').attr('id', 'loading_indicator');
  140. this.$el.append(this.loading_indicator);
  141. // Add data table and header.
  142. var data_table = $('<table/>').attr({
  143. id: 'content_table',
  144. cellpadding: 0
  145. });
  146. this.$el.append(data_table);
  147. var column_names = this.model.get_metadata('column_names'),
  148. header_row = $('<tr/>').css('background-color', this.header_color).appendTo(data_table);
  149. if (column_names) {
  150. header_row.append('<th>' + column_names.join('</th><th>') + '</th>');
  151. }
  152. // Render first chunk.
  153. var self = this,
  154. first_chunk = this.model.get('first_data_chunk');
  155. if (first_chunk) {
  156. // First chunk is bootstrapped, so render now.
  157. this._renderChunk(first_chunk);
  158. }
  159. else {
  160. // No bootstrapping, so get first chunk and then render.
  161. $.when(self.model.get_next_chunk()).then(function(result) {
  162. self._renderChunk(result);
  163. });
  164. }
  165. // -- Show new chunks during scrolling. --
  166. // Set up chunk loading when scrolling using the scrolling element.
  167. this.scroll_elt.scroll(function(){
  168. self.attempt_to_fetch();
  169. });
  170. },
  171. /**
  172. * Returns true if user has scrolled to the bottom of the view.
  173. */
  174. scrolled_to_bottom: function() {
  175. return false;
  176. },
  177. // -- Helper functions. --
  178. _renderCell: function(cell_contents, index, colspan) {
  179. var $cell = $('<td>').text(cell_contents);
  180. var column_types = this.model.get_metadata('column_types');
  181. if (colspan !== undefined) {
  182. $cell.attr('colspan', colspan).addClass('stringalign');
  183. } else if (column_types) {
  184. if (index < column_types.length) {
  185. if (column_types[index] === 'str' || column_types[index] === 'list') {
  186. /* Left align all str columns, right align the rest */
  187. $cell.addClass('stringalign');
  188. }
  189. }
  190. }
  191. return $cell;
  192. },
  193. _renderRow: function(line) {
  194. // Check length of cells to ensure this is a complete row.
  195. var cells = line.split('\t'),
  196. row = $('<tr>'),
  197. num_columns = this.model.get_metadata('columns');
  198. if (this.row_count % 2 !== 0) {
  199. row.css('background-color', this.dark_row_color);
  200. }
  201. if (cells.length === num_columns) {
  202. _.each(cells, function(cell_contents, index) {
  203. row.append(this._renderCell(cell_contents, index));
  204. }, this);
  205. }
  206. else if (cells.length > num_columns) {
  207. // SAM file or like format with optional metadata included.
  208. _.each(cells.slice(0, num_columns - 1), function(cell_contents, index) {
  209. row.append(this._renderCell(cell_contents, index));
  210. }, this);
  211. row.append(this._renderCell(cells.slice(num_columns - 1).join('\t'), num_columns - 1));
  212. }
  213. else if (num_columns > 5 && cells.length === num_columns - 1 ) {
  214. // SAM file or like format with optional metadata missing.
  215. _.each(cells, function(cell_contents, index) {
  216. row.append(this._renderCell(cell_contents, index));
  217. }, this);
  218. row.append($('<td>'));
  219. }
  220. else {
  221. // Comment line, just return the one cell.
  222. row.append(this._renderCell(line, 0, num_columns));
  223. }
  224. this.row_count++;
  225. return row;
  226. },
  227. _renderChunk: function(chunk) {
  228. var data_table = this.$el.find('table');
  229. _.each(chunk.ck_data.split('\n'), function(line, index) {
  230. if (line !== ''){
  231. data_table.append(this._renderRow(line));
  232. }
  233. }, this);
  234. }
  235. });
  236. /**
  237. * Tabular view that is placed at the top level of page. Scrolling occurs
  238. * view top-level elements outside of view.
  239. */
  240. var TopLevelTabularDatasetChunkedView = TabularDatasetChunkedView.extend({
  241. initialize: function(options) {
  242. TabularDatasetChunkedView.prototype.initialize.call(this, options);
  243. // Scrolling happens in top-level elements.
  244. scroll_elt = _.find(this.$el.parents(), function(p) {
  245. return $(p).css('overflow') === 'auto';
  246. });
  247. // If no scrolling element found, use window.
  248. if (!scroll_elt) { scroll_elt = window; }
  249. // Wrap scrolling element for easy access.
  250. this.scroll_elt = $(scroll_elt);
  251. },
  252. /**
  253. * Returns true if user has scrolled to the bottom of the view.
  254. */
  255. scrolled_to_bottom: function() {
  256. return (this.$el.height() - this.scroll_elt.scrollTop() - this.scroll_elt.height() <= 0);
  257. }
  258. });
  259. /**
  260. * Tabular view tnat is embedded in a page. Scrolling occurs in view's el.
  261. */
  262. var EmbeddedTabularDatasetChunkedView = TabularDatasetChunkedView.extend({
  263. initialize: function(options) {
  264. TabularDatasetChunkedView.prototype.initialize.call(this, options);
  265. // Because view is embedded, set up div to do scrolling.
  266. this.scroll_elt = this.$el.css({
  267. position: 'relative',
  268. overflow: 'scroll',
  269. height: this.options.height || '500px'
  270. });
  271. },
  272. /**
  273. * Returns true if user has scrolled to the bottom of the view.
  274. */
  275. scrolled_to_bottom: function() {
  276. return this.$el.scrollTop() + this.$el.innerHeight() >= this.el.scrollHeight;
  277. }
  278. });
  279. // button for trackster visualization
  280. var TabularButtonTracksterView = Backbone.View.extend({
  281. // gene region columns
  282. col: {
  283. chrom : null,
  284. start : null,
  285. end : null
  286. },
  287. // url for trackster
  288. url_viz : null,
  289. // dataset id
  290. dataset_id : null,
  291. // database key
  292. genome_build: null,
  293. // data type
  294. data_type : null,
  295. // backbone initialize
  296. initialize: function (options) {
  297. // check if environment is available
  298. var Galaxy = parent.Galaxy;
  299. // link galaxy modal or create one
  300. if (Galaxy && Galaxy.modal) {
  301. this.modal = Galaxy.modal;
  302. }
  303. // link galaxy frames
  304. if (Galaxy && Galaxy.frame) {
  305. this.frame = Galaxy.frame;
  306. }
  307. // check
  308. if (!this.modal || !this.frame) {
  309. return;
  310. }
  311. // model/metadata
  312. var model = options.model;
  313. var metadata = model.get('metadata');
  314. // check for datatype
  315. if (!model.get('data_type')) {
  316. return;
  317. }
  318. // get data type
  319. this.data_type = model.get('data_type');
  320. // check for bed-file format
  321. if (this.data_type == 'bed')
  322. {
  323. // verify that metadata exists
  324. if (metadata.get('chromCol') && metadata.get('startCol') && metadata.get('endCol'))
  325. {
  326. // read in columns
  327. this.col.chrom = metadata.get('chromCol') - 1;
  328. this.col.start = metadata.get('startCol') - 1;
  329. this.col.end = metadata.get('endCol') - 1;
  330. } else {
  331. console.log('TabularButtonTrackster : Bed-file metadata incomplete.');
  332. return;
  333. }
  334. }
  335. // check for vcf-file format
  336. if (this.data_type == 'vcf')
  337. {
  338. // search array
  339. function search (str, array) {
  340. for (var j = 0; j < array.length; j++)
  341. if (array[j].match(str)) return j;
  342. return -1;
  343. };
  344. // load
  345. this.col.chrom = search('Chrom', metadata.get('column_names'));
  346. this.col.start = search('Pos', metadata.get('column_names'));
  347. this.col.end = null;
  348. // verify that metadata exists
  349. if (this.col.chrom == -1 || this.col.start == -1) {
  350. console.log('TabularButtonTrackster : VCF-file metadata incomplete.');
  351. return;
  352. }
  353. }
  354. // check
  355. if(this.col.chrom === undefined) {
  356. return;
  357. }
  358. // get dataset id
  359. if (model.id) {
  360. this.dataset_id = model.id;
  361. } else {
  362. console.log('TabularButtonTrackster : Dataset identification is missing.');
  363. return;
  364. }
  365. // get url
  366. if (model.get('url_viz')) {
  367. this.url_viz = model.get('url_viz');
  368. } else {
  369. console.log('TabularButtonTrackster : Url for visualization controller is missing.');
  370. return;
  371. }
  372. // get genome_build / database key
  373. if (model.get('genome_build')) {
  374. this.genome_build = model.get('genome_build');
  375. }
  376. // create the icon
  377. var btn_viz = new IconButtonView({
  378. model : new IconButton({
  379. title : 'Visualize',
  380. icon_class : 'chart_curve',
  381. id : 'btn_viz'
  382. })
  383. });
  384. // set element
  385. this.setElement(options.$el);
  386. // add to element
  387. this.$el.append(btn_viz.render().$el);
  388. // hide the button
  389. this.hide();
  390. },
  391. // backbone events
  392. events:
  393. {
  394. 'mouseover tr' : 'show',
  395. 'mouseleave' : 'hide'
  396. },
  397. // show button
  398. show: function (e) {
  399. // is numeric
  400. function is_numeric(n) {
  401. return !isNaN(parseFloat(n)) && isFinite(n);
  402. };
  403. // check
  404. if(this.col.chrom === null)
  405. return;
  406. // get selected data line
  407. var row = $(e.target).parent();
  408. // verify that location has been found
  409. var chrom = row.children().eq(this.col.chrom).html();
  410. var start = row.children().eq(this.col.start).html();
  411. // end is optional
  412. var end = this.col.end ? row.children().eq(this.col.end).html() : start;
  413. // double check location
  414. if (!chrom.match("^#") && chrom !== "" && is_numeric(start)) {
  415. // get target gene region
  416. var btn_viz_pars = {
  417. dataset_id : this.dataset_id,
  418. gene_region : chrom + ":" + start + "-" + end
  419. };
  420. // get button position
  421. var offset = row.offset();
  422. var left = offset.left - 10;
  423. var top = offset.top - $(window).scrollTop() + 3;
  424. // update css
  425. $('#btn_viz').css({'position': 'fixed', 'top': top + 'px', 'left': left + 'px'});
  426. $('#btn_viz').off('click');
  427. $('#btn_viz').click(this.create_trackster_action(this.url_viz, btn_viz_pars, this.genome_build));
  428. // show the button
  429. $('#btn_viz').show();
  430. } else {
  431. // hide the button
  432. $('#btn_viz').hide();
  433. }
  434. },
  435. // hide button
  436. hide: function () {
  437. this.$el.find('#btn_viz').hide();
  438. },
  439. // create action
  440. create_trackster_action : function (vis_url, dataset_params, dbkey) {
  441. // link this
  442. var self = this;
  443. // create function
  444. return function() {
  445. var listTracksParams = {};
  446. if (dbkey) {
  447. listTracksParams[ 'f-dbkey' ] = dbkey;
  448. }
  449. $.ajax({
  450. url: vis_url + '/list_tracks?' + $.param( listTracksParams ),
  451. dataType: 'html',
  452. error: function() {
  453. // show error message
  454. self.modal.show({
  455. title : 'Something went wrong!',
  456. body : 'Unfortunately we could not add this dataset to the track browser. Please try again or contact us.',
  457. buttons : {
  458. 'Cancel': function(){
  459. self.modal.hide();
  460. }
  461. }
  462. });
  463. },
  464. success: function(table_html) {
  465. self.modal.show({
  466. title : 'View Data in a New or Saved Visualization',
  467. buttons :{
  468. 'Cancel': function(){
  469. self.modal.hide();
  470. },
  471. 'View in saved visualization': function(){
  472. // show modal with saved visualizations
  473. self.modal.show(
  474. {
  475. title : 'Add Data to Saved Visualization',
  476. body : table_html,
  477. buttons : {
  478. 'Cancel': function(){
  479. self.modal.hide();
  480. },
  481. 'Add to visualization': function(){
  482. // hide
  483. self.modal.hide();
  484. // search selected fields
  485. self.modal.$el.find('input[name=id]:checked').each(function(){
  486. // get visualization id
  487. var vis_id = $(this).val();
  488. dataset_params.id = vis_id;
  489. // add widget
  490. self.frame.add({
  491. title : 'Trackster',
  492. type : 'url',
  493. content : vis_url + '/trackster?' + $.param(dataset_params)
  494. });
  495. });
  496. }
  497. }
  498. });
  499. },
  500. 'View in new visualization': function(){
  501. // hide
  502. self.modal.hide();
  503. // add widget
  504. self.frame.add({
  505. title : 'Trackster',
  506. type : 'url',
  507. content : vis_url + '/trackster?' + $.param(dataset_params)
  508. });
  509. }
  510. }
  511. });
  512. }
  513. });
  514. return false;
  515. };
  516. }
  517. });
  518. // -- Utility functions. --
  519. /**
  520. * Create a model, attach it to a view, render view, and attach it to a parent element.
  521. */
  522. var createModelAndView = function(model, view, model_config, parent_elt) {
  523. // Create model, view.
  524. var a_view = new view({
  525. model: new model(model_config)
  526. });
  527. // Render view and add to parent element.
  528. a_view.render();
  529. if (parent_elt) {
  530. parent_elt.append(a_view.$el);
  531. }
  532. return a_view;
  533. };
  534. /**
  535. * Create a tabular dataset chunked view (and requisite tabular dataset model)
  536. * and appends to parent_elt.
  537. */
  538. var createTabularDatasetChunkedView = function(options) {
  539. // If no model, create and set model from dataset config.
  540. if (!options.model) {
  541. options.model = new TabularDataset(options.dataset_config);
  542. }
  543. var parent_elt = options.parent_elt;
  544. var embedded = options.embedded;
  545. // Clean up options so that only needed options are passed to view.
  546. delete options.embedded;
  547. delete options.parent_elt;
  548. delete options.dataset_config;
  549. // Create and set up view.
  550. var view = (embedded ? new EmbeddedTabularDatasetChunkedView(options) :
  551. new TopLevelTabularDatasetChunkedView(options));
  552. view.render();
  553. if (parent_elt) {
  554. parent_elt.append(view.$el);
  555. // If we're sticking this in another element, once it's appended check
  556. // to make sure we've filled enough space.
  557. // Without this, the scroll elements don't work.
  558. view.expand_to_container();
  559. }
  560. return view;
  561. };
  562. return {
  563. Dataset: Dataset,
  564. TabularDataset: TabularDataset,
  565. DatasetCollection: DatasetCollection,
  566. TabularDatasetChunkedView: TabularDatasetChunkedView,
  567. createTabularDatasetChunkedView: createTabularDatasetChunkedView
  568. };
  569. });