PageRenderTime 89ms CodeModel.GetById 15ms RepoModel.GetById 2ms app.codeStats 0ms

/static/scripts/viz/sweepster.js

https://bitbucket.org/jmchilton/adapt
JavaScript | 951 lines | 674 code | 119 blank | 158 comment | 31 complexity | bf9d6c97eb2a2688cf6af56d4086c547 MD5 | raw file
  1. /**
  2. * Visualization and components for Sweepster, a visualization for exploring a tool's parameter space via
  3. * genomic visualization.
  4. */
  5. define( ["libs/d3", "viz/trackster/util", "viz/visualization", "viz/trackster/tracks"], function( d3, util, visualization, tracks ) {
  6. /**
  7. * A collection of tool input settings. Object is useful for keeping a list of settings
  8. * for future use without changing the input's value and for preserving inputs order.
  9. */
  10. var ToolInputsSettings = Backbone.Model.extend({
  11. defaults: {
  12. inputs: null,
  13. values: null
  14. }
  15. });
  16. /**
  17. * Tree for a tool's parameters.
  18. */
  19. var ToolParameterTree = Backbone.RelationalModel.extend({
  20. defaults: {
  21. tool: null,
  22. tree_data: null
  23. },
  24. initialize: function(options) {
  25. // Set up tool parameters to work with tree.
  26. var self = this;
  27. this.get('tool').get('inputs').each(function(input) {
  28. if (!input.get_samples()) { return; }
  29. // Listen for changes to input's attributes.
  30. input.on('change:min change:max change:num_samples', function(input) {
  31. if (input.get('in_ptree')) {
  32. self.set_tree_data();
  33. }
  34. }, self);
  35. input.on('change:in_ptree', function(input) {
  36. if (input.get('in_ptree')) {
  37. self.add_param(input);
  38. }
  39. else {
  40. self.remove_param(input);
  41. }
  42. self.set_tree_data();
  43. }, self);
  44. });
  45. // If there is a config, use it.
  46. if (options.config) {
  47. _.each(options.config, function(input_config) {
  48. var input = self.get('tool').get('inputs').find(function(input) {
  49. return input.get('name') === input_config.name;
  50. });
  51. self.add_param(input);
  52. input.set(input_config);
  53. });
  54. }
  55. },
  56. add_param: function(param) {
  57. // If parameter already present, do not add it.
  58. if (param.get('ptree_index')) { return; }
  59. param.set('in_ptree', true);
  60. param.set('ptree_index', this.get_tree_params().length);
  61. },
  62. remove_param: function(param) {
  63. // Remove param from tree.
  64. param.set('in_ptree', false);
  65. param.set('ptree_index', null);
  66. // Update ptree indices for remaining params.
  67. _(this.get_tree_params()).each(function(input, index) {
  68. // +1 to use 1-based indexing.
  69. input.set('ptree_index', index + 1);
  70. });
  71. },
  72. /**
  73. * Sets tree data using tool's inputs.
  74. */
  75. set_tree_data: function() {
  76. // Get samples for each parameter.
  77. var params_samples = _.map(this.get_tree_params(), function(param) {
  78. return {
  79. param: param,
  80. samples: param.get_samples()
  81. };
  82. });
  83. var node_id = 0,
  84. // Creates tree data recursively.
  85. create_tree_data = function(params_samples, index) {
  86. var param_samples = params_samples[index],
  87. param = param_samples.param,
  88. param_label = param.get('label'),
  89. settings = param_samples.samples;
  90. // Create leaves when last parameter setting is reached.
  91. if (params_samples.length - 1 === index) {
  92. return _.map(settings, function(setting) {
  93. return {
  94. id: node_id++,
  95. name: setting,
  96. param: param,
  97. value: setting
  98. };
  99. });
  100. }
  101. // Recurse to handle other parameters.
  102. return _.map(settings, function(setting) {
  103. return {
  104. id: node_id++,
  105. name: setting,
  106. param: param,
  107. value: setting,
  108. children: create_tree_data(params_samples, index + 1)
  109. };
  110. });
  111. };
  112. this.set('tree_data', {
  113. name: 'Root',
  114. id: node_id++,
  115. children: (params_samples.length !== 0 ? create_tree_data(params_samples, 0) : null)
  116. });
  117. },
  118. get_tree_params: function() {
  119. // Filter and sort parameters to get list in tree.
  120. return _(this.get('tool').get('inputs').where( {in_ptree: true} ))
  121. .sortBy( function(input) { return input.get('ptree_index'); } );
  122. },
  123. /**
  124. * Returns number of leaves in tree.
  125. */
  126. get_num_leaves: function() {
  127. return this.get_tree_params().reduce(function(memo, param) { return memo * param.get_samples().length; }, 1);
  128. },
  129. /**
  130. * Returns array of ToolInputsSettings objects based on a node and its subtree.
  131. */
  132. get_node_settings: function(target_node) {
  133. // -- Get fixed settings from tool and parent nodes.
  134. // Start with tool's settings.
  135. var fixed_settings = this.get('tool').get_inputs_dict();
  136. // Get fixed settings using node's parents.
  137. var cur_node = target_node.parent;
  138. if (cur_node) {
  139. while(cur_node.depth !== 0) {
  140. fixed_settings[cur_node.param.get('name')] = cur_node.value;
  141. cur_node = cur_node.parent;
  142. }
  143. }
  144. // Walk subtree starting at clicked node to get full list of settings.
  145. var self = this,
  146. get_settings = function(node, settings) {
  147. // Add setting for this node. Root node does not have a param,
  148. // however.
  149. if (node.param) {
  150. settings[node.param.get('name')] = node.value;
  151. }
  152. if (!node.children) {
  153. // At leaf node, so return settings.
  154. return new ToolInputsSettings({
  155. inputs: self.get('tool').get('inputs'),
  156. values: settings
  157. });
  158. }
  159. else {
  160. // At interior node: return list of subtree settings.
  161. return _.flatten( _.map(node.children, function(c) { return get_settings(c, _.clone(settings)); }) );
  162. }
  163. },
  164. all_settings = get_settings(target_node, fixed_settings);
  165. // If user clicked on leaf, settings is a single dict. Convert to array for simplicity.
  166. if (!_.isArray(all_settings)) { all_settings = [ all_settings ]; }
  167. return all_settings;
  168. },
  169. /**
  170. * Returns all nodes connected a particular node; this includes parents and children of the node.
  171. */
  172. get_connected_nodes: function(node) {
  173. var get_subtree_nodes = function(a_node) {
  174. if (!a_node.children) {
  175. return a_node;
  176. }
  177. else {
  178. // At interior node: return subtree nodes.
  179. return _.flatten( [a_node, _.map(a_node.children, function(c) { return get_subtree_nodes(c); })] );
  180. }
  181. };
  182. // Get node's parents.
  183. var parents = [],
  184. cur_parent = node.parent;
  185. while(cur_parent) {
  186. parents.push(cur_parent);
  187. cur_parent = cur_parent.parent;
  188. }
  189. return _.flatten([parents, get_subtree_nodes(node)]);
  190. },
  191. /**
  192. * Returns the leaf that corresponds to a settings collection.
  193. */
  194. get_leaf: function(settings) {
  195. var cur_node = this.get('tree_data'),
  196. find_child = function(children) {
  197. return _.find(children, function(child) {
  198. return settings[child.param.get('name')] === child.value;
  199. });
  200. };
  201. while (cur_node.children) {
  202. cur_node = find_child(cur_node.children);
  203. }
  204. return cur_node;
  205. },
  206. /**
  207. * Returns a list of parameters used in tree.
  208. */
  209. toJSON: function() {
  210. // FIXME: returning and jsonifying complete param causes trouble on the server side,
  211. // so just use essential attributes for now.
  212. return this.get_tree_params().map(function(param) {
  213. return {
  214. name: param.get('name'),
  215. min: param.get('min'),
  216. max: param.get('max'),
  217. num_samples: param.get('num_samples')
  218. };
  219. });
  220. }
  221. });
  222. var SweepsterTrack = Backbone.RelationalModel.extend({
  223. defaults: {
  224. track: null,
  225. mode: 'Pack',
  226. settings: null,
  227. regions: null
  228. },
  229. relations: [
  230. {
  231. type: Backbone.HasMany,
  232. key: 'regions',
  233. relatedModel: visualization.GenomeRegion
  234. }
  235. ],
  236. initialize: function(options) {
  237. if (options.track) {
  238. // FIXME: find a better way to deal with needed URLs:
  239. var track_config = _.extend({
  240. data_url: galaxy_paths.get('raw_data_url'),
  241. converted_datasets_state_url: galaxy_paths.get('dataset_state_url')
  242. }, options.track);
  243. this.set('track', tracks.object_from_template(track_config, {}, null));
  244. }
  245. },
  246. same_settings: function(a_track) {
  247. var this_settings = this.get('settings'),
  248. other_settings = a_track.get('settings');
  249. for (var prop in this_settings) {
  250. if (!other_settings[prop] ||
  251. this_settings[prop] !== other_settings[prop]) {
  252. return false;
  253. }
  254. }
  255. return true;
  256. },
  257. toJSON: function() {
  258. return {
  259. track: this.get('track').to_dict(),
  260. settings: this.get('settings'),
  261. regions: this.get('regions')
  262. };
  263. }
  264. });
  265. var TrackCollection = Backbone.Collection.extend({
  266. model: SweepsterTrack
  267. });
  268. /**
  269. * Sweepster visualization model.
  270. */
  271. var SweepsterVisualization = visualization.Visualization.extend({
  272. defaults: _.extend({}, visualization.Visualization.prototype.defaults, {
  273. dataset: null,
  274. tool: null,
  275. parameter_tree: null,
  276. regions: null,
  277. tracks: null,
  278. default_mode: 'Pack'
  279. }),
  280. relations: [
  281. {
  282. type: Backbone.HasOne,
  283. key: 'dataset',
  284. relatedModel: Dataset
  285. },
  286. {
  287. type: Backbone.HasOne,
  288. key: 'tool',
  289. relatedModel: Tool
  290. },
  291. {
  292. type: Backbone.HasMany,
  293. key: 'regions',
  294. relatedModel: visualization.GenomeRegion
  295. },
  296. {
  297. type: Backbone.HasMany,
  298. key: 'tracks',
  299. relatedModel: SweepsterTrack
  300. }
  301. // NOTE: cannot use relationship for parameter tree because creating tree is complex.
  302. ],
  303. initialize: function(options) {
  304. var tool_with_samplable_inputs = this.get('tool').copy(true);
  305. this.set('tool_with_samplable_inputs', tool_with_samplable_inputs);
  306. this.set('parameter_tree', new ToolParameterTree({
  307. tool: tool_with_samplable_inputs,
  308. config: options.tree_config
  309. }));
  310. },
  311. add_track: function(track) {
  312. this.get('tracks').add(track);
  313. },
  314. toJSON: function() {
  315. // TODO: could this be easier by using relational models?
  316. return {
  317. id: this.get('id'),
  318. title: 'Parameter exploration for dataset \'' + this.get('dataset').get('name') + '\'',
  319. type: 'sweepster',
  320. dataset_id: this.get('dataset').id,
  321. tool_id: this.get('tool').id,
  322. regions: this.get('regions').toJSON(),
  323. tree_config: this.get('parameter_tree').toJSON(),
  324. tracks: this.get('tracks').toJSON()
  325. };
  326. }
  327. });
  328. /**
  329. * --- Views ---
  330. */
  331. /**
  332. * Sweepster track view.
  333. */
  334. var SweepsterTrackView = Backbone.View.extend({
  335. tagName: 'tr',
  336. TILE_LEN: 250,
  337. initialize: function(options) {
  338. this.canvas_manager = options.canvas_manager;
  339. this.render();
  340. this.model.on('change:track change:mode', this.draw_tiles, this);
  341. },
  342. render: function() {
  343. // Render settings icon and popup.
  344. // TODO: use template.
  345. var settings = this.model.get('settings'),
  346. values = settings.get('values'),
  347. settings_td = $('<td/>').addClass('settings').appendTo(this.$el),
  348. settings_div = $('<div/>').addClass('track-info').hide().appendTo(settings_td);
  349. settings_div.append( $('<div/>').css('font-weight', 'bold').text('Track Settings') );
  350. settings.get('inputs').each(function(input) {
  351. settings_div.append( input.get('label') + ': ' + values[input.get('name')] + '<br/>');
  352. });
  353. var self = this,
  354. run_on_dataset_button = $('<button/>').appendTo(settings_div).text('Run on complete dataset').click(function() {
  355. settings_div.toggle();
  356. self.trigger('run_on_dataset', settings);
  357. });
  358. var icon_menu = create_icon_buttons_menu([
  359. {
  360. title: 'Settings',
  361. icon_class: 'gear track-settings',
  362. on_click: function() {
  363. settings_div.toggle();
  364. }
  365. },
  366. {
  367. title: 'Remove',
  368. icon_class: 'cross-circle',
  369. on_click: function() {
  370. self.$el.remove();
  371. $('.bs-tooltip').remove();
  372. // TODO: remove track from viz collection.
  373. }
  374. }
  375. ]);
  376. settings_td.prepend(icon_menu.$el);
  377. // Render tile placeholders.
  378. this.model.get('regions').each(function() {
  379. self.$el.append($('<td/>').addClass('tile').html(
  380. $('<img/>').attr('src', galaxy_paths.get('image_path') + '/loading_large_white_bg.gif')
  381. ));
  382. });
  383. if (this.model.get('track')) {
  384. this.draw_tiles();
  385. }
  386. },
  387. /**
  388. * Draw tiles for regions.
  389. */
  390. draw_tiles: function() {
  391. var self = this,
  392. track = this.model.get('track'),
  393. regions = this.model.get('regions'),
  394. tile_containers = this.$el.find('td.tile');
  395. // Do nothing if track is not defined.
  396. if (!track) { return; }
  397. // When data is ready, draw tiles.
  398. $.when(track.data_manager.data_is_ready()).then(function(data_ok) {
  399. // Draw tile for each region.
  400. regions.each(function(region, index) {
  401. var resolution = region.length() / self.TILE_LEN,
  402. w_scale = 1/resolution,
  403. mode = self.model.get('mode');
  404. $.when(track.data_manager.get_data(region, mode, resolution, {})).then(function(tile_data) {
  405. var canvas = self.canvas_manager.new_canvas();
  406. canvas.width = self.TILE_LEN;
  407. canvas.height = track.get_canvas_height(tile_data, mode, w_scale, canvas.width);
  408. track.draw_tile(tile_data, canvas.getContext('2d'), mode, resolution, region, w_scale);
  409. $(tile_containers[index]).empty().append(canvas);
  410. });
  411. });
  412. });
  413. }
  414. });
  415. /**
  416. * Tool input (parameter) that enables both value and sweeping inputs. View is unusual as
  417. * it augments an existing input form row rather than creates a completely new HTML element.
  418. */
  419. var ToolInputValOrSweepView = Backbone.View.extend({
  420. // Template for rendering sweep inputs:
  421. number_input_template: '<div class="form-row-input sweep">' +
  422. '<input class="min" type="text" size="6" value="<%= min %>"> - ' +
  423. '<input class="max" type="text" size="6" value="<%= max %>">' +
  424. ' samples: <input class="num_samples" type="text" size="1" value="<%= num_samples %>">' +
  425. '</div>',
  426. select_input_template: '<div class="form-row-input sweep"><%= options %></div>',
  427. initialize: function(options) {
  428. this.$el = options.tool_row;
  429. this.render();
  430. },
  431. render: function() {
  432. var input = this.model,
  433. type = input.get('type'),
  434. single_input_row = this.$el.find('.form-row-input'),
  435. sweep_inputs_row = null;
  436. // Update tool inputs as single input changes.
  437. single_input_row.find(':input').change(function() {
  438. input.set('value', $(this).val());
  439. });
  440. // Add row for parameter sweep inputs.
  441. if (type === 'number') {
  442. sweep_inputs_row = $(_.template(this.number_input_template, this.model.toJSON()));
  443. }
  444. else if (type === 'select') {
  445. var options = _.map(this.$el.find('select option'), function(option) {
  446. return $(option).val();
  447. }),
  448. options_text = options.join(', ');
  449. sweep_inputs_row = $(_.template(this.select_input_template, {
  450. options: options_text
  451. }));
  452. }
  453. sweep_inputs_row.insertAfter(single_input_row);
  454. // Add buttons for adding/removing parameter.
  455. var self = this,
  456. menu = create_icon_buttons_menu([
  457. {
  458. title: 'Add parameter to tree',
  459. icon_class: 'plus-button',
  460. on_click: function () {
  461. input.set('in_ptree', true);
  462. single_input_row.hide();
  463. sweep_inputs_row.show();
  464. $(this).hide();
  465. self.$el.find('.icon-button.toggle').show();
  466. }
  467. },
  468. {
  469. title: 'Remove parameter from tree',
  470. icon_class: 'toggle',
  471. on_click: function() {
  472. // Remove parameter from tree params where name matches clicked paramter.
  473. input.set('in_ptree', false);
  474. sweep_inputs_row.hide();
  475. single_input_row.show();
  476. $(this).hide();
  477. self.$el.find('.icon-button.plus-button').show();
  478. }
  479. }
  480. ],
  481. {
  482. });
  483. this.$el.prepend(menu.$el);
  484. // Show/hide input rows and icons depending on whether parameter is in the tree.
  485. if (input.get('in_ptree')) {
  486. single_input_row.hide();
  487. self.$el.find('.icon-button.plus-button').hide();
  488. }
  489. else {
  490. self.$el.find('.icon-button.toggle').hide();
  491. sweep_inputs_row.hide();
  492. }
  493. // Update input's min, max, number of samples as values change.
  494. _.each(['min', 'max', 'num_samples'], function(attr) {
  495. sweep_inputs_row.find('.' + attr).change(function() {
  496. input.set(attr, parseFloat( $(this).val() ));
  497. });
  498. });
  499. }
  500. });
  501. var ToolParameterTreeDesignView = Backbone.View.extend({
  502. className: 'tree-design',
  503. initialize: function(options) {
  504. this.render();
  505. },
  506. render: function() {
  507. // Start with tool form view.
  508. var tool_form_view = new ToolFormView({
  509. model: this.model.get('tool')
  510. });
  511. tool_form_view.render();
  512. this.$el.append(tool_form_view.$el);
  513. // Set up views for each tool input.
  514. var self = this,
  515. inputs = self.model.get('tool').get('inputs');
  516. this.$el.find('.form-row').not('.form-actions').each(function(i) {
  517. var input_view = new ToolInputValOrSweepView({
  518. model: inputs.at(i),
  519. tool_row: $(this)
  520. });
  521. });
  522. }
  523. });
  524. /**
  525. * Displays and updates parameter tree.
  526. */
  527. var ToolParameterTreeView = Backbone.View.extend({
  528. className: 'tool-parameter-tree',
  529. initialize: function(options) {
  530. // When tree data changes, re-render.
  531. this.model.on('change:tree_data', this.render, this);
  532. },
  533. render: function() {
  534. // Start fresh.
  535. this.$el.children().remove();
  536. var tree_params = this.model.get_tree_params();
  537. if (!tree_params.length) {
  538. return;
  539. }
  540. // Set width, height based on params and samples.
  541. this.width = 100 * (2 + tree_params.length);
  542. this.height = 15 * this.model.get_num_leaves();
  543. var self = this;
  544. // Layout tree.
  545. var cluster = d3.layout.cluster()
  546. .size([this.height, this.width - 160]);
  547. var diagonal = d3.svg.diagonal()
  548. .projection(function(d) { return [d.y, d.x]; });
  549. // Layout nodes.
  550. var nodes = cluster.nodes(this.model.get('tree_data'));
  551. // Setup and add labels for tree levels.
  552. var param_depths = _.uniq(_.pluck(nodes, "y"));
  553. _.each(tree_params, function(param, index) {
  554. var x = param_depths[index+1],
  555. center_left = $('#center').position().left;
  556. self.$el.append( $('<div>').addClass('label')
  557. .text(param.get('label'))
  558. .css('left', x + center_left) );
  559. });
  560. // Set up vis element.
  561. var vis = d3.select(this.$el[0])
  562. .append("svg")
  563. .attr("width", this.width)
  564. .attr("height", this.height + 30)
  565. .append("g")
  566. .attr("transform", "translate(40, 20)");
  567. // Draw links.
  568. var link = vis.selectAll("path.link")
  569. .data(cluster.links(nodes))
  570. .enter().append("path")
  571. .attr("class", "link")
  572. .attr("d", diagonal);
  573. // Draw nodes.
  574. var node = vis.selectAll("g.node")
  575. .data(nodes)
  576. .enter().append("g")
  577. .attr("class", "node")
  578. .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
  579. .on('mouseover', function(a_node) {
  580. var connected_node_ids = _.pluck(self.model.get_connected_nodes(a_node), 'id');
  581. // TODO: probably can use enter() to do this more easily.
  582. node.filter(function(d) {
  583. return _.find(connected_node_ids, function(id) { return id === d.id; }) !== undefined;
  584. }).style('fill', '#f00');
  585. })
  586. .on('mouseout', function() {
  587. node.style('fill', '#000');
  588. });
  589. node.append("circle")
  590. .attr("r", 9);
  591. node.append("text")
  592. .attr("dx", function(d) { return d.children ? -12 : 12; })
  593. .attr("dy", 3)
  594. .attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
  595. .text(function(d) { return d.name; });
  596. }
  597. });
  598. /**
  599. * Sweepster visualization view. View requires rendering in 3-panel setup for now.
  600. */
  601. var SweepsterVisualizationView = Backbone.View.extend({
  602. className: 'Sweepster',
  603. helpText:
  604. '<div><h4>Getting Started</h4>' +
  605. '<ol><li>Create a parameter tree by using the icons next to the tool\'s parameter names to add or remove parameters.' +
  606. '<li>Adjust the tree by using parameter inputs to select min, max, and number of samples' +
  607. '<li>Run the tool with different settings by clicking on tree nodes' +
  608. '</ol></div>',
  609. initialize: function(options) {
  610. this.canvas_manager = new visualization.CanvasManager(this.$el.parents('body'));
  611. this.tool_param_tree_view = new ToolParameterTreeView({ model: this.model.get('parameter_tree') });
  612. this.track_collection_container = $('<table/>').addClass('tracks');
  613. // Handle node clicks for tree data.
  614. this.model.get('parameter_tree').on('change:tree_data', this.handle_node_clicks, this);
  615. // Each track must have a view so it has a canvas manager.
  616. var self = this;
  617. this.model.get('tracks').each(function(track) {
  618. track.get('track').view = self;
  619. });
  620. // Set block, reverse strand block colors; these colors will be used for all tracks.
  621. this.block_color = util.get_random_color();
  622. this.reverse_strand_color = util.get_random_color( [ this.block_color, "#ffffff" ] );
  623. },
  624. render: function() {
  625. // Render tree design view in left panel.
  626. var tree_design_view = new ToolParameterTreeDesignView({
  627. model: this.model.get('parameter_tree')
  628. });
  629. $('#left').append(tree_design_view.$el);
  630. // Render track collection container/view in right panel.
  631. var self = this,
  632. regions = self.model.get('regions'),
  633. tr = $('<tr/>').appendTo(this.track_collection_container);
  634. regions.each(function(region) {
  635. tr.append( $('<th>').text(region.toString()) );
  636. });
  637. tr.children().first().attr('colspan', 2);
  638. var tracks_div = $('<div>').addClass('tiles');
  639. $('#right').append( tracks_div.append(this.track_collection_container) );
  640. self.model.get('tracks').each(function(track) {
  641. self.add_track(track);
  642. });
  643. // -- Render help and tool parameter tree in center panel. --
  644. // Help includes text and a close button.
  645. var help_div = $(this.helpText).addClass('help'),
  646. close_button = create_icon_buttons_menu([
  647. {
  648. title: 'Close',
  649. icon_class: 'cross-circle',
  650. on_click: function() {
  651. $('.bs-tooltip').remove();
  652. help_div.remove();
  653. }
  654. }
  655. ]);
  656. help_div.prepend(close_button.$el.css('float', 'right'));
  657. $('#center').append(help_div);
  658. // Parameter tree:
  659. this.tool_param_tree_view.render();
  660. $('#center').append(this.tool_param_tree_view.$el);
  661. // Set up handler for tree node clicks.
  662. this.handle_node_clicks();
  663. // Set up visualization menu.
  664. var menu = create_icon_buttons_menu(
  665. [
  666. // Save.
  667. /*
  668. { icon_class: 'disk--arrow', title: 'Save', on_click: function() {
  669. // Show saving dialog box
  670. show_modal("Saving...", "progress");
  671. viz.save().success(function(vis_info) {
  672. hide_modal();
  673. viz.set({
  674. 'id': vis_info.vis_id,
  675. 'has_changes': false
  676. });
  677. })
  678. .error(function() {
  679. show_modal( "Could Not Save", "Could not save visualization. Please try again later.",
  680. { "Close" : hide_modal } );
  681. });
  682. } },
  683. */
  684. // Change track modes.
  685. {
  686. icon_class: 'chevron-expand',
  687. title: 'Set display mode'
  688. },
  689. // Close viz.
  690. {
  691. icon_class: 'cross-circle',
  692. title: 'Close',
  693. on_click: function() {
  694. window.location = "${h.url_for( controller='visualization', action='list' )}";
  695. }
  696. }
  697. ],
  698. {
  699. tooltip_config: {placement: 'bottom'}
  700. });
  701. // Create mode selection popup. Mode selection changes default mode and mode for all tracks.
  702. var modes = ['Squish', 'Pack'],
  703. mode_mapping = {};
  704. _.each(modes, function(mode) {
  705. mode_mapping[mode] = function() {
  706. self.model.set('default_mode', mode);
  707. self.model.get('tracks').each(function(track) {
  708. track.set('mode', mode);
  709. });
  710. };
  711. });
  712. make_popupmenu(menu.$el.find('.chevron-expand'), mode_mapping);
  713. menu.$el.attr("style", "float: right");
  714. $("#right .unified-panel-header-inner").append(menu.$el);
  715. },
  716. run_tool_on_dataset: function(settings) {
  717. var tool = this.model.get('tool'),
  718. tool_name = tool.get('name'),
  719. dataset = this.model.get('dataset');
  720. tool.set_input_values(settings.get('values'));
  721. $.when(tool.rerun(dataset)).then(function(outputs) {
  722. // TODO.
  723. });
  724. show_modal('Running ' + tool_name + ' on complete dataset',
  725. tool_name + ' is running on dataset \'' +
  726. dataset.get('name') + '\'. Outputs are in the dataset\'s history.',
  727. {
  728. 'Ok': function() { hide_modal(); }
  729. });
  730. },
  731. /**
  732. * Add track to model and view.
  733. */
  734. add_track: function(pm_track) {
  735. var self = this,
  736. param_tree = this.model.get('parameter_tree');
  737. // Add track to model.
  738. self.model.add_track(pm_track);
  739. var track_view = new SweepsterTrackView({
  740. model: pm_track,
  741. canvas_manager: self.canvas_manager
  742. });
  743. track_view.on('run_on_dataset', self.run_tool_on_dataset, self);
  744. self.track_collection_container.append(track_view.$el);
  745. track_view.$el.hover(function() {
  746. var settings_leaf = param_tree.get_leaf(pm_track.get('settings').get('values'));
  747. var connected_node_ids = _.pluck(param_tree.get_connected_nodes(settings_leaf), 'id');
  748. // TODO: can do faster with enter?
  749. d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node")
  750. .filter(function(d) {
  751. return _.find(connected_node_ids, function(id) { return id === d.id; }) !== undefined;
  752. }).style('fill', '#f00');
  753. },
  754. function() {
  755. d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node").style('fill', '#000');
  756. });
  757. return pm_track;
  758. },
  759. /**
  760. * Sets up handling when tree nodes are clicked. When a node is clicked, the tool is run for each of
  761. * the settings defined by the node's subtree and tracks are added for each run.
  762. */
  763. handle_node_clicks: function() {
  764. // When node clicked in tree, run tool and add tracks to model.
  765. var self = this,
  766. param_tree = this.model.get('parameter_tree'),
  767. regions = this.model.get('regions'),
  768. node = d3.select(this.tool_param_tree_view.$el[0]).selectAll("g.node");
  769. node.on("click", function(d, i) {
  770. // Get all settings corresponding to node.
  771. var tool = self.model.get('tool'),
  772. dataset = self.model.get('dataset'),
  773. all_settings = param_tree.get_node_settings(d),
  774. run_jobs_deferred = $.Deferred();
  775. // Do not allow 10+ jobs to be run.
  776. if (all_settings.length >= 10) {
  777. show_modal("Whoa there cowboy!",
  778. "You clicked on a node to try " + self.model.get('tool').get('name') +
  779. " with " + all_settings.length +
  780. " different combinations of settings. You can only run 10 jobs at a time.",
  781. {
  782. "Ok": function() { hide_modal(); run_jobs_deferred.resolve(false); }
  783. });
  784. }
  785. else {
  786. run_jobs_deferred.resolve(true);
  787. }
  788. // Take action when deferred resolves.
  789. $.when(run_jobs_deferred).then(function(run_jobs) {
  790. if (!run_jobs) { return; }
  791. // Create and add tracks for each settings group.
  792. var new_tracks = _.map(all_settings, function(settings) {
  793. var pm_track = new SweepsterTrack({
  794. settings: settings,
  795. regions: regions,
  796. mode: self.model.get('default_mode')
  797. });
  798. self.add_track(pm_track);
  799. return pm_track;
  800. });
  801. // For each track, run tool using track's settings and update track.
  802. _.each(new_tracks, function(pm_track, index) {
  803. setTimeout(function() {
  804. // Set inputs and run tool.
  805. // console.log('running with settings', pm_track.get('settings'));
  806. tool.set_input_values(pm_track.get('settings').get('values'));
  807. $.when(tool.rerun(dataset, regions)).then(function(output) {
  808. // Create and add track for output dataset.
  809. var track_config = _.extend({
  810. data_url: galaxy_paths.get('raw_data_url'),
  811. converted_datasets_state_url: galaxy_paths.get('dataset_state_url')
  812. }, output.first().get('track_config')),
  813. track_obj = tracks.object_from_template(track_config, self, null);
  814. // Track uses only raw data.
  815. track_obj.data_manager.set('data_type', 'raw_data');
  816. // Set track block colors.
  817. track_obj.prefs.block_color = self.block_color;
  818. track_obj.prefs.reverse_strand_color = self.reverse_strand_color;
  819. pm_track.set('track', track_obj);
  820. });
  821. }, index * 10000);
  822. });
  823. });
  824. });
  825. }
  826. });
  827. return {
  828. SweepsterVisualization: SweepsterVisualization,
  829. SweepsterVisualizationView: SweepsterVisualizationView
  830. };
  831. });