Make it simpler to save/restore visualizations and enable saving/restoring of paramamonster visualizations.

This commit is contained in:
Jeremy Goecks
2012-06-28 15:48:50 -04:00
parent 7d4aefe6be
commit baf5dc82ee
9 changed files with 264 additions and 72 deletions
+7 -5
View File
@@ -353,8 +353,6 @@ class UsesVisualizationMixin( SharableItemSecurityMixin, UsesLibraryMixinItems )
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 @@ class UsesVisualizationMixin( SharableItemSecurityMixin, UsesLibraryMixinItems )
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 @@ class UsesVisualizationMixin( SharableItemSecurityMixin, UsesLibraryMixinItems )
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
+36 -6
View File
@@ -16,7 +16,7 @@ from galaxy.datatypes.interval import Gff, Bed
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 @@ class TracksController( BaseUIController, UsesVisualizationMixin, UsesHistoryDat
@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 )
+24 -4
View File
@@ -5,14 +5,26 @@ from galaxy.web.framework.helpers import time_ago, grids, iff
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 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
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" )
+1 -1
View File
@@ -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: "",
+2 -1
View File
@@ -46,7 +46,8 @@ var Tool = BaseModel.extend({
key: 'inputs',
relatedModel: 'ToolInput',
reverseRelation: {
key: 'tool'
key: 'tool',
includeInJSON: false
}
}
],
+139 -11
View File
@@ -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 @@ var ToolParameterTree = Backbone.Model.extend({
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 @@ var ToolParameterTree = Backbone.Model.extend({
}, 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 @@ var ToolParameterTree = Backbone.Model.extend({
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 @@ var ParamaMonsterTrack = Backbone.Model.extend({
}
}
return true;
},
toJSON: function() {
return {
track: this.get('track').to_dict(),
settings: this.get('settings'),
regions: this.get('regions')
};
}
});
@@ -235,18 +284,45 @@ var TrackCollection = Backbone.Collection.extend({
*/
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 @@ var ParamaMonsterVisualization = Visualization.extend({
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 @@ var ParamaMonsterTrackView = Backbone.View.extend({
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 @@ var ParamaMonsterTrackView = Backbone.View.extend({
// 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 @@ var ParamaMonsterVisualizationView = Backbone.View.extend({
// 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 @@ var ParamaMonsterVisualizationView = Backbone.View.extend({
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 @@ var ParamaMonsterVisualizationView = Backbone.View.extend({
});
},
/**
* 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,
+16 -19
View File
@@ -69,7 +69,7 @@ var CanvasManager = function(default_font) {
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 @@ _.extend( CanvasManager.prototype, {
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 @@ var GenomeDataManager = Cache.extend({
// 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 @@ var GenomeDataManager = Cache.extend({
// 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 @@ var GenomeDataManager = Cache.extend({
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 GenomeDataManager = Cache.extend({
//
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 @@ var Genome = Backbone.Model.extend({
/**
* A genomic region.
*/
var GenomeRegion = Backbone.Model.extend({
var GenomeRegion = Backbone.RelationalModel.extend({
defaults: {
chrom: null,
start: 0,
@@ -431,9 +428,9 @@ var GenomeRegion = Backbone.Model.extend({
!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 @@ var GenomeRegion = Backbone.Model.extend({
chrom: this.get('chrom'),
start: this.get('start'),
end: this.get('end')
}
};
},
/**
@@ -556,7 +553,7 @@ var Visualization = Backbone.RelationalModel.extend({
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 @@ var Visualization = Backbone.RelationalModel.extend({
*/
save: function() {
return $.ajax({
url: this.url(),
url: this.url,
type: "POST",
dataType: "json",
data: {
@@ -818,7 +815,7 @@ var add_datasets = function(dataset_url, add_track_async_url, success_fn) {
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
+2 -1
View File
@@ -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>
+37 -24
View File
@@ -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>