PageRenderTime 69ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/client/galaxy/scripts/viz/sweepster.js

https://bitbucket.org/natefoo/galaxy-central
JavaScript | 942 lines | 664 code | 118 blank | 160 comment | 31 complexity | f5a9b126251b332e7c5cb5e00fa76d15 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", "utils/config", "mvc/ui/icon-button"],
  6. function(_, d3, util, visualization, tracks, tools, data, config, mod_icon_btn) {
  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.Model.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. // 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.Model.extend({
  223. defaults: {
  224. track: null,
  225. mode: 'Pack',
  226. settings: null,
  227. regions: null
  228. },
  229. initialize: function(options) {
  230. this.set('regions', options.regions);
  231. if (options.track) {
  232. // FIXME: find a better way to deal with needed URLs:
  233. var track_config = _.extend({
  234. data_url: galaxy_config.root + 'dummy1',
  235. converted_datasets_state_url: galaxy_config.root + 'dummy2'
  236. }, options.track);
  237. this.set('track', tracks.object_from_template(track_config, {}, null));
  238. }
  239. },
  240. same_settings: function(a_track) {
  241. var this_settings = this.get('settings'),
  242. other_settings = a_track.get('settings');
  243. for (var prop in this_settings) {
  244. if (!other_settings[prop] ||
  245. this_settings[prop] !== other_settings[prop]) {
  246. return false;
  247. }
  248. }
  249. return true;
  250. },
  251. toJSON: function() {
  252. return {
  253. track: this.get('track').to_dict(),
  254. settings: this.get('settings'),
  255. regions: this.get('regions')
  256. };
  257. }
  258. });
  259. var TrackCollection = Backbone.Collection.extend({
  260. model: SweepsterTrack
  261. });
  262. /**
  263. * Sweepster visualization model.
  264. */
  265. var SweepsterVisualization = visualization.Visualization.extend({
  266. defaults: _.extend({}, visualization.Visualization.prototype.defaults, {
  267. dataset: null,
  268. tool: null,
  269. parameter_tree: null,
  270. regions: null,
  271. tracks: null,
  272. default_mode: 'Pack'
  273. }),
  274. initialize: function(options) {
  275. this.set('dataset', new data.Dataset(options.dataset));
  276. this.set('tool', new tools.Tool(options.tool));
  277. this.set('regions', new visualization.GenomeRegionCollection(options.regions));
  278. this.set('tracks', new TrackCollection(options.tracks));
  279. var tool_with_samplable_inputs = this.get('tool');
  280. this.set('tool_with_samplable_inputs', tool_with_samplable_inputs);
  281. // Remove complex parameters for now.
  282. tool_with_samplable_inputs.remove_inputs( [ 'data', 'hidden_data', 'conditional', 'text' ] );
  283. this.set('parameter_tree', new ToolParameterTree({
  284. tool: tool_with_samplable_inputs,
  285. config: options.tree_config
  286. }));
  287. },
  288. add_track: function(track) {
  289. this.get('tracks').add(track);
  290. },
  291. toJSON: function() {
  292. return {
  293. id: this.get('id'),
  294. title: 'Parameter exploration for dataset \'' + this.get('dataset').get('name') + '\'',
  295. type: 'sweepster',
  296. dataset_id: this.get('dataset').id,
  297. tool_id: this.get('tool').id,
  298. regions: this.get('regions').toJSON(),
  299. tree_config: this.get('parameter_tree').toJSON(),
  300. tracks: this.get('tracks').toJSON()
  301. };
  302. }
  303. });
  304. /**
  305. * --- Views ---
  306. */
  307. /**
  308. * Sweepster track view.
  309. */
  310. var SweepsterTrackView = Backbone.View.extend({
  311. tagName: 'tr',
  312. TILE_LEN: 250,
  313. initialize: function(options) {
  314. this.canvas_manager = options.canvas_manager;
  315. this.render();
  316. this.model.on('change:track change:mode', this.draw_tiles, this);
  317. },
  318. render: function() {
  319. // Render settings icon and popup.
  320. // TODO: use template.
  321. var settings = this.model.get('settings'),
  322. values = settings.get('values'),
  323. settings_td = $('<td/>').addClass('settings').appendTo(this.$el),
  324. settings_div = $('<div/>').addClass('track-info').hide().appendTo(settings_td);
  325. settings_div.append( $('<div/>').css('font-weight', 'bold').text('Track Settings') );
  326. settings.get('inputs').each(function(input) {
  327. settings_div.append( input.get('label') + ': ' + values[input.get('name')] + '<br/>');
  328. });
  329. var self = this,
  330. run_on_dataset_button = $('<button/>').appendTo(settings_div).text('Run on complete dataset').click(function() {
  331. settings_div.toggle();
  332. self.trigger('run_on_dataset', settings);
  333. });
  334. var icon_menu = mod_icon_btn.create_icon_buttons_menu([
  335. {
  336. title: 'Settings',
  337. icon_class: 'gear track-settings',
  338. on_click: function() {
  339. settings_div.toggle();
  340. }
  341. },
  342. {
  343. title: 'Remove',
  344. icon_class: 'cross-circle',
  345. on_click: function() {
  346. self.$el.remove();
  347. $('.tooltip').remove();
  348. // TODO: remove track from viz collection.
  349. }
  350. }
  351. ]);
  352. settings_td.prepend(icon_menu.$el);
  353. // Render tile placeholders.
  354. this.model.get('regions').each(function() {
  355. self.$el.append($('<td/>').addClass('tile').html(
  356. $('<img/>').attr('src', galaxy_config.root + 'images/loading_large_white_bg.gif')
  357. ));
  358. });
  359. if (this.model.get('track')) {
  360. this.draw_tiles();
  361. }
  362. },
  363. /**
  364. * Draw tiles for regions.
  365. */
  366. draw_tiles: function() {
  367. var self = this,
  368. track = this.model.get('track'),
  369. regions = this.model.get('regions'),
  370. tile_containers = this.$el.find('td.tile');
  371. // Do nothing if track is not defined.
  372. if (!track) { return; }
  373. // When data is ready, draw tiles.
  374. $.when(track.data_manager.data_is_ready()).then(function(data_ok) {
  375. // Draw tile for each region.
  376. regions.each(function(region, index) {
  377. var resolution = region.length() / self.TILE_LEN,
  378. w_scale = 1/resolution,
  379. mode = self.model.get('mode');
  380. $.when(track.data_manager.get_data(region, mode, resolution, {})).then(function(tile_data) {
  381. var canvas = self.canvas_manager.new_canvas();
  382. canvas.width = self.TILE_LEN;
  383. canvas.height = track.get_canvas_height(tile_data, mode, w_scale, canvas.width);
  384. track.draw_tile(tile_data, canvas.getContext('2d'), mode, region, w_scale);
  385. $(tile_containers[index]).empty().append(canvas);
  386. });
  387. });
  388. });
  389. }
  390. });
  391. /**
  392. * Tool input (parameter) that enables both value and sweeping inputs. View is unusual as
  393. * it augments an existing input form row rather than creates a completely new HTML element.
  394. */
  395. var ToolInputValOrSweepView = Backbone.View.extend({
  396. // Template for rendering sweep inputs:
  397. number_input_template: '<div class="form-row-input sweep">' +
  398. '<input class="min" type="text" size="6" value="<%= min %>"> - ' +
  399. '<input class="max" type="text" size="6" value="<%= max %>">' +
  400. ' samples: <input class="num_samples" type="text" size="1" value="<%= num_samples %>">' +
  401. '</div>',
  402. select_input_template: '<div class="form-row-input sweep"><%= options %></div>',
  403. initialize: function(options) {
  404. this.$el = options.tool_row;
  405. this.render();
  406. },
  407. render: function() {
  408. var input = this.model,
  409. type = input.get('type'),
  410. single_input_row = this.$el.find('.form-row-input'),
  411. sweep_inputs_row = null;
  412. // Update tool inputs as single input changes.
  413. single_input_row.find(':input').change(function() {
  414. input.set('value', $(this).val());
  415. });
  416. // Add row for parameter sweep inputs.
  417. if (input instanceof tools.IntegerToolParameter) {
  418. sweep_inputs_row = $(_.template(this.number_input_template, this.model.toJSON()));
  419. }
  420. else if (input instanceof tools.SelectToolParameter) {
  421. var options = _.map(this.$el.find('select option'), function(option) {
  422. return $(option).val();
  423. }),
  424. options_text = options.join(', ');
  425. sweep_inputs_row = $(_.template(this.select_input_template, {
  426. options: options_text
  427. }));
  428. }
  429. sweep_inputs_row.insertAfter(single_input_row);
  430. // Add buttons for adding/removing parameter.
  431. var self = this,
  432. menu = mod_icon_btn.create_icon_buttons_menu([
  433. {
  434. title: 'Add parameter to tree',
  435. icon_class: 'plus-button',
  436. on_click: function () {
  437. input.set('in_ptree', true);
  438. single_input_row.hide();
  439. sweep_inputs_row.show();
  440. $(this).hide();
  441. self.$el.find('.icon-button.toggle').show();
  442. }
  443. },
  444. {
  445. title: 'Remove parameter from tree',
  446. icon_class: 'toggle',
  447. on_click: function() {
  448. // Remove parameter from tree params where name matches clicked paramter.
  449. input.set('in_ptree', false);
  450. sweep_inputs_row.hide();
  451. single_input_row.show();
  452. $(this).hide();
  453. self.$el.find('.icon-button.plus-button').show();
  454. }
  455. }
  456. ],
  457. {
  458. });
  459. this.$el.prepend(menu.$el);
  460. // Show/hide input rows and icons depending on whether parameter is in the tree.
  461. if (input.get('in_ptree')) {
  462. single_input_row.hide();
  463. self.$el.find('.icon-button.plus-button').hide();
  464. }
  465. else {
  466. self.$el.find('.icon-button.toggle').hide();
  467. sweep_inputs_row.hide();
  468. }
  469. // Update input's min, max, number of samples as values change.
  470. _.each(['min', 'max', 'num_samples'], function(attr) {
  471. sweep_inputs_row.find('.' + attr).change(function() {
  472. input.set(attr, parseFloat( $(this).val() ));
  473. });
  474. });
  475. }
  476. });
  477. var ToolParameterTreeDesignView = Backbone.View.extend({
  478. className: 'tree-design',
  479. initialize: function(options) {
  480. this.render();
  481. },
  482. render: function() {
  483. // Start with tool form view.
  484. var tool_form_view = new tools.ToolFormView({
  485. model: this.model.get('tool')
  486. });
  487. tool_form_view.render();
  488. this.$el.append(tool_form_view.$el);
  489. // Set up views for each tool input.
  490. var self = this,
  491. inputs = self.model.get('tool').get('inputs');
  492. this.$el.find('.form-row').not('.form-actions').each(function(i) {
  493. var input_view = new ToolInputValOrSweepView({
  494. model: inputs.at(i),
  495. tool_row: $(this)
  496. });
  497. });
  498. }
  499. });
  500. /**
  501. * Displays and updates parameter tree.
  502. */
  503. var ToolParameterTreeView = Backbone.View.extend({
  504. className: 'tool-parameter-tree',
  505. initialize: function(options) {
  506. // When tree data changes, re-render.
  507. this.model.on('change:tree_data', this.render, this);
  508. },
  509. render: function() {
  510. // Start fresh.
  511. this.$el.children().remove();
  512. var tree_params = this.model.get_tree_params();
  513. if (!tree_params.length) {
  514. return;
  515. }
  516. // Set width, height based on params and samples.
  517. this.width = 100 * (2 + tree_params.length);
  518. this.height = 15 * this.model.get_num_leaves();
  519. var self = this;
  520. // Layout tree.
  521. var cluster = d3.layout.cluster()
  522. .size([this.height, this.width - 160]);
  523. var diagonal = d3.svg.diagonal()
  524. .projection(function(d) { return [d.y, d.x]; });
  525. // Layout nodes.
  526. var nodes = cluster.nodes(this.model.get('tree_data'));
  527. // Setup and add labels for tree levels.
  528. var param_depths = _.uniq(_.pluck(nodes, "y"));
  529. _.each(tree_params, function(param, index) {
  530. var x = param_depths[index+1],
  531. center_left = $('#center').position().left;
  532. self.$el.append( $('<div>').addClass('label')
  533. .text(param.get('label'))
  534. .css('left', x + center_left) );
  535. });
  536. // Set up vis element.
  537. var vis = d3.select(this.$el[0])
  538. .append("svg")
  539. .attr("width", this.width)
  540. .attr("height", this.height + 30)
  541. .append("g")
  542. .attr("transform", "translate(40, 20)");
  543. // Draw links.
  544. var link = vis.selectAll("path.link")
  545. .data(cluster.links(nodes))
  546. .enter().append("path")
  547. .attr("class", "link")
  548. .attr("d", diagonal);
  549. // Draw nodes.
  550. var node = vis.selectAll("g.node")
  551. .data(nodes)
  552. .enter().append("g")
  553. .attr("class", "node")
  554. .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
  555. .on('mouseover', function(a_node) {
  556. var connected_node_ids = _.pluck(self.model.get_connected_nodes(a_node), 'id');
  557. // TODO: probably can use enter() to do this more easily.
  558. node.filter(function(d) {
  559. return _.find(connected_node_ids, function(id) { return id === d.id; }) !== undefined;
  560. }).style('fill', '#f00');
  561. })
  562. .on('mouseout', function() {
  563. node.style('fill', '#000');
  564. });
  565. node.append("circle")
  566. .attr("r", 9);
  567. node.append("text")
  568. .attr("dx", function(d) { return d.children ? -12 : 12; })
  569. .attr("dy", 3)
  570. .attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
  571. .text(function(d) { return d.name; });
  572. }
  573. });
  574. /**
  575. * Sweepster visualization view. View requires rendering in 3-panel setup for now.
  576. */
  577. var SweepsterVisualizationView = Backbone.View.extend({
  578. className: 'Sweepster',
  579. helpText:
  580. '<div><h4>Getting Started</h4>' +
  581. '<ol><li>Create a parameter tree by using the icons next to the tool\'s parameter names to add or remove parameters.' +
  582. '<li>Adjust the tree by using parameter inputs to select min, max, and number of samples' +
  583. '<li>Run the tool with different settings by clicking on tree nodes' +
  584. '</ol></div>',
  585. initialize: function(options) {
  586. this.canvas_manager = new visualization.CanvasManager(this.$el.parents('body'));
  587. this.tool_param_tree_view = new ToolParameterTreeView({ model: this.model.get('parameter_tree') });
  588. this.track_collection_container = $('<table/>').addClass('tracks');
  589. // Handle node clicks for tree data.
  590. this.model.get('parameter_tree').on('change:tree_data', this.handle_node_clicks, this);
  591. // Each track must have a view so it has a canvas manager.
  592. var self = this;
  593. this.model.get('tracks').each(function(track) {
  594. track.get('track').view = self;
  595. });
  596. // Set block, reverse strand block colors; these colors will be used for all tracks.
  597. this.config = config.ConfigSettingCollection.from_models_and_saved_values(
  598. [
  599. { key: 'name', label: 'Name', type: 'text', default_value: '' },
  600. { key: 'a_color', label: 'A Color', type: 'color', default_value: "#FF0000" },
  601. { key: 'c_color', label: 'C Color', type: 'color', default_value: "#00FF00" },
  602. { key: 'g_color', label: 'G Color', type: 'color', default_value: "#0000FF" },
  603. { key: 't_color', label: 'T Color', type: 'color', default_value: "#FF00FF" },
  604. { key: 'n_color', label: 'N Color', type: 'color', default_value: "#AAAAAA" },
  605. { key: 'block_color', label: 'Block color', type: 'color' },
  606. { key: 'reverse_strand_color', label: 'Antisense strand color', type: 'color' },
  607. ], {}
  608. );
  609. },
  610. render: function() {
  611. // Render tree design view in left panel.
  612. var tree_design_view = new ToolParameterTreeDesignView({
  613. model: this.model.get('parameter_tree')
  614. });
  615. $('#left').append(tree_design_view.$el);
  616. // Render track collection container/view in right panel.
  617. var self = this,
  618. regions = self.model.get('regions'),
  619. tr = $('<tr/>').appendTo(this.track_collection_container);
  620. regions.each(function(region) {
  621. tr.append( $('<th>').text(region.toString()) );
  622. });
  623. tr.children().first().attr('colspan', 2);
  624. var tracks_div = $('<div>').addClass('tiles');
  625. $('#right').append( tracks_div.append(this.track_collection_container) );
  626. self.model.get('tracks').each(function(track) {
  627. self.add_track(track);
  628. });
  629. // -- Render help and tool parameter tree in center panel. --
  630. // Help includes text and a close button.
  631. var help_div = $(this.helpText).addClass('help'),
  632. close_button = mod_icon_btn.create_icon_buttons_menu([
  633. {
  634. title: 'Close',
  635. icon_class: 'cross-circle',
  636. on_click: function() {
  637. $('.tooltip').remove();
  638. help_div.remove();
  639. }
  640. }
  641. ]);
  642. help_div.prepend(close_button.$el.css('float', 'right'));
  643. $('#center').append(help_div);
  644. // Parameter tree:
  645. this.tool_param_tree_view.render();
  646. $('#center').append(this.tool_param_tree_view.$el);
  647. // Set up handler for tree node clicks.
  648. this.handle_node_clicks();
  649. // Set up visualization menu.
  650. var menu = mod_icon_btn.create_icon_buttons_menu(
  651. [
  652. // Save.
  653. /*
  654. { icon_class: 'disk--arrow', title: 'Save', on_click: function() {
  655. // Show saving dialog box
  656. show_modal("Saving...", "progress");
  657. viz.save().success(function(vis_info) {
  658. hide_modal();
  659. viz.set({
  660. 'id': vis_info.vis_id,
  661. 'has_changes': false
  662. });
  663. })
  664. .error(function() {
  665. show_modal( "Could Not Save", "Could not save visualization. Please try again later.",
  666. { "Close" : hide_modal } );
  667. });
  668. } },
  669. */
  670. // Change track modes.
  671. {
  672. icon_class: 'chevron-expand',
  673. title: 'Set display mode'
  674. },
  675. // Close viz.
  676. {
  677. icon_class: 'cross-circle',
  678. title: 'Close',
  679. on_click: function() {
  680. window.location = "${h.url_for( controller='visualization', action='list' )}";
  681. }
  682. }
  683. ],
  684. {
  685. tooltip_config: {placement: 'bottom'}
  686. });
  687. // Create mode selection popup. Mode selection changes default mode and mode for all tracks.
  688. var modes = ['Squish', 'Pack'],
  689. mode_mapping = {};
  690. _.each(modes, function(mode) {
  691. mode_mapping[mode] = function() {
  692. self.model.set('default_mode', mode);
  693. self.model.get('tracks').each(function(track) {
  694. track.set('mode', mode);
  695. });
  696. };
  697. });
  698. make_popupmenu(menu.$el.find('.chevron-expand'), mode_mapping);
  699. menu.$el.attr("style", "float: right");
  700. $("#right .unified-panel-header-inner").append(menu.$el);
  701. },
  702. get_base_color: function(base) {
  703. return this.config.get_value(base.toLowerCase() + '_color') ||
  704. this.config.get_value('n_color');
  705. },
  706. run_tool_on_dataset: function(settings) {
  707. var tool = this.model.get('tool'),
  708. tool_name = tool.get('name'),
  709. dataset = this.model.get('dataset');
  710. tool.set_input_values(settings.get('values'));
  711. $.when(tool.rerun(dataset)).then(function(outputs) {
  712. // TODO.
  713. });
  714. show_modal('Running ' + tool_name + ' on complete dataset',
  715. tool_name + ' is running on dataset \'' +
  716. dataset.get('name') + '\'. Outputs are in the dataset\'s history.',
  717. {
  718. 'Ok': function() { hide_modal(); }
  719. });
  720. },
  721. /**
  722. * Add track to model and view.
  723. */
  724. add_track: function(pm_track) {
  725. var self = this,
  726. param_tree = this.model.get('parameter_tree');
  727. // Add track to model.
  728. self.model.add_track(pm_track);
  729. var track_view = new SweepsterTrackView({
  730. model: pm_track,
  731. canvas_manager: self.canvas_manager
  732. });
  733. track_view.on('run_on_dataset', self.run_tool_on_dataset, self);
  734. self.track_collection_container.append(track_view.$el);
  735. track_view.$el.hover(function() {
  736. var settings_leaf = param_tree.get_leaf(pm_track.get('settings').get('values'));
  737. var connected_node_ids = _.pluck(param_tree.get_connected_nodes(settings_leaf), 'id');
  738. // TODO: can do faster with enter?
  739. d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node")
  740. .filter(function(d) {
  741. return _.find(connected_node_ids, function(id) { return id === d.id; }) !== undefined;
  742. }).style('fill', '#f00');
  743. },
  744. function() {
  745. d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node").style('fill', '#000');
  746. });
  747. return pm_track;
  748. },
  749. /**
  750. * Sets up handling when tree nodes are clicked. When a node is clicked, the tool is run for each of
  751. * the settings defined by the node's subtree and tracks are added for each run.
  752. */
  753. handle_node_clicks: function() {
  754. // When node clicked in tree, run tool and add tracks to model.
  755. var self = this,
  756. param_tree = this.model.get('parameter_tree'),
  757. regions = this.model.get('regions'),
  758. node = d3.select(this.tool_param_tree_view.$el[0]).selectAll("g.node");
  759. node.on("click", function(d, i) {
  760. // Get all settings corresponding to node.
  761. var tool = self.model.get('tool'),
  762. dataset = self.model.get('dataset'),
  763. all_settings = param_tree.get_node_settings(d),
  764. run_jobs_deferred = $.Deferred();
  765. // Do not allow 10+ jobs to be run.
  766. if (all_settings.length >= 10) {
  767. show_modal("Whoa there cowboy!",
  768. "You clicked on a node to try " + self.model.get('tool').get('name') +
  769. " with " + all_settings.length +
  770. " different combinations of settings. You can only run 10 jobs at a time.",
  771. {
  772. "Ok": function() { hide_modal(); run_jobs_deferred.resolve(false); }
  773. });
  774. }
  775. else {
  776. run_jobs_deferred.resolve(true);
  777. }
  778. // Take action when deferred resolves.
  779. $.when(run_jobs_deferred).then(function(run_jobs) {
  780. if (!run_jobs) { return; }
  781. // Create and add tracks for each settings group.
  782. var new_tracks = _.map(all_settings, function(settings) {
  783. var pm_track = new SweepsterTrack({
  784. settings: settings,
  785. regions: regions,
  786. mode: self.model.get('default_mode')
  787. });
  788. self.add_track(pm_track);
  789. return pm_track;
  790. });
  791. // For each track, run tool using track's settings and update track.
  792. _.each(new_tracks, function(pm_track, index) {
  793. setTimeout(function() {
  794. // Set inputs and run tool.
  795. tool.set_input_values(pm_track.get('settings').get('values'));
  796. $.when(tool.rerun(dataset, regions)).then(function(output) {
  797. // HACKish: output is an HDA with track config attribute. To create a track
  798. // that works correctly with Backbone relational, it is necessary to
  799. // use a modified version of the track config.
  800. var dataset = output.first(),
  801. track_config = dataset.get('track_config');
  802. // Set dataset to be the tool's output.
  803. track_config.dataset = dataset;
  804. // Set tool to null so that it is not unpacked; unpacking it messes with
  805. // the tool parameters and parameter tree.
  806. track_config.tool = null;
  807. track_config.prefs = self.config.to_key_value_dict();
  808. // Create and add track for output dataset.
  809. var track_obj = tracks.object_from_template(track_config, self, null);
  810. track_obj.init_for_tool_data();
  811. pm_track.set('track', track_obj);
  812. });
  813. }, index * 10000);
  814. });
  815. });
  816. });
  817. }
  818. });
  819. return {
  820. SweepsterVisualization: SweepsterVisualization,
  821. SweepsterVisualizationView: SweepsterVisualizationView
  822. };
  823. });