PageRenderTime 77ms CodeModel.GetById 2ms app.highlight 67ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/viz/paramamonster.js

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