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
@@ -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>
@@ -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">
@@ -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(){
@@ -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
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
},
var ScatterplotDisplay = Backbone.View.extend({
initialize : function( attributes ){
this.config = _.extend( _.clone( this.defaults ), attributes.config || {});
this.data = null,
this.dataset = attributes.dataset;
//console.debug( this + '.config:', this.config );
this.calcNumPages();
},
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 ){
alert( 'Error loading data:\n' + xhr.responseText );
console.error( xhr, status, message );
});
xhr.always( function(){
view.hideLoadingIndicator();
view.trigger( 'data:error', view );
alert( 'Error loading data:\n' + xhr.responseText );
});
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' );
renderData : function(){
this.renderLeftControls();
this.renderRightControls();
this.renderPlot( this.data );
this.getStats();
},
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();
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;
},
hideLoadingIndicator : function( speed ){
speed = speed || 'fast';
this.$el.find( '.loading-indicator' ).hide();
},
renderData : function( data ){
this.$el.find( '.controls' ).empty().append( this.renderControls( data ) ).show();
this.renderPlot( data );
this.getStats( data );
},
renderControls : function( data ){
renderRightControls : function(){
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 ];
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 );
});
},
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( ' ' ));
},
renderPrevNext : function( data ){
// this is cra-zazy
if( !data
|| ( this.config.pagination.currPage === 0 && data.length < this.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
/** render and show the d3 plot into the svg node of the view */
renderPlot : function(){
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 );
$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 );
},
// 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 ]);
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 || '' );
}
if( view.config.pagination.currPage === 0 ){
return this;
},
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;
},
// ------------------------------------------------------------------------ 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,
$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(); });
// 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 @@ var ScatterplotView = Backbone.View.extend({
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 ){
@@ -46,8 +46,9 @@ function scatterplot( renderTo, config, data ){
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 @@ function scatterplot( renderTo, config, data ){
//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 @@ function scatterplot( renderTo, config, data ){
.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 @@ function scatterplot( renderTo, config, data ){
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>'
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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>