PageRenderTime 66ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/mvc/data.js

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