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

This commit is contained in:
Carl Eberhard
2014-02-07 11:24:32 -05:00
parent 2ced0e26b4
commit 3c1983a3d4
13 changed files with 583 additions and 418 deletions
@@ -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 @@ var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({
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({}));
// 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_chartDisplay();
//TODO: auto render if given both x, y column choices in query for page
// set up behaviours
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();
// 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' );
// render the tab content
this._render_dataControl();
this._render_chartControls();
this._render_chartDisplay();
// set up behaviours
// 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 @@ var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({
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 @@ var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({
//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(){