1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/a253d56a0dd6/ Changeset: a253d56a0dd6 User: carlfeberhard Date: 2014-02-07 17:24:32 Summary: Visualizations framework: allow for saving and loading registry visualizations, allow better merging of kwargs, config, saved config, add config utilities to common/templates; Scatterplot: allow saving as visualization, more mvc-based, increase max zoom, refactor and cleanup; Charts, Scatterplot: remove visualization data_source Affected #: 14 files diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/charts/config/charts.xml --- a/config/plugins/visualizations/charts/config/charts.xml +++ b/config/plugins/visualizations/charts/config/charts.xml @@ -7,15 +7,9 @@ <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">charts</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>charts.mako</template></visualization> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/common/templates/config_utils.mako --- /dev/null +++ b/config/plugins/visualizations/common/templates/config_utils.mako @@ -0,0 +1,46 @@ +<%def name="add_config_defaults( defaults )"> +## overwrite default_config_dict with config (if any) then assign to config +<% + for key, default in defaults.items(): + if key not in config or config[ key ] is None: + config[ key ] = default +%> +</%def> + +<%def name="config_form( config_dict )"> +## render form for everything in possible config +</%def> + +<%def name="link_to_change_config( link_contents, new_settings, target='' )"> +<% + # assumes there's a config var + url_for_args = { + 'controller' : 'visualization', + 'action' : 'render', + 'visualization_name' : visualization_name, + 'title' : title + } + url_for_args.update( config ) + url_for_args.update( new_settings ) + if visualization_id: + url_for_args[ 'id' ] = visualization_id +%> + <a href="${h.url_for( **url_for_args )}" target="${target}">${link_contents}</a> +</%def> + +<%def name="save_button( text='Save' )"> +<% + # still a GET + url_for_args = { + 'controller' : 'visualization', + 'action' : 'saved', + 'type' : visualization_name, + 'title' : title, + 'config' : h.to_json_string( config ) + } + # save to existing visualization + if visualization_id: + url_for_args[ 'id' ] = visualization_id +%> + <form action="${h.url_for( **url_for_args )}" method="post"><input type="submit" value="${text}" /></form> +</%def> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/common/templates/visualization_base.mako --- /dev/null +++ b/config/plugins/visualizations/common/templates/visualization_base.mako @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +<% _=n_ %> + +%if embedded: + ${self.as_embedded()} +%else: + ${self.as_page()} +%endif + +## render this inside another page or via ajax +<%def name="as_embedded()"> + ${self.stylesheets()} + ${self.javascripts()} + ${self.get_body()} +</%def> + +## render this as it's own page +<%def name="as_page()"> +<!DOCTYPE HTML> +<html> + <head> + <title>${self.title()}</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + ${self.metas()} + ${self.stylesheets()} + ${self.javascripts()} + </head> + <body> + ${self.get_body()} + </body> +</html> +</%def> +##TODO: late_javascripts + +## Default body +<%def name="get_body()"></%def> + +## Default title +<%def name="title()">${visualization_name}</%def> + +## Additional metas can be defined by templates inheriting from this one. +<%def name="metas()"></%def> + +## Default stylesheets +<%def name="stylesheets()"> +${h.css('base')} +</%def> + +## Default javascripts +<%def name="javascripts()"> +${h.js( + "libs/jquery/jquery", + "libs/jquery/jquery.migrate" +)} + +<script type="text/javascript"> + // console protection + window.console = window.console || { + log : function(){}, + debug : function(){}, + info : function(){}, + warn : function(){}, + error : function(){}, + assert : function(){} + }; +</script> +</%def> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/config/scatterplot.xml --- a/config/plugins/visualizations/scatterplot/config/scatterplot.xml +++ b/config/plugins/visualizations/scatterplot/config/scatterplot.xml @@ -7,15 +7,9 @@ <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> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/src/handlebars/editor.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/editor.handlebars +++ b/config/plugins/visualizations/scatterplot/src/handlebars/editor.handlebars @@ -10,14 +10,20 @@ <a title="Use this tab to change how the chart is drawn" href="#chart-control" data-toggle="tab" >Chart Controls</a></li> - {{! both stats and chart start as disabled since there's no info yet }} + {{! chart starts as disabled since there's no info yet }} <li class="disabled"><a title="This tab will display the chart" href="#chart-display" data-toggle="tab">Chart</a></li> + {{! ... }} + <li class="file-controls"> +<!-- <button class="copy-btn btn btn-default" + title="Save this as a new visualization">Save to new</button>--> + <button class="save-btn btn btn-default">Save</button> + </li></ul> - {{! data form, chart config form, stats, and chart all get their own tab }} + {{! data form, chart config form, chart all get their own tab }} <div class="tab-content"> {{! ---------------------------- tab for data settings form }} <div id="data-control" class="scatterplot-config-control tab-pane active"> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js @@ -1,24 +1,22 @@ /* ============================================================================= todo: - Remove 'chart' names - Make this (the config control/editor) and the ScatterplotView (in scatterplot.js) both - views onto a visualization/revision model + import button(display), func(model) - when user doesn't match Move margins into wid/hi calcs (so final svg dims are w/h) Better separation of AJAX in scatterplot.js (maybe pass in function?) Labels should auto fill in chart control when dataset has column_names Allow column selection/config using the peek output as a base for UI - Allow setting perPage of config - Auto render if given data and/or config + Allow setting perPage in 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 Save as SVG/png Does it work w/ Galaxy.Frame? Embedding Small multiples Drag & Drop other splots onto current (redraw with new axis and differentiate the datasets) + Remove 'chart' names + Somehow link out from info box? Subclass on specific datatypes? (vcf, cuffdiff, etc.) What can be common/useful to other visualizations? @@ -40,138 +38,190 @@ if( !this.model ){ this.model = new Visualization({ type: 'scatterplot' }); } - console.log( this + '.initialize, attributes:', attributes ); + this.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 ); + this.log( 'dataset:', this.dataset ); -//TODO: ScatterplotView -> ScatterplotDisplay, this.plotView -> this.display - this.plotView = new ScatterplotView({ + this.display = new ScatterplotDisplay({ dataset : attributes.dataset, model : this.model -//TODO: if data }); }, // ------------------------------------------------------------------------- CONTROLS RENDERING render : function(){ //console.log( this + '.render' ); - // render the tab controls, areas and loading indicator - this.$el.append( ScatterplotConfigEditor.templates.mainLayout({})); + this.$el.empty().append( ScatterplotConfigEditor.templates.mainLayout({})); + if( this.model.id ){ + this.$el.find( '.copy-btn' ).show(); + this.$el.find( '.save-btn' ).text( 'Update saved' ); + } + this.$el.find( '[title]' ).tooltip(); // render the tab content - this.$el.find( '#data-control' ).append( this._render_dataControl() ); - this._render_chartControls( this.$el.find( '#chart-control' ) ); - //this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' ); + this._render_dataControl(); + this._render_chartControls(); this._render_chartDisplay(); - //TODO: auto render if given both x, y column choices in query for page + // set up behaviours - // set up behaviours - this.$el.find( '[title]' ).tooltip(); - - // uncomment any of the following to have that tab show on initial load (for testing) - //this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + // auto render if given both x, y column choices + var config = this.model.get( 'config' ); + if( this.model.id && _.isFinite( config.xColumn ) && _.isFinite( config.yColumn ) ){ + this.renderChart(); + } return this; }, - _render_dataControl : function(){ - // controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) - var dataset = this.dataset; + /** controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) */ + _render_dataControl : function( $where ){ + //TODO: better handling of missing column names, column types + $where = $where || this.$el; + var editor = this, + dataset = this.dataset, + column_names = dataset.metadata_column_names || [], + config = this.model.get( 'config' ); //console.log( 'metadata_column_types:', this.dataset.metadata_column_types ); //console.log( 'metadata_column_names:', this.dataset.metadata_column_names ); - var allColumns = _.map( dataset.metadata_column_types, function( type, i ){ - var column = { index: i, type: type, name: ( 'column ' + ( i + 1 ) ) }; - if( dataset.metadata_column_names && dataset.metadata_column_names[ i ] ){ - column.name = dataset.metadata_column_names[ i ]; - } - return column; - }); - var numericColumns = _.filter( allColumns, function( column, i ){ - return ( ( column.type === 'int' ) || ( column.type === 'float' ) ); - }); - if( numericColumns < 2 ){ +//TODO: to peek based control + var numericColumns = [], + allColumns = _.map( dataset.metadata_column_types, function( type, i ){ + // save column data for select rendering, adding metadata name if available in dataset + var column = { index: i, type: type, name: ( column_names[ i ] || ( 'column ' + ( i + 1 )) ) }; + // also add column to numerics if numeric type + if( ( column.type === 'int' ) || ( column.type === 'float' ) ){ + numericColumns.push( column ); + } + return column; + }); + if( numericColumns.length < 2 ){ numericColumns = allColumns; } //console.log( 'allColumns:', allColumns ); //console.log( 'numericColumns:', numericColumns ); // render the html - var $dataControl = this.$el.find( '.tab-pane#data-control' ); + var $dataControl = $where.find( '.tab-pane#data-control' ); $dataControl.html( ScatterplotConfigEditor.templates.dataControl({ allColumns : allColumns, numericColumns : numericColumns })); - // preset to column selectors if they were passed in the config in the query string - $dataControl.find( '[name="xColumn"]' ).val( this.plotView.config.xColumn || numericColumns[0].index ); - $dataControl.find( '[name="yColumn"]' ).val( this.plotView.config.yColumn || numericColumns[1].index ); - if( this.plotView.config.idColumn !== undefined ){ +//TODO: column selection boilerplate + // preset to column selectors if they were passed in the config in the query string; set up events + var newConfig = { + xColumn : ( _.isFinite( config.xColumn ) )? ( config.xColumn ): ( numericColumns[0].index ), + yColumn : ( _.isFinite( config.yColumn ) )? ( config.yColumn ): ( numericColumns[1].index ), + idColumn : allColumns[0].index + }; + // use an idColumn from the config or attempt to get one different from the numeric + if( _.isFinite( config.idColumn ) ){ + newConfig.idColumn = config.idColumn; + } else { + if( allColumns.length > 2 ){ + var uniqueCol = _.find( allColumns, function( column, i ){ + return i !== newConfig.xColumn && i !== newConfig.yColumn; + }); + newConfig.idColumn = uniqueCol.index; + } + } + config = this.model.set( 'config', newConfig, { silent: true }).get( 'config' ); + + $dataControl.find( '[name="xColumn"]' ).val( config.xColumn ).on( 'change', function(){ + editor.model.set( 'config', { xColumn: Number( $( this ).val() ) }); + }); + $dataControl.find( '[name="yColumn"]' ).val( config.yColumn ).on( 'change', function(){ + editor.model.set( 'config', { yColumn: Number( $( this ).val() ) }); + }); + $dataControl.find( 'select[name="idColumn"]' ).val( config.idColumn ).on( 'change', function(){ + editor.model.set( 'config', { idColumn: Number( $( this ).val() ) }); + }); + if( config.idColumn !== undefined ){ $dataControl.find( '#include-id-checkbox' ).prop( 'checked', true ).trigger( 'change' ); - $dataControl.find( 'select[name="idColumn"]' ).val( this.plotView.config.idColumn ); } + $dataControl.find( '[title]' ).tooltip(); return $dataControl; }, - _render_chartControls : function( $chartControls ){ - // tab content to control how the chart is rendered (data glyph size, chart size, etc.) - $chartControls.html( ScatterplotConfigEditor.templates.chartControl( this.plotView.config ) ); + /** tab content to control how the chart is rendered (data glyph size, chart size, etc.) */ + _render_chartControls : function( $where ){ + $where = $where || this.$el; + var editor = this, + config = this.model.get( 'config' ), + $chartControls = $where.find( '#chart-control' ); + + // ---- skeleton/form for controls + $chartControls.html( ScatterplotConfigEditor.templates.chartControl( config ) ); //console.debug( '$chartControl:', $chartControls ); - // set up behaviours, js on sliders - //console.debug( 'numeric sliders:', $chartControls.find( '.numeric-slider-input' ) ); - // what to do when the slider changes: update display and update chartConfig - var view = this, - // limits for controls (by control/chartConfig id) - //TODO: move into TwoVarScatterplot - controlRanges = { + // ---- slider controls + // limits for controls (by control/chartConfig id) + //TODO: as class attribute + var controlRanges = { 'datapointSize' : { min: 2, max: 10, step: 1 }, 'width' : { min: 200, max: 800, step: 20 }, 'height' : { min: 200, max: 800, step: 20 } }; function onSliderChange(){ - var $this = $( this ); - $this.siblings( '.slider-output' ).text( $this.slider( 'value' ) ); + // set the model config when changed and update the slider output text + var $this = $( this ), + //note: returns a number nicely enough + newVal = $this.slider( 'value' ); + // parent of slide event target has html5 attr data-config-key + editor.model.set( 'config', _.object([[ $this.parent().data( 'config-key' ), newVal ]]) ); + $this.siblings( '.slider-output' ).text( newVal ); } + + //console.debug( 'numeric sliders:', $chartControls.find( '.numeric-slider-input' ) ); $chartControls.find( '.numeric-slider-input' ).each( function(){ + // set up the slider with control ranges, change event; set output text to initial value var $this = $( this ), configKey = $this.attr( 'data-config-key' ), sliderSettings = _.extend( controlRanges[ configKey ], { - value : view.plotView.config[ configKey ], + value : config[ configKey ], change : onSliderChange, slide : onSliderChange }); //console.debug( configKey + ' slider settings:', sliderSettings ); $this.find( '.slider' ).slider( sliderSettings ); + $this.children( '.slider-output' ).text( config[ configKey ] ); }); -//TODO: to more common area (like render)? + + // ---- axes labels + var columnNames = this.dataset.metadata_column_names || []; + var xLabel = config.xLabel || columnNames[ config.xColumn ] || 'X'; + var yLabel = config.yLabel || columnNames[ config.yColumn ] || 'Y'; // set label inputs to current x, y metadata_column_names (if any) - if( this.dataset.metadata_column_names ){ - //var colNames = this.dataset.metadata_column_names; - //$chartControls.find( 'input[name="X-axis-label"]' ).val( colNames ); - //$chartControls.find( 'input[name="Y-axis-label"]' ).val( colNames ); -//TODO: on change of x, y data controls - } + $chartControls.find( 'input[name="X-axis-label"]' ).val( xLabel ) + .on( 'change', function(){ + editor.model.set( 'config', { xLabel: $( this ).val() }); + }); + $chartControls.find( 'input[name="Y-axis-label"]' ).val( yLabel ) + .on( 'change', function(){ + editor.model.set( 'config', { yLabel: $( this ).val() }); + }); //console.debug( '$chartControls:', $chartControls ); + $chartControls.find( '[title]' ).tooltip(); return $chartControls; }, - _render_chartDisplay : function(){ - // render the tab content where the chart is displayed (but not the chart itself) - var $chartDisplay = this.$el.find( '.tab-pane#chart-display' ); - this.plotView.setElement( $chartDisplay ); - this.plotView.render(); + /** render the tab content where the chart is displayed (but not the chart itself) */ + _render_chartDisplay : function( $where ){ + $where = $where || this.$el; + var $chartDisplay = $where.find( '.tab-pane#chart-display' ); + this.display.setElement( $chartDisplay ); + this.display.render(); + + $chartDisplay.find( '[title]' ).tooltip(); return $chartDisplay; }, @@ -179,7 +229,22 @@ events : { 'change #include-id-checkbox' : 'toggleThirdColumnSelector', 'click #data-control .render-button' : 'renderChart', - 'click #chart-control .render-button' : 'renderChart' + 'click #chart-control .render-button' : 'renderChart', + 'click .save-btn' : 'saveVisualization', + //'click .copy-btn' : function(e){ this.model.save(); } + }, + + saveVisualization : function(){ + var editor = this; + this.model.save() + .fail( function( xhr, status, message ){ + console.error( xhr, status, message ); + editor.trigger( 'save:error', view ); + alert( 'Error loading data:\n' + xhr.responseText ); + }) + .then( function(){ + editor.render(); + }); }, toggleThirdColumnSelector : function(){ @@ -192,42 +257,9 @@ //console.log( this + '.renderChart' ); // fetch the data, (re-)render the chart this.$el.find( '.nav li.disabled' ).removeClass( 'disabled' ); - this.updateConfigWithDataSettings(); - this.updateConfigWithChartSettings(); this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); - this.plotView.fetchData(); - //console.debug( this.plotView.$el ); - }, - - // ------------------------------------------------------------------------- GET DATA/CHART SETTINGS - updateConfigWithDataSettings : function(){ - // parse the column values for both indeces (for the data fetch) and names (for the chart) - var $dataControls = this.$el.find( '#data-control' ); - var settings = { - xColumn : Number( $dataControls.find( '[name="xColumn"]' ).val() ), - yColumn : Number( $dataControls.find( '[name="yColumn"]' ).val() ) - }; - if( $dataControls.find( '#include-id-checkbox' ).prop( 'checked' ) ){ - settings.idColumn = $dataControls.find( '[name="idColumn"]' ).val(); - } - //console.log( '\t data settings:', settings ); - return _.extend( this.plotView.config, settings ); - }, - - updateConfigWithChartSettings : function(){ - // gets the user-selected chartConfig from the chart settings panel - var plotView = this.plotView, - $chartControls = this.$el.find( '#chart-control' ); - // use a loop of config keys to get the form values for these sliders - [ 'datapointSize', 'width', 'height' ].forEach( function( v, i ){ - plotView.config[ v ] = $chartControls.find( '.numeric-slider-input[data-config-key="' + v + '"]' ) - .find( '.slider' ).slider( 'value' ); - }); - // update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName - plotView.config.x.label = $chartControls.find( 'input[name="X-axis-label"]' ).val(); - plotView.config.y.label = $chartControls.find( 'input[name="Y-axis-label"]' ).val(); - //console.log( '\t chartSettings:', settings ); - return plotView.config; + this.display.fetchData(); + //console.debug( this.display.$el ); }, toString : function(){ diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/src/scatterplot-display.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -6,242 +6,242 @@ * computing and displaying data stats * controls for pagination of data (if needed) */ -var ScatterplotView = Backbone.View.extend({ - //TODO: should be a view on visualization(revision) model +var ScatterplotDisplay = Backbone.View.extend({ - defaults : { - metadata : { - dataLines : undefined - }, - - pagination : { - currPage : 0, - perPage : 3000 - }, - - width : 400, - height : 400, - - margin : { - top : 16, - right : 16, - bottom : 40, - left : 54 - }, - - x : { - ticks : 10, - label : 'X' - }, - y : { - ticks : 10, - label : 'Y' - }, - - datapointSize : 4, - animDuration : 500 + initialize : function( attributes ){ + this.data = null, + this.dataset = attributes.dataset; + this.calcNumPages(); }, - initialize : function( attributes ){ - this.config = _.extend( _.clone( this.defaults ), attributes.config || {}); - this.dataset = attributes.dataset; - //console.debug( this + '.config:', this.config ); - }, - - updateConfig : function( newConfig ){ - //console.log( this + '.updateConfig:', newConfig ); - this.config = this.config || {}; - //TODO: validate here - _.extend( this.config, newConfig ); - //TODO: implement rerender flag + calcNumPages : function(){ + var config = this.model.get( 'config' ); + this.lineCount = this.dataset.metadata_data_lines, + this.numPages = ( this.lineCount )?( Math.ceil( this.lineCount / config.pagination.perPage ) ):( undefined ); + if( !this.lineCount || this.numPages === undefined ){ + console.warn( 'no data total found' ); + } }, fetchData : function(){ //TODO: doesn't work bc it's rendered in render()... this.showLoadingIndicator( 'getting data' ); //console.debug( 'currPage', this.config.pagination.currPage ); - var view = this; + var view = this, + config = this.model.get( 'config' ), //TODO: very tied to datasets - should be generalized eventually xhr = jQuery.getJSON( '/api/datasets/' + this.dataset.id, { data_type : 'raw_data', provider : 'dataset-column', - limit : this.config.pagination.perPage, - offset : ( this.config.pagination.currPage * this.config.pagination.perPage ) + limit : config.pagination.perPage, + offset : ( config.pagination.currPage * config.pagination.perPage ) }); xhr.done( function( data ){ - view.renderData( data.data ); + view.data = data.data; + view.trigger( 'data:fetched', view ); + view.renderData(); }); xhr.fail( function( xhr, status, message ){ + console.error( xhr, status, message ); + view.trigger( 'data:error', view ); alert( 'Error loading data:\n' + xhr.responseText ); - console.error( xhr, status, message ); - }); - xhr.always( function(){ - view.hideLoadingIndicator(); }); return xhr; }, - render : function( data ){ - this.$el.addClass( 'scatterplot-display' ).html([ - '<div class="controls clear"></div>', + showLoadingIndicator : function(){ + // display the loading indicator over the tab panels if hidden, update message (if passed) + this.$el.find( '.scatterplot-data-info' ).html([ '<div class="loading-indicator">', '<span class="fa fa-spinner fa-spin"></span>', - '<span class="loading-indicator-message"></span>', + '<span class="loading-indicator-message">loading...</span>', + '</div>' + ].join( '' )); + }, + + template : function(){ + var html = [ + '<div class="controls clear">', + '<div class="left">', + '</div>', + '<div class="right">', + '<p class="scatterplot-data-info"></p>', + '<button class="stats-toggle-btn">Stats</button>', + '<button class="rerender-btn">Redraw</button>', + '</div>', '</div>', '<svg/>', //TODO: id '<div class="stats-display"></div>' - ].join( '' )); - this.$el.children().hide(); + ].join( '' ); + return html; + }, - if( data ){ - this.renderData( data ); + render : function(){ + this.$el.addClass( 'scatterplot-display' ).html( this.template() ); + if( this.data ){ + this.renderData(); } return this; }, - showLoadingIndicator : function( message, speed ){ - // display the loading indicator over the tab panels if hidden, update message (if passed) -//TODO: move loading indicator into data-info-text - message = message || ''; - speed = speed || 'fast'; - var $indicator = this.$el.find( '.loading-indicator' ); - - if( message ){ $indicator.find( '.loading-indicator-message' ).text( message ); } - if( !$indicator.is( ':visible' ) ){ - this.toggleStats( false ); - $indicator.css({ left: ( this.config.width / 2 ), top: this.config.height / 2 }).show(); - } + renderData : function(){ + this.renderLeftControls(); + this.renderRightControls(); + this.renderPlot( this.data ); + this.getStats(); }, - hideLoadingIndicator : function( speed ){ - speed = speed || 'fast'; - this.$el.find( '.loading-indicator' ).hide(); + renderLeftControls : function(){ + if( this.lineCount ){ + this.$el.find( '.controls .left' ).empty().append( this.renderPagination() ); + } else { + this.$el.find( '.controls .left' ).empty().append( this.renderPrevNext() ); + } + return this; }, - renderData : function( data ){ - this.$el.find( '.controls' ).empty().append( this.renderControls( data ) ).show(); - this.renderPlot( data ); - this.getStats( data ); + renderRightControls : function(){ + var view = this; + this.setLineInfo( this.data ); + // clear prev. handlers due to closure around data + this.$el.find( '.stats-toggle-btn' ) + .off().click( function(){ + view.toggleStats(); + }); + this.$el.find( '.rerender-btn' ) + .off().click( function(){ + view.resetZoom(); + view.renderPlot( this.data ); + }); }, - renderControls : function( data ){ - var view = this; - var $left = $( '<div class="left"></div>' ), - $right = $( '<div class="right"></div>' ); - - $left.append([ - this.renderPrevNext( data ), - this.renderPagination( data ) - ]); - $right.append([ - this.renderLineInfo( data ), - $( '<button>Stats</button>' ).addClass( 'stats-toggle-btn' ) - .click( function(){ - view.toggleStats(); - }), - $( '<button>Redraw</button>' ).addClass( 'rerender-btn' ) - .click( function(){ - view.renderPlot( data ); - }) - ]); - return [ $left, $right ]; + /** render and show the d3 plot into the svg node of the view */ + renderPlot : function(){ + var view = this, + $svg = this.$el.find( 'svg' ); + // turn off stats, clear previous svg, and make it visible + this.toggleStats( false ); + $svg.off().empty().show() + // set up listeners for events from plot + .on( 'zoom.scatterplot', function( ev, zoom ){ + //TODO: possibly throttle this + //console.debug( 'zoom.scatterplot', zoom.scale, zoom.translate ); + view.model.set( 'config', zoom ); + }); + //TODO: may not be necessary to off/on this more than the initial on + // call the sep. d3 function to generate the plot + scatterplot( $svg.get( 0 ), this.model.get( 'config' ), this.data ); }, - renderLineInfo : function( data ){ - var totalLines = this.dataset.metadata_data_lines || 'an unknown number of', - lineStart = ( this.config.pagination.currPage * this.config.pagination.perPage ), - lineEnd = lineStart + data.length; - return $( '<p/>' ).addClass( 'scatterplot-data-info' ) - .text([ 'Displaying lines', lineStart + 1, 'to', lineEnd, 'of', totalLines, 'lines' ].join( ' ' )); + setLineInfo : function( data, contents ){ + if( data ){ + var config = this.model.get( 'config' ), + totalLines = this.lineCount || 'an unknown total', + lineStart = config.pagination.currPage * config.pagination.perPage, + lineEnd = lineStart + data.length; + this.$el.find( '.controls p.scatterplot-data-info' ) + .text([ lineStart + 1, 'to', lineEnd, 'of', totalLines ].join( ' ' )); + } else { + this.$el.find( '.controls p.scatterplot-data-info' ).html( contents || '' ); + } + + return this; }, - renderPrevNext : function( data ){ - // this is cra-zazy - if( !data - || ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; } + resetZoom : function( scale, translate ){ + scale = ( scale !== undefined )?( scale ):( 1 ); + translate = ( translate !== undefined )?( translate ):( [ 0, 0 ] ); + this.model.set( 'config', { scale: scale, translate: translate } ); + return this; + }, - function makePage$Li( text ){ - return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' )); - } -//TODO: cache numPages/numLines in config + // ------------------------------------------------------------------------ data pagination +//TODO: to pagination control + goToPage : function( page ){ + var pagination = this.model.get( 'config' ).pagination; + //console.debug( 'goToPage', page, pagination, this.numPages ); + if( page <= 0 ){ page = 0; } + if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; } + if( page === pagination.currPage ){ return this; } + + //console.debug( '\t going to page ' + page ) + pagination.currPage = page; + this.model.set( 'config', { pagination: pagination }); + this.resetZoom(); + this.fetchData(); + return this; + }, + + nextPage : function(){ + var currPage = this.model.get( 'config' ).pagination.currPage; + return this.goToPage( currPage + 1 ); + }, + + prevPage : function(){ + var currPage = this.model.get( 'config' ).pagination.currPage; + return this.goToPage( currPage - 1 ); + }, + + /** render previous and next pagination buttons */ + renderPrevNext : function(){ + var config = this.model.get( 'config' ); + // if there's no data or there's less than one page of data - return null + if( !this.data ){ return null; } + if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; } + var view = this, - dataLines = this.dataset.metadata_data_lines, - numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined ); - //console.debug( 'data:', this.dataset.metadata_data_lines, 'numPages:', numPages ); + $prev = $( '<li><a href="javascript:void(0);">Prev</a></li>' ) + .click( function(){ view.prevPage(); }), + $next = $( '<li><a href="javascript:void(0);">Next</a></li>' ) + .click( function(){ view.nextPage(); }); - // prev next buttons - var $prev = makePage$Li( 'Prev' ).click( function(){ - if( view.config.pagination.currPage > 0 ){ - view.config.pagination.currPage -= 1; - view.fetchData(); - } - }), - $next = makePage$Li( 'Next' ).click( function(){ - if( !numPages || view.config.pagination.currPage < ( numPages - 1 ) ){ - view.config.pagination.currPage += 1; - view.fetchData(); - } - }), - $prevNextList = $( '<ul/>' ).addClass( 'pagination data-prev-next' ) - .append([ $prev, $next ]); - - if( view.config.pagination.currPage === 0 ){ + // disable if it either end + if( config.pagination.currPage === 0 ){ $prev.addClass( 'disabled' ); } - if( numPages && view.config.pagination.currPage === ( numPages - 1 ) ){ + if( this.numPages && config.pagination.currPage === ( this.numPages - 1 ) ){ $next.addClass( 'disabled' ); } - return $prevNextList; + return $( '<ul/>' ).addClass( 'pagination data-prev-next' ).append([ $prev, $next ]); }, - renderPagination : function( data ){ - // this is cra-zazy - if( !data - || ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; } + /** render page links for each possible page (if we can) */ + renderPagination : function(){ + var config = this.model.get( 'config' ); + // if there's no data, no page count, or there's less than one page of data - return null + if( !this.data ){ return null; } + if( !this.numPages ){ return null; } + if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; } - function makePage$Li( text ){ - return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' )); - } -//TODO: cache numPages/numLines in config var view = this, - dataLines = this.dataset.metadata_data_lines, - numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined ); - //console.debug( 'data:', this.dataset.metadata_data_lines, 'numPages:', numPages ); + $pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' ); + pageNumClick = function( ev ){ + view.goToPage( $( this ).data( 'page' ) ); + }; - // page numbers (as separate control) - //var $paginationContainer = $( '<div/>' ).addClass( 'pagination-container' ), - var $pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' ); - function pageNumClick( ev ){ - view.config.pagination.currPage = $( this ).data( 'page' ); - view.fetchData(); - } - for( var i=0; i<numPages; i+=1 ){ - // add page data for later event handling - var $pageLi = makePage$Li( i + 1 ).attr( 'data-page', i ).click( pageNumClick ); - if( i === this.config.pagination.currPage ){ + for( var i=0; i<this.numPages; i+=1 ){ + // add html5 data tag 'page' for later click event handler use + var $pageLi = $([ '<li><a href="javascript:void(0);">', i + 1, '</a></li>' ].join( '' )) + .attr( 'data-page', i ).click( pageNumClick ); + // highlight the current page + if( i === config.pagination.currPage ){ $pageLi.addClass( 'active' ); } $pagesList.append( $pageLi ); } - // placing the pages list in an extra container allows us to set a max-width and scroll if overflow - //$paginationContainer.append( $pagesList ); - //return $paginationContainer; return $pagesList; }, - renderPlot : function( data ){ - this.toggleStats( false ); - var $svg = this.$el.find( 'svg' ); - $svg.off().empty().show(); - scatterplot( $svg.get( 0 ), this.config, data ); - }, - - getStats : function( data ){ - var view = this; + // ------------------------------------------------------------------------ statistics display + /** create a webworker to calc stats for data given */ + getStats : function(){ + if( !this.data ){ return; } + var view = this, + config = this.model.get( 'config' ), meanWorker = new Worker( '/plugins/visualizations/scatterplot/static/worker-stats.js' ); meanWorker.postMessage({ - data : data, - keys : [ this.config.xColumn, this.config.yColumn ] + data : this.data, + keys : [ config.xColumn, config.yColumn ] }); meanWorker.onerror = function( event ){ meanWorker.terminate(); @@ -254,9 +254,9 @@ renderStats : function( stats, error ){ //console.debug( 'renderStats:', stats, error ); //console.debug( JSON.stringify( stats, null, ' ' ) ); - var $statsTable = this.$el.find( '.stats-display' ); - - var xLabel = this.config.x.label, yLabel = this.config.y.label, + var config = this.model.get( 'config' ), + $statsTable = this.$el.find( '.stats-display' ), + xLabel = config.x.label, yLabel = config.y.label, $table = $( '<table/>' ).addClass( 'table' ) .append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' )) .append( _.map( stats, function( stat, key ){ diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/src/scatterplot.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js @@ -46,8 +46,9 @@ var zoom = d3.behavior.zoom() .x( interpolaterFns.x ) .y( interpolaterFns.y ) - .scaleExtent([ 1, 10 ]); -//TODO: you can prog. set the zoom and pan with zoom.scale( val ) and zoom.translate([ x, y ])... + .scaleExtent([ 1, 30 ]) + .scale( config.scale || 1 ) + .translate( config.translate || [ 0, 0 ] ); //console.debug( renderTo ); var svg = d3.select( renderTo ) @@ -99,7 +100,7 @@ //console.log( 'axis.y.g:', axis.y.g ); // ................................ axis labels - var padding = 4; + var padding = 6; // x-axis label axis.x.label = svg.append( 'text' ) .attr( 'class', 'axis-label' ) @@ -176,18 +177,17 @@ .enter().append( 'svg:circle' ) .classed( "glyph", true ) .attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); }) - // give them a 'entry' position and style - .attr( "cy", config.height ) + .attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); }) .attr( "r", 0 ); // for all EXISTING glyphs and those that need to be added: transition anim to final state datapoints.transition().duration( config.animDuration ) - .attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); }) .attr( "r", config.datapointSize ); //console.log( 'datapoints:', datapoints ); function _redrawDatapointsClipped(){ return datapoints + //TODO: interpolates twice .attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); }) .attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); }) .style( 'display', 'block' ) @@ -200,29 +200,34 @@ return false; }).style( 'display', 'none' ); } + _redrawDatapointsClipped(); // .................................................................... behaviors function zoomed( scale, translateX, translateY ){ - //console.debug( 'zoom', this, scale, translateX, translateY, arguments ); + //console.debug( 'zoom', this, zoom.scale(), zoom.translate() ); + // re-render axis, grid, and datapoints + $( '.chart-info-box' ).remove(); axis.redraw(); _redrawDatapointsClipped(); grid = renderGrid(); - $( '.chart-info-box' ).remove(); - $( svg.node() ).trigger( 'zoom.scatterplot', [] ); + + $( svg.node() ).trigger( 'zoom.scatterplot', { + scale : zoom.scale(), + translate : zoom.translate() + }); } //TODO: programmatically set zoom/pan and save in config //TODO: set pan/zoom limits zoom.on( "zoom", zoomed ); - function infoBox( top, left, d ){ // create an abs pos. element containing datapoint data (d) near the point (top, left) // with added padding to clear the mouse pointer left += 8; return $([ '<div class="chart-info-box" style="position: absolute">', - (( config.idColumn )?( '<div>' + d[ config.idColumn ] + '</div>' ):( '' )), + (( config.idColumn !== undefined )?( '<div>' + d[ config.idColumn ] + '</div>' ):( '' )), '<div>', getX( d ), '</div>', '<div>', getY( d ), '</div>', '</div>' diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/static/scatterplot-edit.js --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -1,1 +1,1 @@ -function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){q.redraw(),e(),s=d(),$(".chart-info-box").remove(),$(o.node()).trigger("zoom.scatterplot",[])}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,10]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.x.ticks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.y.ticks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=4;q.x.label=o.append("text").attr("class","axis-label").text(b.x.label).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("class","axis-label").text(b.y.label).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",b.height).attr("r",0);t.transition().duration(b.animDuration).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",b.datapointSize),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="'+i((f=b.x,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="'+i((f=b.y,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n <option value="',(d=c.index)?d=d.call(a,{hash:{},data:b}):(d=a.index,d=typeof d===j?d.apply(a):d),e+=k(d)+'">',(d=c.name)?d=d.call(a,{hash:{},data:b}):(d=a.name,d=typeof d===j?d.apply(a):d),e+=k(d)+"</option>\n "}function g(){return'checked="true"'}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var h,i="",j="function",k=this.escapeExpression,l=this;return i+='<p class="help-text">\n Use the following controls to change the data used by the chart.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n\n<div class="column-select">\n <label>Data column for X: </label>\n <select name="xColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n<div class="column-select">\n <label>Data column for Y: </label>\n <select name="yColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="include-id">\n <label for="include-id-checkbox">Include a third column as data point IDs?</label>\n <input type="checkbox" name="include-id" id="include-id-checkbox" />\n <p class="help-text-small">\n These will be displayed (along with the x and y values) when you hover over\n a data point.\n </p>\n</div>\n<div class="column-select" style="display: none">\n <label for="ID-select">Data column for IDs: </label>\n <select name="idColumn">\n ',h=c.each.call(b,b.allColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="first-line-header" style="display: none;">\n <p>Possible headers: ',(h=c.possibleHeaders)?h=h.call(b,{hash:{},data:e}):(h=b.possibleHeaders,h=typeof h===j?h.apply(b):h),i+=k(h)+'\n </p>\n <label for="first-line-header-checkbox">Use the above as column headers?</label>\n <input type="checkbox" name="include-id" id="first-line-header-checkbox"\n ',h=c["if"].call(b,b.usePossibleHeaders,{hash:{},inverse:l.noop,fn:l.program(3,g,e),data:e}),(h||0===h)&&(i+=h),i+='/>\n <p class="help-text-small">\n It looks like Galaxy couldn\'t get proper column headers for this data.\n Would you like to use the column headers above as column names to select columns?\n </p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),console.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,console.log("dataset:",this.dataset),this.plotView=new ScatterplotView({dataset:a.dataset,model:this.model})},render:function(){return this.$el.append(ScatterplotConfigEditor.templates.mainLayout({})),this.$el.find("#data-control").append(this._render_dataControl()),this._render_chartControls(this.$el.find("#chart-control")),this._render_chartDisplay(),this.$el.find("[title]").tooltip(),this},_render_dataControl:function(){var a=this.dataset,b=_.map(a.metadata_column_types,function(b,c){var d={index:c,type:b,name:"column "+(c+1)};return a.metadata_column_names&&a.metadata_column_names[c]&&(d.name=a.metadata_column_names[c]),d}),c=_.filter(b,function(a){return"int"===a.type||"float"===a.type});2>c&&(c=b);var d=this.$el.find(".tab-pane#data-control");return d.html(ScatterplotConfigEditor.templates.dataControl({allColumns:b,numericColumns:c})),d.find('[name="xColumn"]').val(this.plotView.config.xColumn||c[0].index),d.find('[name="yColumn"]').val(this.plotView.config.yColumn||c[1].index),void 0!==this.plotView.config.idColumn&&(d.find("#include-id-checkbox").prop("checked",!0).trigger("change"),d.find('select[name="idColumn"]').val(this.plotView.config.idColumn)),d},_render_chartControls:function(a){function b(){var a=$(this);a.siblings(".slider-output").text(a.slider("value"))}a.html(ScatterplotConfigEditor.templates.chartControl(this.plotView.config));var c=this,d={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};return a.find(".numeric-slider-input").each(function(){var a=$(this),e=a.attr("data-config-key"),f=_.extend(d[e],{value:c.plotView.config[e],change:b,slide:b});a.find(".slider").slider(f)}),this.dataset.metadata_column_names,a},_render_chartDisplay:function(){var a=this.$el.find(".tab-pane#chart-display");return this.plotView.setElement(a),this.plotView.render(),a},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart"},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.updateConfigWithDataSettings(),this.updateConfigWithChartSettings(),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.plotView.fetchData()},updateConfigWithDataSettings:function(){var a=this.$el.find("#data-control"),b={xColumn:Number(a.find('[name="xColumn"]').val()),yColumn:Number(a.find('[name="yColumn"]').val())};return a.find("#include-id-checkbox").prop("checked")&&(b.idColumn=a.find('[name="idColumn"]').val()),_.extend(this.plotView.config,b)},updateConfigWithChartSettings:function(){var a=this.plotView,b=this.$el.find("#chart-control");return["datapointSize","width","height"].forEach(function(c){a.config[c]=b.find('.numeric-slider-input[data-config-key="'+c+'"]').find(".slider").slider("value")}),a.config.x.label=b.find('input[name="X-axis-label"]').val(),a.config.y.label=b.find('input[name="Y-axis-label"]').val(),a.config},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotView=Backbone.View.extend({defaults:{metadata:{dataLines:void 0},pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500},initialize:function(a){this.config=_.extend(_.clone(this.defaults),a.config||{}),this.dataset=a.dataset},updateConfig:function(a){this.config=this.config||{},_.extend(this.config,a)},fetchData:function(){this.showLoadingIndicator("getting data");var a=this;return xhr=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:this.config.pagination.perPage,offset:this.config.pagination.currPage*this.config.pagination.perPage}),xhr.done(function(b){a.renderData(b.data)}),xhr.fail(function(a,b,c){alert("Error loading data:\n"+a.responseText),console.error(a,b,c)}),xhr.always(function(){a.hideLoadingIndicator()}),xhr},render:function(a){return this.$el.addClass("scatterplot-display").html(['<div class="controls clear"></div>','<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message"></span>',"</div>","<svg/>",'<div class="stats-display"></div>'].join("")),this.$el.children().hide(),a&&this.renderData(a),this},showLoadingIndicator:function(a,b){a=a||"",b=b||"fast";var c=this.$el.find(".loading-indicator");a&&c.find(".loading-indicator-message").text(a),c.is(":visible")||(this.toggleStats(!1),c.css({left:this.config.width/2,top:this.config.height/2}).show())},hideLoadingIndicator:function(a){a=a||"fast",this.$el.find(".loading-indicator").hide()},renderData:function(a){this.$el.find(".controls").empty().append(this.renderControls(a)).show(),this.renderPlot(a),this.getStats(a)},renderControls:function(a){var b=this,c=$('<div class="left"></div>'),d=$('<div class="right"></div>');return c.append([this.renderPrevNext(a),this.renderPagination(a)]),d.append([this.renderLineInfo(a),$("<button>Stats</button>").addClass("stats-toggle-btn").click(function(){b.toggleStats()}),$("<button>Redraw</button>").addClass("rerender-btn").click(function(){b.renderPlot(a)})]),[c,d]},renderLineInfo:function(a){var b=this.dataset.metadata_data_lines||"an unknown number of",c=this.config.pagination.currPage*this.config.pagination.perPage,d=c+a.length;return $("<p/>").addClass("scatterplot-data-info").text(["Displaying lines",c+1,"to",d,"of",b,"lines"].join(" "))},renderPrevNext:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;var c=this,d=this.dataset.metadata_data_lines,e=d?Math.ceil(d/this.config.pagination.perPage):void 0,f=b("Prev").click(function(){c.config.pagination.currPage>0&&(c.config.pagination.currPage-=1,c.fetchData())}),g=b("Next").click(function(){(!e||c.config.pagination.currPage<e-1)&&(c.config.pagination.currPage+=1,c.fetchData())}),h=$("<ul/>").addClass("pagination data-prev-next").append([f,g]);return 0===c.config.pagination.currPage&&f.addClass("disabled"),e&&c.config.pagination.currPage===e-1&&g.addClass("disabled"),h},renderPagination:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}function c(){d.config.pagination.currPage=$(this).data("page"),d.fetchData()}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;for(var d=this,e=this.dataset.metadata_data_lines,f=e?Math.ceil(e/this.config.pagination.perPage):void 0,g=$("<ul/>").addClass("pagination data-pages"),h=0;f>h;h+=1){var i=b(h+1).attr("data-page",h).click(c);h===this.config.pagination.currPage&&i.addClass("active"),g.append(i)}return g},renderPlot:function(a){this.toggleStats(!1);var b=this.$el.find("svg");b.off().empty().show(),scatterplot(b.get(0),this.config,a)},getStats:function(a){var b=this;meanWorker=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js"),meanWorker.postMessage({data:a,keys:[this.config.xColumn,this.config.yColumn]}),meanWorker.onerror=function(){meanWorker.terminate()},meanWorker.onmessage=function(a){b.renderStats(a.data)}},renderStats:function(a){var b=this.$el.find(".stats-display"),c=this.config.x.label,d=this.config.y.label,e=$("<table/>").addClass("table").append(["<thead><th></th><th>",c,"</th><th>",d,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));b.empty().append(e)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{metadata:{dataLines:void 0},pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500}}}); \ No newline at end of file +function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.x.ticks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.y.ticks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("class","axis-label").text(b.x.label).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("class","axis-label").text(b.y.label).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="'+i((f=b.x,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="'+i((f=b.y,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n <option value="',(d=c.index)?d=d.call(a,{hash:{},data:b}):(d=a.index,d=typeof d===j?d.apply(a):d),e+=k(d)+'">',(d=c.name)?d=d.call(a,{hash:{},data:b}):(d=a.name,d=typeof d===j?d.apply(a):d),e+=k(d)+"</option>\n "}function g(){return'checked="true"'}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var h,i="",j="function",k=this.escapeExpression,l=this;return i+='<p class="help-text">\n Use the following controls to change the data used by the chart.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n\n<div class="column-select">\n <label>Data column for X: </label>\n <select name="xColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n<div class="column-select">\n <label>Data column for Y: </label>\n <select name="yColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="include-id">\n <label for="include-id-checkbox">Include a third column as data point IDs?</label>\n <input type="checkbox" name="include-id" id="include-id-checkbox" />\n <p class="help-text-small">\n These will be displayed (along with the x and y values) when you hover over\n a data point.\n </p>\n</div>\n<div class="column-select" style="display: none">\n <label for="ID-select">Data column for IDs: </label>\n <select name="idColumn">\n ',h=c.each.call(b,b.allColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="first-line-header" style="display: none;">\n <p>Possible headers: ',(h=c.possibleHeaders)?h=h.call(b,{hash:{},data:e}):(h=b.possibleHeaders,h=typeof h===j?h.apply(b):h),i+=k(h)+'\n </p>\n <label for="first-line-header-checkbox">Use the above as column headers?</label>\n <input type="checkbox" name="include-id" id="first-line-header-checkbox"\n ',h=c["if"].call(b,b.usePossibleHeaders,{hash:{},inverse:l.noop,fn:l.program(3,g,e),data:e}),(h||0===h)&&(i+=h),i+='/>\n <p class="help-text-small">\n It looks like Galaxy couldn\'t get proper column headers for this data.\n Would you like to use the column headers above as column names to select columns?\n </p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),this.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.log("dataset:",this.dataset),this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.dataset,d=c.metadata_column_names||[],e=this.model.get("config"),f=[],g=_.map(c.metadata_column_types,function(a,b){var c={index:b,type:a,name:d[b]||"column "+(b+1)};return("int"===c.type||"float"===c.type)&&f.push(c),c});f.length<2&&(f=g);var h=a.find(".tab-pane#data-control");h.html(ScatterplotConfigEditor.templates.dataControl({allColumns:g,numericColumns:f}));var i={xColumn:_.isFinite(e.xColumn)?e.xColumn:f[0].index,yColumn:_.isFinite(e.yColumn)?e.yColumn:f[1].index,idColumn:g[0].index};if(_.isFinite(e.idColumn))i.idColumn=e.idColumn;else if(g.length>2){var j=_.find(g,function(a,b){return b!==i.xColumn&&b!==i.yColumn});i.idColumn=j.index}return e=this.model.set("config",i,{silent:!0}).get("config"),h.find('[name="xColumn"]').val(e.xColumn).on("change",function(){b.model.set("config",{xColumn:Number($(this).val())})}),h.find('[name="yColumn"]').val(e.yColumn).on("change",function(){b.model.set("config",{yColumn:Number($(this).val())})}),h.find('select[name="idColumn"]').val(e.idColumn).on("change",function(){b.model.set("config",{idColumn:Number($(this).val())})}),void 0!==e.idColumn&&h.find("#include-id-checkbox").prop("checked",!0).trigger("change"),h.find("[title]").tooltip(),h},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.calcNumPages()},calcNumPages:function(){var a=this.model.get("config");this.lineCount=this.dataset.metadata_data_lines,this.numPages=this.lineCount?Math.ceil(this.lineCount/a.pagination.perPage):void 0,this.lineCount&&void 0!==this.numPages||console.warn("no data total found")},fetchData:function(){this.showLoadingIndicator("getting data");var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="left">',"</div>",'<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){return this.lineCount?this.$el.find(".controls .left").empty().append(this.renderPagination()):this.$el.find(".controls .left").empty().append(this.renderPrevNext()),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},goToPage:function(a){var b=this.model.get("config").pagination;return 0>=a&&(a=0),this.numPages&&a>=this.numPages&&(a=this.numPages-1),a===b.currPage?this:(b.currPage=a,this.model.set("config",{pagination:b}),this.resetZoom(),this.fetchData(),this)},nextPage:function(){var a=this.model.get("config").pagination.currPage;return this.goToPage(a+1)},prevPage:function(){var a=this.model.get("config").pagination.currPage;return this.goToPage(a-1)},renderPrevNext:function(){var a=this.model.get("config");if(!this.data)return null;if(0===a.pagination.currPage&&this.data.length<a.pagination.perPage)return null;var b=this,c=$('<li><a href="javascript:void(0);">Prev</a></li>').click(function(){b.prevPage()}),d=$('<li><a href="javascript:void(0);">Next</a></li>').click(function(){b.nextPage()});return 0===a.pagination.currPage&&c.addClass("disabled"),this.numPages&&a.pagination.currPage===this.numPages-1&&d.addClass("disabled"),$("<ul/>").addClass("pagination data-prev-next").append([c,d])},renderPagination:function(){var a=this.model.get("config");if(!this.data)return null;if(!this.numPages)return null;if(0===a.pagination.currPage&&this.data.length<a.pagination.perPage)return null;var b=this,c=$("<ul/>").addClass("pagination data-pages");pageNumClick=function(){b.goToPage($(this).data("page"))};for(var d=0;d<this.numPages;d+=1){var e=$(['<li><a href="javascript:void(0);">',d+1,"</a></li>"].join("")).attr("data-page",d).click(pageNumClick);d===a.pagination.currPage&&e.addClass("active"),c.append(e)}return c},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.x.label,e=b.y.label,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/static/scatterplot.css --- a/config/plugins/visualizations/scatterplot/static/scatterplot.css +++ b/config/plugins/visualizations/scatterplot/static/scatterplot.css @@ -39,6 +39,22 @@ font-size: smaller; } +.scatterplot-editor .file-controls { + float: right; + /*margin-right: 16px;*/ +} + +.file-controls .btn { + height: 24px; + padding: 0px 10px 0px 10px; + line-height: 24px; +} + +.file-controls .copy-btn { + display: none; +} + + /* ============================================ config controls */ .scatterplot-config-control { max-width: 768px; @@ -84,8 +100,6 @@ /* ============================================ plot display */ /* -------------------------------------------- load indicators */ .scatterplot-display .loading-indicator { - position: absolute; - margin: 10px 0px 0px 10px; color: grey; } diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -1,18 +1,8 @@ <% - 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' + default_title = "Scatterplot of '" + hda.name + "'" + info = hda.name + if hda.info: + info += ' : ' + hda.info # optionally bootstrap data from dprov ##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) ) @@ -23,7 +13,7 @@ <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> -<title>${hda.name} | ${visualization_name}</title> +<title>${title or default_title} | ${visualization_display_name}</title> ## ---------------------------------------------------------------------------- <link type="text/css" rel="Stylesheet" media="screen" href="/static/style/base.css"> @@ -52,23 +42,29 @@ %if not embedded: ## dataset info: only show if on own page <div class="chart-header"> - <h2>${title}</h2> + <h2>${title or default_title}</h2><p>${info}</p></div><div class="scatterplot-editor"></div><script type="text/javascript"> $(function(){ - var model = new ScatterplotModel( ${h.to_json_string( config )} ), - hdaJson = ${h.to_json_string( hda_dict )}, + var model = new ScatterplotModel({ + id : ${h.to_json_string( visualization_id )} || undefined, + title : "${title or default_title}", + config : ${h.to_json_string( config, indent=2 )} + }), + hdaJson = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ), indent=2 )}, 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(); + + model.on( 'change:title', function(){ + $( '.chart-header h2' ).text( model.get( 'title' ) ); + }) }); </script> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b config/plugins/visualizations/visualization_base.mako --- a/config/plugins/visualizations/visualization_base.mako +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -<% _=n_ %> - -%if embedded: - ${self.as_embedded()} -%else: - ${self.as_page()} -%endif - -## render this inside another page or via ajax -<%def name="as_embedded()"> - ${self.stylesheets()} - ${self.javascripts()} - ${self.get_body()} -</%def> - -## render this as it's own page -<%def name="as_page()"> -<!DOCTYPE HTML> -<html> - <head> - <title>${self.title()}</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - ${self.metas()} - ${self.stylesheets()} - ${self.javascripts()} - </head> - <body> - ${self.get_body()} - </body> -</html> -</%def> -##TODO: late_javascripts - -## Default body -<%def name="get_body()"></%def> - -## Default title -<%def name="title()">${visualization_name}</%def> - -## Additional metas can be defined by templates inheriting from this one. -<%def name="metas()"></%def> - -## Default stylesheets -<%def name="stylesheets()"> -${h.css('base')} -</%def> - -## Default javascripts -<%def name="javascripts()"> -${h.js( - "libs/jquery/jquery", - "libs/jquery/jquery.migrate" -)} - -<script type="text/javascript"> - // console protection - window.console = window.console || { - log : function(){}, - debug : function(){}, - info : function(){}, - warn : function(){}, - error : function(){}, - assert : function(){} - }; -</script> -</%def> diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b lib/galaxy/visualization/registry.py --- a/lib/galaxy/visualization/registry.py +++ b/lib/galaxy/visualization/registry.py @@ -223,7 +223,7 @@ def is_object_applicable( self, trans, target_object, data_source_tests ): """ Run a visualization's data_source tests to find out if - it be applied to the target_object. + it can be applied to the target_object. """ #log.debug( 'is_object_applicable( self, trans, %s, %s )', target_object, data_source_tests ) for test in data_source_tests: @@ -306,9 +306,9 @@ Both `params` and `param_modifiers` default to an empty dictionary. """ - visualization = self.plugins.get( visualization_name ) - expected_params = visualization.config.get( 'params', {} ) - param_modifiers = visualization.config.get( 'param_modifiers', {} ) + plugin = self.plugins.get( visualization_name ) + expected_params = plugin.config.get( 'params', {} ) + param_modifiers = plugin.config.get( 'param_modifiers', {} ) return ( expected_params, param_modifiers ) def query_dict_to_resources( self, trans, controller, visualization_name, query_dict ): @@ -322,6 +322,16 @@ trans, controller, param_confs, query_dict, param_modifiers ) return resources + def query_dict_to_config( self, trans, controller, visualization_name, query_dict ): + """ + Given a query string dict (i.e. kwargs) from a controller action, parse + and return any key/value pairs found in the plugin's `params` section. + """ + plugin = self.plugins.get( visualization_name ) + param_confs = plugin.config.get( 'params', {} ) + config = self.resource_parser.parse_config( trans, controller, param_confs, query_dict ) + return config + # ------------------------------------------------------------------- parsing the config file class ParsingException( ValueError ): @@ -559,37 +569,38 @@ for test_elem in xml_tree_list: test_type = test_elem.get( 'type', 'eq' ) - test_result = test_elem.text + test_result = test_elem.text.strip() if test_elem.text else None if not test_type or not test_result: log.warn( 'Skipping test. Needs both type attribute and text node to be parsed: ' + '%s, %s' %( test_type, test_elem.text ) ) continue + test_result = test_result.strip() # test_attr can be a dot separated chain of object attributes (e.g. dataset.datatype) - convert to list #TODO: too dangerous - constrain these to some allowed list #TODO: does this err if no test_attr - it should... test_attr = test_elem.get( 'test_attr' ) test_attr = test_attr.split( self.ATTRIBUTE_SPLIT_CHAR ) if isinstance( test_attr, str ) else [] + #log.debug( 'test_type: %s, test_attr: %s, test_result: %s', test_type, test_attr, test_result ) + # build a lambda function that gets the desired attribute to test getter = self._build_getattr_lambda( test_attr ) - # result type should tell the registry how to convert the result before the test test_result_type = test_elem.get( 'result_type', 'string' ) # test functions should be sent an object to test, and the parsed result expected from the test - - # is test_attr attribute an instance of result if test_type == 'isinstance': + # is test_attr attribute an instance of result #TODO: wish we could take this further but it would mean passing in the datatypes_registry test_fn = lambda o, result: isinstance( getter( o ), result ) - # does the object itself have a datatype attr and does that datatype have the given dataprovider elif test_type == 'has_dataprovider': + # does the object itself have a datatype attr and does that datatype have the given dataprovider test_fn = lambda o, result: ( hasattr( getter( o ), 'has_dataprovider' ) and getter( o ).has_dataprovider( result ) ) - # default to simple (string) equilavance (coercing the test_attr to a string) else: + # default to simple (string) equilavance (coercing the test_attr to a string) test_fn = lambda o, result: str( getter( o ) ) == result tests.append({ @@ -737,6 +748,16 @@ The keys used to store the new values can optionally be re-mapped to new keys (e.g. dataset_id="NNN" -> hda=<HistoryDatasetAsscoation>). """ + primitive_parsers = { + 'str' : lambda param: galaxy.util.sanitize_html.sanitize_html( param, 'utf-8' ), + 'bool' : lambda param: galaxy.util.string_as_bool( param ), + 'int' : lambda param: int( param ), + 'float' : lambda param: float( param ), + #'date' : lambda param: , + 'json' : ( lambda param: galaxy.util.json.from_json_string( + galaxy.util.sanitize_html.sanitize_html( param ) ) ), + } + #TODO: kinda torn as to whether this belongs here or in controllers.visualization # taking the (questionable) design path of passing a controller in # (which is the responsible party for getting model, etc. resources ) @@ -749,8 +770,6 @@ If param is required and not present, raises a `KeyError`. """ #log.debug( 'parse_parameter_dictionary, query_params:\n%s', query_params ) - # 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 @@ -782,12 +801,43 @@ if resource == None: if param_config[ 'required' ]: raise KeyError( 'required param %s not found in URL' %( param_name ) ) - resource = self.parse_parameter_default( trans, param_config ) + resource = self.parse_parameter_default( trans, controller, param_config ) resources[ var_name_in_template ] = resource return resources + def parse_config( self, trans, controller, param_config_dict, query_params ): + """ + Return `query_params` dict parsing only JSON serializable params. + Complex params such as models, etc. are left as the original query value. + Keys in `query_params` not found in the `param_config_dict` will not be + returned. + """ + #log.debug( 'parse_config, query_params:\n%s', query_params ) + config = {} + for param_name, param_config in param_config_dict.items(): + config_val = query_params.get( param_name, None ) + if config_val is not None and param_config[ 'type' ] in self.primitive_parsers: + try: + config_val = self.parse_parameter( trans, controller, param_config, config_val ) + + except Exception, exception: + log.warn( 'Exception parsing visualization param from query: ' + + '%s, %s, (%s) %s' %( param_name, config_val, str( type( exception ) ), str( exception ) )) + config_val = None + + # here - we've either had no value in the query_params or there was a failure to parse + # so: if there's a default and it's not None, add it to the config + if config_val is None: + if param_config.get( 'default', None ) is None: + continue + config_val = self.parse_parameter_default( trans, controller, param_config ) + + config[ param_name ] = config_val + + return config + #TODO: I would LOVE to rip modifiers out completely def parse_parameter_modifiers( self, trans, controller, param_modifiers, query_params ): """ @@ -811,11 +861,11 @@ target_modifiers[ modifier_name ] = modifier else: #TODO: required attr? - target_modifiers[ modifier_name ] = self.parse_parameter_default( trans, modifier_config ) + target_modifiers[ modifier_name ] = self.parse_parameter_default( trans, controller, modifier_config ) return parsed_modifiers - def parse_parameter_default( self, trans, param_config ): + def parse_parameter_default( self, trans, controller, param_config ): """ Parse any default values for the given param, defaulting the default to `None`. @@ -828,46 +878,8 @@ # otherwise, parse (currently param_config['default'] is a string just like query param and needs to be parsed) # this saves us the trouble of parsing the default when the config file is read # (and adding this code to the xml parser) - return self.parse_parameter( trans, param_config, default ) + return self.parse_parameter( trans, controller, 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 ): """ @@ -892,24 +904,11 @@ parsed_param.append( self._parse_param( trans, expected_param_data, query_param, recurse=False ) ) return parsed_param - primitive_parsers = { - 'str' : lambda param: galaxy.util.sanitize_html.sanitize_html( param, 'utf-8' ), - 'bool' : lambda param: galaxy.util.string_as_bool( param ), - 'int' : lambda param: int( param ), - 'float' : lambda param: float( param ), - #'date' : lambda param: , - 'json' : ( lambda param: galaxy.util.json.from_json_string( - galaxy.util.sanitize_html.sanitize_html( param ) ) ), - } - parser = primitive_parsers.get( param_type, None ) - if parser: + if param_type in self.primitive_parsers: #TODO: what about param modifiers on primitives? - parsed_param = parser( query_param ) + parsed_param = self.primitive_parsers[ param_type ]( query_param ) - #TODO: constrain_to - # this gets complicated - for strings - relatively simple but still requires splitting and using in - # for more complicated cases (ints, json) this gets weird quick - #TODO:?? remove? + #TODO: constrain_to: this gets complicated - remove? # db models #TODO: subclass here? diff -r a11c52513e4472237c9070a06ff11f123af83c06 -r a253d56a0dd6be3816581a0c7ed0ada191a1693b lib/galaxy/webapps/galaxy/controllers/visualization.py --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -1,9 +1,10 @@ from __future__ import absolute_import import os +import copy from sqlalchemy import desc, or_, and_ -from paste.httpexceptions import HTTPNotFound +from paste.httpexceptions import HTTPNotFound, HTTPBadRequest from galaxy import model, web from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings @@ -13,6 +14,9 @@ from galaxy.datatypes.interval import Bed from galaxy.util.json import from_json_string from galaxy.util.sanitize_html import sanitize_html +from galaxy.util import bunch +from galaxy import util +from galaxy.visualization import registry from galaxy.visualization.genomes import decode_dbkey from galaxy.visualization.data_providers.phyloviz import PhylovizDataProvider from galaxy.visualization.genomes import GenomeRegion @@ -22,6 +26,27 @@ import logging log = logging.getLogger( __name__ ) +# -- Misc. +class OpenObject( dict ): + #TODO: move to util.data_structures + """ + A dict that allows assignment and attribute retrieval using the dot + operator. + + If an attribute isn't contained in the dict `None` is returned (no + KeyError). + JSON-serializable. + """ + def __getitem__( self, key ): + if key not in self: + return None + return super( OpenObject, self ).__getitem__( key ) + + def __getattr__( self, key ): + return self.__getitem__( key ) + + + # # -- Grids -- # @@ -168,9 +193,14 @@ """ Returns dictionary used to create item link. """ - controller = "visualization" - action = item.type - return dict( controller=controller, action=action, id=item.id ) + url_kwargs = dict( controller='visualization', id=item.id ) + #TODO: hack to build link to saved visualization - need trans in this function instead in order to do + #link_data = trans.app.visualizations_registry.get_visualizations( trans, item ) + if item.type in registry.VisualizationsRegistry.BUILT_IN_VISUALIZATIONS: + url_kwargs[ 'action' ] = item.type + else: + url_kwargs[ 'action' ] = 'saved' + return url_kwargs # Grid definition title = "Saved Visualizations" @@ -179,6 +209,7 @@ default_filter = dict( title="All", deleted="False", tags="All", sharing="All" ) columns = [ grids.TextColumn( "Title", key="title", attach_popup=True, link=get_url_args ), + #TODO: should use display name when available 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" ), @@ -623,7 +654,6 @@ Save a visualization; if visualization does not have an ID, a new visualization is created. Returns JSON of visualization. """ - # Get visualization attributes from kwargs or from config. vis_config = from_json_string( vis_json ) vis_type = type or vis_config[ 'type' ] @@ -686,10 +716,45 @@ # # Visualizations. # + @web.expose + @web.require_login( "use Galaxy visualizations", use_panels=True ) + def saved( self, trans, id=None, revision=None, type=None, config=None, title=None, **kwargs ): + """ + """ + DEFAULT_VISUALIZATION_NAME = 'Unnamed Visualization' + + # post to saved in order to save a visualization + #TODO: re-route this one to clear up signature + if trans.request.method == 'POST': + if type is None or config is None: + return HTTPBadRequest( 'A visualization type and config are required to save a visualization' ) + if isinstance( config, basestring ): + config = from_json_string( config ) + title = title or DEFAULT_VISUALIZATION_NAME + #TODO: allow saving to (updating) a specific revision - should be part of UsesVisualization + #TODO: would be easier if this returned the visualization directly + returned = self.save_visualization( trans, config, type, id, title ) + + # redirect to GET to prevent annoying 'Do you want to post again?' dialog on page reload + render_url = web.url_for( controller='visualization', action='saved', id=returned.get( 'vis_id' ) ) + return trans.response.send_redirect( render_url ) + + if id is None: + return HTTPBadRequest( 'A valid visualization id is required to load a visualization' ) + + # render the saved visualization by passing to render, sending latest revision config + #TODO: allow loading a specific revision - should be part of UsesVisualization + visualization = self.get_visualization( trans, id, check_ownership=True, check_accessible=False ) + config = copy.copy( visualization.latest_revision.config ) + + # re-add title to kwargs for passing to render + if title: + kwargs[ 'title' ] = title + return self.render( trans, visualization.type, visualization, config=config, **kwargs ) @web.expose @web.require_login( "use Galaxy visualizations", use_panels=True ) - def render( self, trans, visualization_name, embedded=None, **kwargs ): + def render( self, trans, visualization_name, visualization=None, config=None, embedded=None, **kwargs ): """ Render the appropriate visualization template, parsing the `kwargs` into appropriate variables and resources (such as ORM models) @@ -697,6 +762,8 @@ URL: /visualization/show/{visualization_name} """ + config = config or {} + # validate name vs. registry registry = trans.app.visualizations_registry if not registry: @@ -708,20 +775,32 @@ returned = None try: - # convert query string to resources for template based on registry config - #NOTE: passing in controller to keep resource lookup within the controller's responsibilities - # (and not the ResourceParser) - resources = registry.query_dict_to_resources( trans, self, visualization_name, kwargs ) + # get the config for passing to the template from the kwargs dict, parsed using the plugin's params setting + config_from_kwargs = registry.query_dict_to_config( trans, self, visualization_name, kwargs ) + config.update( config_from_kwargs ) + config = OpenObject( **config ) + # further parse config to resources (models, etc.) used in template based on registry config + resources = registry.query_dict_to_resources( trans, self, visualization_name, config ) + + # if a saved visualization, pass in the encoded visualization id or None if a new render + encoded_visualization_id = None + if visualization: + encoded_visualization_id = trans.security.encode_id( visualization.id ) + + visualization_display_name = plugin.config[ 'name' ] + title = visualization.latest_revision.title if visualization else kwargs.get( 'title', None ) # look up template and render template_path = plugin.config[ 'template' ] returned = registry.fill_template( trans, plugin, template_path, - visualization_name=visualization_name, query_args=kwargs, - embedded=embedded, shared_vars={}, **resources ) - #NOTE: passing *unparsed* kwargs as query_args - #NOTE: shared_vars is a dictionary for shared data in the template + visualization_name=visualization_name, visualization_display_name=visualization_display_name, + title=title, saved_visualization=visualization, visualization_id=encoded_visualization_id, + embedded=embedded, query=kwargs, vars={}, config=config, **resources ) + #NOTE: passing *unparsed* kwargs as query + #NOTE: vars is a dictionary for shared data in the template # this feels hacky to me but it's what mako recommends: # http://docs.makotemplates.org/en/latest/runtime.html + #TODO: should vars contain all the passed in arguments? is that even necessary? #TODO: embedded except Exception, exception: 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.