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

/static/scripts/viz/paramamonster.js

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