mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
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:
@@ -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>'
|
||||
|
||||
Reference in New Issue
Block a user