PageRenderTime 60ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/viz/sweepster.js

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