Visualizations Registry: default to on

This commit is contained in:
Carl Eberhard
2014-01-14 10:04:20 -05:00
parent 7bb726c80b
commit d4aa83f35f
15 changed files with 349 additions and 39 deletions
@@ -7,9 +7,15 @@
<test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
<data_source>
<model_class>Visualization</model_class>
<test test_attr="type">scatterplot</test>
<to_param param_attr="id">visualization_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
<param type="visualization" var_name_in_template="visualization">visualization_id</param>
</params>
<template>scatterplot.mako</template>
</visualization>
@@ -10,6 +10,7 @@ todo:
Allow setting perPage of config
Auto render if given data and/or config
Allow option to auto set width/height based on screen real estate avail.
Handle large number of pages better (Known genes hg19)
Use d3.nest to allow grouping, pagination/filtration by group (e.g. chromCol)
Semantic HTML (figure, caption)
Save as visualization, load from visualization
@@ -29,26 +30,28 @@ todo:
* configuring which data will be used
* configuring the plot display
*/
var ScatterplotConfigEditor = BaseView.extend( LoggableMixin ).extend({
var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({
//TODO: !should be a view on a visualization model
//logger : console,
className : 'scatterplot-control-form',
/** initialize requires a configuration Object containing a dataset Object */
initialize : function( attributes ){
//console.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.config || !attributes.dataset ){
throw new Error( "ScatterplotView requires a configuration and dataset" );
if( !this.model ){
this.model = new Visualization({ type: 'scatterplot' });
}
//console.log( 'config:', attributes.config );
console.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.dataset ){
throw new Error( "ScatterplotConfigEditor requires a dataset" );
}
this.dataset = attributes.dataset;
//console.log( 'dataset:', this.dataset );
console.log( 'dataset:', this.dataset );
//TODO: ScatterplotView -> ScatterplotDisplay, this.plotView -> this.display
this.plotView = new ScatterplotView({
dataset : attributes.dataset,
config : attributes.config
model : this.model
//TODO: if data
});
},
@@ -58,8 +61,7 @@ var ScatterplotConfigEditor = BaseView.extend( LoggableMixin ).extend({
//console.log( this + '.render' );
// render the tab controls, areas and loading indicator
this.$el.append( ScatterplotConfigEditor.templates.mainLayout({
}));
this.$el.append( ScatterplotConfigEditor.templates.mainLayout({}));
// render the tab content
this.$el.find( '#data-control' ).append( this._render_dataControl() );
File diff suppressed because one or more lines are too long
@@ -1,3 +1,24 @@
<%
hda_dict = trans.security.encode_dict_ids( hda.to_dict() )
config = query_args
title = "Scatterplot of '" + hda.name + "'"
info = hda.info
visualization = context.get( 'visualization' )
if visualization is not None:
config = visualization.latest_revision.config
config.update( query_args )
title = visualization.title
info = config.get( 'description', info )
config[ 'type' ] = 'scatterplot'
# optionally bootstrap data from dprov
##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) )
%>
## ----------------------------------------------------------------------------
<!DOCTYPE HTML>
<html>
<head>
@@ -13,14 +34,15 @@
## ----------------------------------------------------------------------------
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery.migrate.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script>
<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script>
<script type="text/javascript" src="/static/scripts/libs/underscore.js"></script>
<script type="text/javascript" src="/static/scripts/libs/backbone/backbone.js"></script>
<script type="text/javascript" src="/static/scripts/libs/handlebars.runtime.js"></script>
<script type="text/javascript" src="/static/scripts/libs/d3.js"></script>
<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script>
<script type="text/javascript" src="/static/scripts/utils/LazyDataLoader.js"></script>
<script type="text/javascript" src="/static/scripts/mvc/base-mvc.js"></script>
<script type="text/javascript" src="/static/scripts/mvc/visualization/visualization-model.js"></script>
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot-edit.js"></script>
</head>
@@ -30,25 +52,20 @@
%if not embedded:
## dataset info: only show if on own page
<div class="chart-header">
<h2>Scatterplot of '${hda.name}'</h2>
<p>${hda.info}</p>
<h2>${title}</h2>
<p>${info}</p>
</div>
<div class="scatterplot-editor"></div>
<script type="text/javascript">
$(function(){
<%
# optionally, bootstrap data from dprov
data = None
##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) )
%>
var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )};
var editor = new ScatterplotConfigEditor({
el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hda.id ),
config : ${h.to_json_string( query_args )},
dataset : ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )}
}).render();
var model = new ScatterplotModel( ${h.to_json_string( config )} ),
hdaJson = ${h.to_json_string( hda_dict )},
editor = new ScatterplotConfigEditor({
el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hdaJson.id ),
model : model,
dataset : hdaJson
}).render();
window.editor = editor;
// uncomment to auto render for development
//$( '.render-button:visible' ).click();
+2 -2
View File
@@ -87,9 +87,9 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
load_genome_index_tools( self.toolbox )
# visualizations registry: associates resources with visualizations, controls how to render
self.visualizations_registry = None
if self.config.visualizations_plugins_directory:
if self.config.visualization_plugins_directory:
self.visualizations_registry = VisualizationsRegistry( self,
directories_setting=self.config.visualizations_plugins_directory,
directories_setting=self.config.visualization_plugins_directory,
template_cache_dir=self.config.template_cache )
# Load security policy.
self.security_agent = self.model.security_agent
+1 -1
View File
@@ -343,7 +343,7 @@ class Configuration( object ):
self.fluent_host = kwargs.get( 'fluent_host', 'localhost' )
self.fluent_port = int( kwargs.get( 'fluent_port', 24224 ) )
# visualization plugin framework
self.visualizations_plugins_directory = kwargs.get( 'visualizations_plugins_directory', None )
self.visualization_plugins_directory = kwargs.get( 'visualization_plugins_directory', None )
@property
def sentry_dsn_public( self ):
+115 -3
View File
@@ -38,6 +38,61 @@ add description element to visualization.
user_pref for ordering/ex/inclusion of particular visualizations
"""
# ------------------------------------------------------------------- misc
#TODO: move to utils?
def getattr_recursive( item, attr_key, *args ):
"""
Allows dot member notation in attribute name when getting an item's attribute.
NOTE: also searches dictionaries
"""
#print '\n\t getattr_recursive:', item, attr_key
using_default = len( args ) >= 1
default = args[0] if using_default else None
#print '\t defaults:', using_default, default
for attr_key in attr_key.split( '.' ):
try:
#print '\t\t attr:', attr_key, 'item:', item
if isinstance( item, dict ):
item = item.__getitem__( attr_key )
else:
item = getattr( item, attr_key )
except ( KeyError, AttributeError ), err:
#print '\t\t error:', err
if using_default:
#print '\t\t\t default:', default
return default
raise
return item
def hasattr_recursive( item, attr_key ):
"""
Allows dot member notation in attribute name when getting an item's attribute.
NOTE: also searches dictionaries
"""
if '.' in attr_key:
attr_key, last_key = attr_key.rsplit( '.', 1 )
item = getattr_recursive( item, attr_key, None )
if item is None:
return False
attr_key = last_key
try:
#print '\t\t attr:', attr_key, 'item:', item
if isinstance( item, dict ):
return item.__contains__( attr_key )
else:
return hasattr( item, attr_key )
except ( KeyError, AttributeError ), err:
return False
return True
# ------------------------------------------------------------------- the registry
class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
"""
@@ -141,21 +196,26 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
`visualization_name` if it's applicable to `target_object` or
`None` if it's not.
"""
#log.debug( 'VisReg.get_visualization: %s, %s', visualization_name, target_object )
visualization = self.plugins.get( visualization_name, None )
if not visualization:
return None
data_sources = visualization.config[ 'data_sources' ]
for data_source in data_sources:
#log.debug( 'data_source: %s', data_source )
# currently a model class is required
model_class = data_source[ 'model_class' ]
#log.debug( '\t model_class: %s', model_class )
if not isinstance( target_object, model_class ):
continue
#log.debug( '\t passed model_class' )
# tests are optional - default is the above class test
tests = data_source[ 'tests' ]
if tests and not self.is_object_applicable( trans, target_object, tests ):
continue
#log.debug( '\t passed tests' )
param_data = data_source[ 'to_params' ]
url = self.get_visualization_url( trans, target_object, visualization_name, param_data )
@@ -199,7 +259,7 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
#NOTE: tests are OR'd, if any test passes - the visualization can be applied
if test_fn( target_object, test_result ):
#log.debug( 'test passed' )
#log.debug( '\t test passed' )
return True
return False
@@ -212,6 +272,8 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
#precondition: the target_object should be usable by the visualization (accrd. to data_sources)
# convert params using vis.data_source.to_params
params = self.get_url_params( trans, target_object, param_data )
#for param in params:
# print param
# we want existing visualizations to work as normal but still be part of the registry (without mod'ing)
# so generate their urls differently
@@ -238,13 +300,20 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
# one or the other is needed
# assign takes precedence (goes last, overwrites)?
#NOTE this is only one level
if target_attr and hasattr( target_object, target_attr ):
params[ to_param_name ] = getattr( target_object, target_attr )
#print 'target_object:', target_object
#print 'target_attr:', target_attr
if target_attr and hasattr_recursive( target_object, target_attr ):
params[ to_param_name ] = getattr_recursive( target_object, target_attr )
#print 'params[ %s ]:' %( to_param_name ), params[ to_param_name ]
if assign:
params[ to_param_name ] = assign
#NOTE!: don't expose raw ids: encode id, _id
if params:
#TODO: double encodes if from config
params = trans.security.encode_dict_ids( params )
return params
@@ -407,6 +476,7 @@ class DataSourceParser( object ):
# these are the allowed classes to associate visualizations with (as strings)
# any model_class element not in this list will throw a parsing ParsingExcepion
ALLOWED_MODEL_CLASSES = [
'Visualization',
'HistoryDatasetAssociation',
'LibraryDatasetDatasetAssociation'
]
@@ -683,6 +753,9 @@ class ResourceParser( object ):
If param is required and not present, raises a `KeyError`.
"""
# first parse any params from any visualizations that were passed
query_params = self.get_params_from_visualization_param( trans, controller, param_config_dict, query_params )
# parse the modifiers first since they modify the params coming next
#TODO: this is all really for hda_ldda - which we could replace with model polymorphism
params_that_modify_other_params = self.parse_parameter_modifiers(
@@ -761,6 +834,44 @@ class ResourceParser( object ):
# (and adding this code to the xml parser)
return self.parse_parameter( trans, param_config, default )
def get_params_from_visualization_param( self, trans, controller, param_config_dict, query_params ):
log.debug( 'parse_visualization_params: %s', param_config_dict )
log.debug( ' : %s', query_params )
# first, find the visualization in the parameters if any
visualization = None
#precondition: assume one visualization
for param_name, param_config in param_config_dict.items():
if param_config.get( 'type' ) == 'visualization':
query_val = query_params.get( param_name )
if query_val is None:
continue
log.debug( 'found visualization param: %s, %s', param_name, query_val )
visualization = self.parse_parameter( trans, controller, param_config, query_val )
if visualization:
break
# if no vis is found, can't get any new params from it: return the original query_params
if not visualization:
log.debug( 'visualization not found' )
return query_params
log.debug( 'found visualization: %s', visualization )
# next, attempt to copy any params from the visualizations config
visualization_config = visualization.latest_revision.config
log.debug( '\t config: %s', visualization_config )
params_from_visualization = {}
for param_name, param_config in param_config_dict.items():
if param_name in visualization_config:
params_from_visualization[ param_name ] = visualization_config[ param_name ]
log.debug( 'params_from_visualization: %s', params_from_visualization )
# layer the query_params over the params from the visualization, returning the combined
params_from_visualization.update( query_params )
return params_from_visualization
#TODO: make parse_visualization separate
def parse_parameter( self, trans, controller, expected_param_data, query_param,
recurse=True, param_modifiers=None ):
"""
@@ -808,6 +919,7 @@ class ResourceParser( object ):
#TODO: subclass here?
elif param_type == 'visualization':
encoded_visualization_id = query_param
log.debug( 'visualization param, id : %s', encoded_visualization_id )
#TODO:?? some fallback if there's no get_X in controller that's passed?
parsed_param = controller.get_visualization( trans, encoded_visualization_id,
check_ownership=False, check_accessible=True )
+13 -2
View File
@@ -248,7 +248,6 @@ var HDAEditView = hdaBase.HDABaseView.extend( LoggableMixin ).extend(
classes : 'dataset-visualize-btn',
faIcon : 'fa-bar-chart-o'
});
$icon.addClass( 'visualize-icon' ); // needed?
// No need for popup menu because there's a single visualization.
if( _.keys( visualizations ).length === 1 ) {
@@ -259,9 +258,21 @@ var HDAEditView = hdaBase.HDABaseView.extend( LoggableMixin ).extend(
} else {
var popup_menu_options = [];
_.each( visualizations, function( linkData ) {
linkData.func = function(){
if( Galaxy.frame.active ){
Galaxy.frame.add({
title : "Visualization",
type : "url",
content : linkData.href
});
return false;
}
return true;
};
popup_menu_options.push( linkData );
return false;
});
var popup = new PopupMenu( $icon, popup_menu_options );
PopupMenu.create( $icon, popup_menu_options );
}
return $icon;
},
+4
View File
@@ -359,6 +359,10 @@ var PopupMenu = Backbone.View.extend({
return 'PopupMenu';
}
});
/** shortcut to new for when you don't need to preserve the ref */
PopupMenu.create = function _create( $button, options ){
return new PopupMenu( $button, options );
};
// -----------------------------------------------------------------------------
// the following class functions are bridges from the original make_popupmenu and make_popup_menus
@@ -0,0 +1,158 @@
//define([
//], function(){
//function saveVis( title, config ){
// var xhr = jQuery.ajax( '/api/visualizations', {
// type : 'POST',
// data : {
// type : 'scatterplot',
// title : title,
// config : JSON.stringify( config )
// }
// });
// xhr.fail( function( xhr, status, message ){
// console.debug( jQuery.makeArray( arguments ) );
// console.error( 'Error saving visualization:', xhr.responseJSON.error );
// });
// return xhr.then( function( saveInfo ){
// return saveInfo;
// });
//}
//==============================================================================
/** @class Model for a saved Galaxy visualization.
*
* @augments Backbone.Model
* @borrows LoggableMixin#logger as #logger
* @borrows LoggableMixin#log as #log
* @constructs
*/
var Visualization = Backbone.Model.extend( LoggableMixin ).extend(
/** @lends Visualization.prototype */{
///** logger used to record this.log messages, commonly set to console */
//// comment this out to suppress log output
//logger : console,
/** default attributes for a model */
defaults : {
//id : null,
//type : null,
//title : null,
//dbkey : null,
//user_id : null,
//slug : null,
//revisions : [],
//latest_revision : null
//(this is unusual in that visualizations don't have configs, revisions do)
//config : {}
},
url : function(){
return galaxy_config.root + 'api/visualizations';
},
/** Set up the model, determine if accessible, bind listeners
* @see Backbone.Model#initialize
*/
initialize : function( data ){
this.log( this + '.initialize', data, this.attributes );
this._setUpListeners();
},
/** set up any event listeners
*/
_setUpListeners : function(){
},
// ........................................................................ config
setConfig: function( config ){
var oldConfig = this.get( 'config' );
// extend if already exists (and clone in order to trigger change)
if( _.isObject( oldConfig ) ){
config = _.extend( _.clone( oldConfig ), config );
}
this.set( 'config', config );
return this;
},
// ........................................................................ common queries
// ........................................................................ ajax
// ........................................................................ misc
/** String representation */
toString : function(){
var idAndTitle = this.get( 'id' ) || '';
if( this.get( 'title' ) ){
idAndTitle += ':' + this.get( 'title' );
}
return 'Visualization(' + idAndTitle + ')';
}
});
//==============================================================================
/** @class Backbone collection of visualization models
*
* @borrows LoggableMixin#logger as #logger
* @borrows LoggableMixin#log as #log
* @constructs
*/
var VisualizationCollection = Backbone.Collection.extend( LoggableMixin ).extend(
/** @lends VisualizationCollection.prototype */{
model : Visualization,
///** logger used to record this.log messages, commonly set to console */
//// comment this out to suppress log output
//logger : console,
url : function(){
return galaxy_config.root + 'api/visualizations';
},
/** Set up.
* @see Backbone.Collection#initialize
*/
initialize : function( models, options ){
options = options || {};
//this._setUpListeners();
},
//_setUpListeners : function(){
//},
// ........................................................................ common queries
// ........................................................................ ajax
// ........................................................................ misc
set : function( models, options ){
// arrrrrrrrrrrrrrrrrg...
// override to get a correct/smarter merge when incoming data is partial (e.g. stupid backbone)
// w/o this partial models from the server will fill in missing data with model defaults
// and overwrite existing data on the client
// see Backbone.Collection.set and _prepareModel
var collection = this;
models = _.map( models, function( model ){
var existing = collection.get( model.id );
if( !existing ){ return model; }
// merge the models _BEFORE_ calling the superclass version
var merged = existing.toJSON();
_.extend( merged, model );
return merged;
});
// now call superclass when the data is filled
Backbone.Collection.prototype.set.call( this, models, options );
},
/** String representation. */
toString : function(){
return ([ 'VisualizationCollection(', [ this.historyId, this.length ].join(), ')' ].join( '' ));
}
});
//==============================================================================
//return {
// Visualization : Visualization,
// VisualizationCollection : VisualizationCollection
//};});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -216,7 +216,7 @@ ${h.templates(
)}
${h.js(
"mvc/visualizations/scatterplotControlForm",
"mvc/visualization/scatterplotControlForm",
)}
<script type="text/javascript">
+1 -1
View File
@@ -177,7 +177,7 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# Visualizations config directory: where to look for individual visualization plugins.
# The path is relative to the Galaxy root dir. To use an absolute path begin the path
# with '/'.
#visualizations_plugins_directory = config/plugins/visualizations
visualization_plugins_directory = config/plugins/visualizations
# Each job is given a unique empty directory as its current working directory.
# This option defines in what parent directory those directories will be