PageRenderTime 55ms CodeModel.GetById 6ms RepoModel.GetById 0ms app.codeStats 0ms

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

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