commit/galaxy-central: jgoecks: Make it simpler to save/restore visualizations and enable saving/restoring of paramamonster visualizations.
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/changeset/603e4ed98fa7/ changeset: 603e4ed98fa7 user: jgoecks date: 2012-06-28 21:48:50 summary: Make it simpler to save/restore visualizations and enable saving/restoring of paramamonster visualizations. affected #: 9 files diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -353,8 +353,6 @@ viz_types = [ "trackster", "circster" ] - len_files = None - def create_visualization( self, trans, title, slug, type, dbkey, annotation=None, config={} ): """ Create visualiation and first revision. """ visualization = self._create_visualization( trans, title, type, dbkey, slug, annotation ) @@ -434,9 +432,9 @@ end = config['viewport']['end'] overview = config['viewport']['overview'] vis_rev.config[ "viewport" ] = { 'chrom': chrom, 'start': start, 'end': end, 'overview': overview } - elif type == 'circster': - # TODO. - pass + else: + # Default action is to save the config as is with no validation. + vis_rev.config = config vis.latest_revision = vis_rev session.add( vis_rev ) @@ -543,6 +541,10 @@ if 'viewport' in latest_revision.config: config['viewport'] = latest_revision.config['viewport'] + else: + # Default action is to return config unaltered. + latest_revision = visualization.latest_revision + config = latest_revision.config return config diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea lib/galaxy/web/controllers/tracks.py --- a/lib/galaxy/web/controllers/tracks.py +++ b/lib/galaxy/web/controllers/tracks.py @@ -16,7 +16,7 @@ from galaxy.model import NoConverterException, ConverterDependencyException from galaxy.visualization.tracks.data_providers import * from galaxy.visualization.genomes import decode_dbkey, Genomes -from galaxy.visualization.tracks.visual_analytics import get_tool_def, get_dataset_job +from galaxy.visualization.tracks.visual_analytics import get_dataset_job class NameColumn( grids.TextColumn ): @@ -471,12 +471,42 @@ @web.expose @web.require_login( "use Galaxy visualizations", use_panels=True ) - def paramamonster( self, trans, hda_ldda, dataset_id ): - # Get dataset. - dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) + def paramamonster( self, trans, id=None, hda_ldda=None, dataset_id=None, regions=None ): + if id: + # Loading a shared visualization. + viz = self.get_visualization( trans, id ) + viz_config = self.get_visualization_config( trans, viz ) + dataset = self.get_dataset( trans, viz_config[ 'dataset_id' ] ) + else: + # Loading new visualization. + dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) + job = get_dataset_job( dataset ) + viz_config = { + 'dataset_id': dataset_id, + 'tool_id': job.tool_id, + 'regions': regions + } + + viz_config[ 'regions' ] = [ + { + 'chrom': 'chr19', + 'start': '10000', + 'end': '26000' + }, + { + 'chrom': 'chr19', + 'start': '150000', + 'end': '175000' + } + + ] - return trans.fill_template_mako( "visualization/paramamonster.mako", dataset=dataset, - tool=self.app.toolbox.tools_by_id[ 'cufflinks' ].to_dict( trans, for_display=True ) ) + # Add tool, dataset attributes to config based on id. + tool = trans.app.toolbox.get_tool( viz_config[ 'tool_id' ] ) + viz_config[ 'tool' ] = tool.to_dict( trans, for_display=True ) + viz_config[ 'dataset' ] = dataset.get_api_value() + + return trans.fill_template_mako( "visualization/paramamonster.mako", config=viz_config ) @web.expose @web.require_login( "use Galaxy visualizations", use_panels=True ) diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea lib/galaxy/web/controllers/visualization.py --- a/lib/galaxy/web/controllers/visualization.py +++ b/lib/galaxy/web/controllers/visualization.py @@ -5,14 +5,26 @@ from galaxy.util.sanitize_html import sanitize_html class VisualizationListGrid( grids.Grid ): + def get_link( item ): + """ + Returns dictionary used to create item link. + """ + controller = "tracks" + if item.type == "trackster": + action = "browser" + elif item.type == "paramamonster": + action = "paramamonster" + elif item.type == "circster": + action = "circster" + return dict( controller=controller, action=action, id=item.id ) + # Grid definition title = "Saved Visualizations" model_class = model.Visualization default_sort_key = "-update_time" default_filter = dict( title="All", deleted="False", tags="All", sharing="All" ) columns = [ - grids.TextColumn( "Title", key="title", attach_popup=True, - link=( lambda item: dict( controller="tracks", action="browser", id=item.id ) ) ), + grids.TextColumn( "Title", key="title", attach_popup=True, link=get_link ), grids.TextColumn( "Type", key="type" ), grids.TextColumn( "Dbkey", key="dbkey" ), grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ), @@ -383,12 +395,20 @@ template="visualization/create.mako" ) @web.json - def save( self, trans, config, type, id=None, title=None, dbkey=None, annotation=None ): + def save( self, trans, vis_json=None, type=None, id=None, title=None, dbkey=None, annotation=None ): """ Save a visualization; if visualization does not have an ID, a new visualization is created. Returns JSON of visualization. """ - return self.save_visualization( trans, from_json_string( config ), type, id, title, dbkey, annotation ) + + # Get visualization attributes from kwargs or from config. + vis_config = from_json_string( vis_json ) + vis_type = type or vis_config[ 'type' ] + vis_id = id or vis_config.get( 'id', None ) + vis_title = title or vis_config.get( 'title', None ) + vis_dbkey = dbkey or vis_config.get( 'dbkey', None ) + vis_annotation = annotation or vis_config.get( 'annotation', None ) + return self.save_visualization( trans, vis_config, vis_type, vis_id, vis_title, vis_dbkey, vis_annotation ) @web.expose @web.require_login( "edit visualizations" ) diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea static/scripts/mvc/data.js --- a/static/scripts/mvc/data.js +++ b/static/scripts/mvc/data.js @@ -2,7 +2,7 @@ * A dataset. In Galaxy, datasets are associated with a history, so * this object is also known as a HistoryDatasetAssociation. */ -var Dataset = Backbone.Model.extend({ +var Dataset = Backbone.RelationalModel.extend({ defaults: { id: "", type: "", diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea static/scripts/mvc/tools.js --- a/static/scripts/mvc/tools.js +++ b/static/scripts/mvc/tools.js @@ -46,7 +46,8 @@ key: 'inputs', relatedModel: 'ToolInput', reverseRelation: { - key: 'tool' + key: 'tool', + includeInJSON: false } } ], diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea static/scripts/viz/paramamonster.js --- a/static/scripts/viz/paramamonster.js +++ b/static/scripts/viz/paramamonster.js @@ -6,7 +6,7 @@ /** * Tree for a tool's parameters. */ -var ToolParameterTree = Backbone.Model.extend({ +var ToolParameterTree = Backbone.RelationalModel.extend({ defaults: { tool: null, tree_data: null @@ -18,9 +18,6 @@ this.get('tool').get('inputs').each(function(input) { if (!input.get_samples()) { return; } - // All inputs are in tree to start. - self.add_param(input); - // Listen for changes to input's attributes. input.on('change:min change:max change:num_samples', function(input) { if (input.get('in_ptree')) { @@ -38,7 +35,16 @@ }, self); }); - self.set_tree_data(); + // If there is a config, use it. + if (options.config) { + _.each(options.config, function(input_config) { + var input = self.get('tool').get('inputs').find(function(input) { + return input.get('name') === input_config.name; + }); + self.add_param(input); + input.set(input_config); + }); + } }, add_param: function(param) { @@ -203,16 +209,51 @@ cur_node = find_child(cur_node.children); } return cur_node; + }, + + /** + * Returns a list of parameters used in tree. + */ + toJSON: function() { + // FIXME: returning and jsonifying complete param causes trouble on the server side, + // so just use essential attributes for now. + return this.get_tree_params().map(function(param) { + return { + name: param.get('name'), + min: param.get('min'), + max: param.get('max'), + num_samples: param.get('num_samples') + }; + }); } }); -var ParamaMonsterTrack = Backbone.Model.extend({ +var ParamaMonsterTrack = Backbone.RelationalModel.extend({ defaults: { track: null, settings: null, regions: null }, + relations: [ + { + type: Backbone.HasMany, + key: 'regions', + relatedModel: 'GenomeRegion' + } + ], + + initialize: function(options) { + // FIXME: find a better way to deal with needed URLs: + var track_config = _.extend({ + data_url: galaxy_paths.get('raw_data_url'), + converted_datasets_state_url: galaxy_paths.get('dataset_state_url') + }, options.track); + // HACK: remove prefs b/c they cause a redraw, which is not supported now. + delete track_config.mode; + this.set('track', object_from_template(track_config, {}, null)); + }, + same_settings: function(a_track) { var this_settings = this.get('settings'), other_settings = a_track.get('settings'); @@ -223,6 +264,14 @@ } } return true; + }, + + toJSON: function() { + return { + track: this.get('track').to_dict(), + settings: this.get('settings'), + regions: this.get('regions') + }; } }); @@ -235,18 +284,45 @@ */ var ParamaMonsterVisualization = Visualization.extend({ defaults: _.extend({}, Visualization.prototype.defaults, { + dataset: null, tool: null, parameter_tree: null, regions: null, tracks: null }), + + relations: [ + { + type: Backbone.HasOne, + key: 'dataset', + relatedModel: 'Dataset' + }, + { + type: Backbone.HasOne, + key: 'tool', + relatedModel: 'Tool' + }, + { + type: Backbone.HasMany, + key: 'regions', + relatedModel: 'GenomeRegion' + }, + { + type: Backbone.HasMany, + key: 'tracks', + relatedModel: 'ParamaMonsterTrack' + } + // NOTE: cannot use relationship for parameter tree because creating tree is complex. + ], initialize: function(options) { var tool_with_samplable_inputs = this.get('tool').copy(true); this.set('tool_with_samplable_inputs', tool_with_samplable_inputs); - this.set('parameter_tree', new ToolParameterTree({ tool: tool_with_samplable_inputs })); - this.set('tracks', new TrackCollection()); + this.set('parameter_tree', new ToolParameterTree({ + tool: tool_with_samplable_inputs, + config: options.tree_config + })); }, add_placeholder: function(settings) { @@ -255,6 +331,20 @@ add_track: function(track) { this.get('tracks').add(track); + }, + + toJSON: function() { + // TODO: could this be easier by using relational models? + return { + id: this.get('id'), + title: 'Parameter exploration for dataset \'' + this.get('dataset').get('name') + '\'', + type: 'paramamonster', + dataset_id: this.get('dataset').id, + tool_id: this.get('tool').id, + regions: this.get('regions').toJSON(), + tree_config: this.get('parameter_tree').toJSON(), + tracks: this.get('tracks').toJSON() + }; } }); @@ -301,7 +391,7 @@ settings_td.prepend(icon_menu.$el); // Render tile placeholders. - _.each(this.model.get('regions'), function() { + this.model.get('regions').each(function() { self.$el.append($('<td/>').addClass('tile').html( $('<img/>').attr('src', galaxy_paths.get('image_path') + '/loading_large_white_bg.gif') )); @@ -322,7 +412,7 @@ // When data is ready, draw tiles. $.when(track.data_manager.data_is_ready()).then(function(data_ok) { // Draw tile for each region. - _.each(regions, function(region, index) { + regions.each(function(region, index) { var resolution = region.length() / 300, w_scale = 1/resolution, mode = 'Pack'; @@ -550,6 +640,12 @@ // Handle node clicks for tree data. this.model.get('parameter_tree').on('change:tree_data', this.handle_node_clicks, this); + + // Each track must have a view so it has a canvas manager. + var self = this; + this.model.get('tracks').each(function(track) { + track.get('track').view = self; + }); }, render: function() { @@ -565,13 +661,17 @@ regions = self.model.get('regions'), tr = $('<tr/>').appendTo(this.track_collection_container); - _.each(regions, function(region) { + regions.each(function(region) { tr.append( $('<th>').text(region.toString()) ); }); tr.children().first().attr('colspan', 2); $('#right').append(this.track_collection_container); + self.model.get('tracks').each(function(track) { + self.add_track(track); + }); + // Render tool parameter tree in center panel. this.tool_param_tree_view.render(); $('#center').append(this.tool_param_tree_view.$el); @@ -588,6 +688,34 @@ }); }, + /** + * Add track to model and view. + */ + add_track: function(pm_track) { + var self = this; + self.model.add_track(pm_track); + var track_view = new ParamaMonsterTrackView({ + model: pm_track, + canvas_manager: self.canvas_manager + }); + track_view.on('run_on_dataset', self.run_tool_on_dataset, self); + self.track_collection_container.append(track_view.$el); + track_view.$el.hover(function() { + var settings_leaf = param_tree.get_leaf(settings); + var connected_node_ids = _.pluck(param_tree.get_connected_nodes(settings_leaf), 'id'); + + // TODO: can do faster with enter? + d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node") + .filter(function(d) { + return _.find(connected_node_ids, function(id) { return id === d.id; }) !== undefined; + }).style('fill', '#f00'); + }, + function() { + d3.select(self.tool_param_tree_view.$el[0]).selectAll("g.node").style('fill', '#000'); + }); + return pm_track; + }, + handle_node_clicks: function() { // When node clicked in tree, run tool and add tracks to model. var self = this, diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea static/scripts/viz/visualization.js --- a/static/scripts/viz/visualization.js +++ b/static/scripts/viz/visualization.js @@ -69,7 +69,7 @@ this.load_pattern( 'left_strand', "/visualization/strand_left.png" ); this.load_pattern( 'right_strand_inv', "/visualization/strand_right_inv.png" ); this.load_pattern( 'left_strand_inv', "/visualization/strand_left_inv.png" ); -} +}; _.extend( CanvasManager.prototype, { load_pattern: function( key, path ) { @@ -79,7 +79,7 @@ image.src = galaxy_paths.attributes.image_path + path; image.onload = function() { patterns[key] = dummy_context.createPattern( image, "repeat" ); - } + }; }, get_pattern: function( key ) { return this.patterns[key]; @@ -215,8 +215,8 @@ // ReferenceDataManager does not have dataset. if (dataset) { - params['dataset_id'] = dataset.id; - params['hda_ldda'] = dataset.get('hda_ldda'); + params.dataset_id = dataset.id; + params.hda_ldda = dataset.get('hda_ldda'); } $.extend(params, extra_params); @@ -265,17 +265,16 @@ // TODO: this logic could be improved if the visualization knew whether // the data was "index" or "data." // - var - key_ary = this.get('key_ary'), + var key_ary = this.get('key_ary'), obj_cache = this.get('obj_cache'), - key, region, entry_region, mode, entry; + key, entry_region; for (var i = 0; i < key_ary.length; i++) { entry_region = new GenomeRegion(key_ary[i]); if (entry_region.contains(region)) { // This entry has data in the requested range. Return if data // is compatible and can be subsetted. - var entry = obj_cache[key]; + entry = obj_cache[key]; if ( is_deferred(entry) || ( this.get('data_mode_compatible')(entry, mode) && this.get('can_subset')(entry) ) ) { this.move_key_to_end(key, i); @@ -298,8 +297,6 @@ this.set_elt(region, entry); }, - /** - /** "Deep" data request; used as a parameter for DataManager.get_more_data() */ DEEP_DATA_REQ: "deep", @@ -341,7 +338,7 @@ // var data_manager = this, - new_data_request = this.load_data(query_region, mode, resolution, extra_params) + new_data_request = this.load_data(query_region, mode, resolution, extra_params), new_data_available = $.Deferred(); // load_data sets cache to new_data_request, but use custom deferred object so that signal and data // is all data, not just new data. @@ -408,7 +405,7 @@ /** * A genomic region. */ -var GenomeRegion = Backbone.Model.extend({ +var GenomeRegion = Backbone.RelationalModel.extend({ defaults: { chrom: null, start: 0, @@ -431,9 +428,9 @@ !this.get('end') && 'as_str' in options) { var pieces = options.as_str.split(':'), chrom = pieces[0], - pieces = pieces.split('-'), - start = pieces[0], - end = pieces[1]; + start_end = pieces.split('-'), + start = start_end[0], + end = start_end[1]; this.set('chrom', chrom); this.set('start', start); this.set('end', end); @@ -462,7 +459,7 @@ chrom: this.get('chrom'), start: this.get('start'), end: this.get('end') - } + }; }, /** @@ -556,7 +553,7 @@ datasets: [] }, - url: function() { return galaxy_paths.get("visualization_url"); }, + url: galaxy_paths.get("visualization_url"), /** * POSTs visualization's JSON to its URL using the parameter 'vis_json' @@ -565,7 +562,7 @@ */ save: function() { return $.ajax({ - url: this.url(), + url: this.url, type: "POST", dataType: "json", data: { @@ -818,7 +815,7 @@ requests[requests.length] = $.ajax({ url: add_track_async_url, data: data, - dataType: "json", + dataType: "json" }); }); // To preserve order, wait until there are definitions for all tracks and then add diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea templates/base_panels.mako --- a/templates/base_panels.mako +++ b/templates/base_panels.mako @@ -57,7 +57,8 @@ data_url: '${h.url_for( controller="tracks", action="data" )}', raw_data_url: '${h.url_for( controller="tracks", action="raw_data" )}', converted_datasets_state_url: '${h.url_for( controller="tracks", action="converted_datasets_state" )}', - dataset_state_url: '${h.url_for( controller="tracks", action="dataset_state" )}' + dataset_state_url: '${h.url_for( controller="tracks", action="dataset_state" )}', + visualization_url: '${h.url_for( controller="visualization", action="save" )}' }); </script></%def> diff -r 063a1d690724b3485f41c620f664f11c7390ceef -r 603e4ed98fa7cb3b3c47f71b3b77bedd67f50fea templates/visualization/paramamonster.mako --- a/templates/visualization/paramamonster.mako +++ b/templates/visualization/paramamonster.mako @@ -90,34 +90,43 @@ <script type="text/javascript"> var viz; $(function() { - // -- Viz set up. -- - - var tool = new Tool(JSON.parse('${ h.to_json_string( tool ) }')), - regions = [ - new GenomeRegion({ - chrom: 'chr19', - start: '10000', - end: '26000' - }), - new GenomeRegion({ - chrom: 'chr19', - start: '150000', - end: '175000' - }) - ], - // HACK: need to replace \ with \\ due to simplejson bug. - dataset = new Dataset(JSON.parse('${ h.to_json_string( dataset.get_api_value() ).replace('\\', '\\\\' ) }')); - - - viz = new ParamaMonsterVisualization({ - tool: tool, - dataset: dataset, - regions: regions - }); + // -- Viz set up. -- + var viz = new ParamaMonsterVisualization( + ${ h.to_json_string( config ).replace('\\', '\\\\' )} + ); var viz_view = new ParamaMonsterVisualizationView({ model: viz }); viz_view.render(); $('.unified-panel-body').append(viz_view.$el); + + // -- Menu set up. -- + var menu = create_icon_buttons_menu([ + { icon_class: 'disk--arrow', title: 'Save', on_click: function() { + // Show saving dialog box + show_modal("Saving...", "progress"); + + viz.save().success(function(vis_info) { + hide_modal(); + viz.set({ + 'id': vis_info.vis_id, + 'has_changes': false + }); + }) + .error(function() { + show_modal( "Could Not Save", "Could not save visualization. Please try again later.", + { "Close" : hide_modal } ); + }); + } }, + { icon_class: 'cross-circle', title: 'Close', on_click: function() { + window.location = "${h.url_for( controller='visualization', action='list' )}"; + } } + ], + { + tipsy_config: {gravity: 'n'} + }); + + menu.$el.attr("style", "float: right"); + $("#right .unified-panel-header-inner").append(menu.$el); }); </script></%def> @@ -129,4 +138,8 @@ </%def><%def name="right_panel()"> + <div class="unified-panel-header" unselectable="on"> + <div class="unified-panel-header-inner"> + </div> + </div></%def> \ No newline at end of file Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
Bitbucket