Scatterplot overhaul progress. Should just need to fix bootstrap tooltip loading and it's good to go.

This commit is contained in:
Dannon Baker
2017-12-04 08:46:03 -05:00
parent b6509f402f
commit c23c2324bc
13 changed files with 893 additions and 760 deletions
@@ -1,387 +0,0 @@
/* =============================================================================
todo:
localize
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 in chart controls
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 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' namessave
Somehow link out from info box?
Subclass on specific datatypes? (vcf, cuffdiff, etc.)
What can be common/useful to other visualizations?
============================================================================= */
/**
* Scatterplot config control UI as a backbone view
* handles:
* configuring which data will be used
* configuring the plot display
*/
var ScatterplotConfigEditor = Backbone.View.extend({
//TODO: !should be a view on a visualization model
//logger : console,
className : 'scatterplot-control-form',
/** initialize requires a configuration Object containing a dataset Object */
initialize : function( attributes ){
if( !this.model ){
this.model = new Visualization({ type: 'scatterplot' });
}
//this.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.dataset ){
throw new Error( "ScatterplotConfigEditor requires a dataset" );
}
this.dataset = attributes.dataset;
//this.log( 'dataset:', this.dataset );
this.display = new ScatterplotDisplay({
dataset : attributes.dataset,
model : this.model
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
//console.log( this + '.render' );
// render the tab controls, areas and loading indicator
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._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;
},
/** get an object with arrays keyed with possible column types (numeric, text, all)
* and if metadata_column_types is set on the dataset, add the indeces of each
* column into the appropriate array.
* Used to disable certain columns from being selected for x, y axes.
*/
_getColumnIndecesByType : function(){
//TODO: not sure these contraints are necc. now
var types = {
numeric : [],
text : [],
all : []
};
_.each( this.dataset.metadata_column_types || [], function( type, i ){
if( type === 'int' || type === 'float' ){
types.numeric.push( i );
} else if( type === 'str' || type === 'list' ){
types.text.push( i );
}
types.all.push( i );
});
if( types.numeric.length < 2 ){
types.numeric = [];
}
//console.log( 'types:', JSON.stringify( types ) );
return types;
},
/** controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) */
_render_dataControl : function( $where ){
$where = $where || this.$el;
var editor = this,
//column_names = dataset.metadata_column_names || [],
config = this.model.get( 'config' ),
columnTypes = this._getColumnIndecesByType();
// render the html
var $dataControl = $where.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotConfigEditor.templates.dataControl({
peek : this.dataset.peek
}));
$dataControl.find( '.peek' ).peekColumnSelector({
controls : [
{ label: 'X Column', id: 'xColumn', selected: config.xColumn, disabled: columnTypes.text },
{ label: 'Y Column', id: 'yColumn', selected: config.yColumn, disabled: columnTypes.text },
{ label: 'ID Column', id: 'idColumn', selected: config.idColumn }
]
//renameColumns : true
}).on( 'peek-column-selector.change', function( ev, data ){
//console.info( 'new selection:', data );
editor.model.set( 'config', data );
}).on( 'peek-column-selector.rename', function( ev, data ){
//console.info( 'new column names', data );
});
$dataControl.find( '[title]' ).tooltip();
return $dataControl;
},
/** tab content to control how the chart is rendered (data glyph size, chart size, etc.) */
_render_chartControls : function( $where ){
//TODO: as controls on actual chart
$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 );
// ---- slider controls
// limits for controls (by control/chartConfig id)
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(){
// 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 : config[ configKey ],
change : onSliderChange,
slide : onSliderChange
});
//console.debug( configKey + ' slider settings:', sliderSettings );
$this.find( '.slider' ).slider( sliderSettings );
$this.children( '.slider-output' ).text( config[ configKey ] );
});
// ---- 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)
$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 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;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'click #data-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.display.render();
});
},
toggleThirdColumnSelector : function(){
// show/hide the id selector on the data settings panel
this.$el.find( 'select[name="idColumn"]' ).parent().toggle();
},
// ------------------------------------------------------------------------- CHART/STATS RENDERING
renderChart : function(){
//console.log( this + '.renderChart' );
// fetch the data, (re-)render the chart
this.$el.find( '.nav li.disabled' ).removeClass( 'disabled' );
this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
this.display.fetchData();
//console.debug( this.display.$el );
},
toString : function(){
return 'ScatterplotConfigEditor(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotConfigEditor.templates = {
// tabbed, main layout for the editor (not used for scatterplot-display)
mainLayout : _.template([
'<div class="scatterplot-editor tabbable tabs-left">',
// tab buttons/headers using Bootstrap
'<ul class="nav nav-tabs">',
// start with the data controls as the displayed tab
'<li class="active">',
'<a title="Use this tab to change which data are used"',
'href="#data-control" data-toggle="tab">Data Controls</a>',
'</li>',
'<li>',
'<a title="Use this tab to change how the chart is drawn"',
'href="#chart-control" data-toggle="tab" >Chart Controls</a>',
'</li>',
// 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>',
// button for saving the visualization config on the server
'<li class="file-controls">',
'<button class="save-btn btn btn-default">Save</button>',
'</li>',
'</ul>',
// 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"></div>',
// tab for chart graphics control form
'<div id="chart-control" class="scatterplot-config-control tab-pane"></div>',
// tab for actual chart
'<div id="chart-display" class="scatterplot-display tab-pane"></div>',
'</div>',
'</div>',
].join('')),
// the controls for data selection (this is mostly done with column selector now)
// TODO: this could be moved to the main template above
// TODO: localize
dataControl : _.template([
'<p class="help-text">',
'Use the following control to change which columns are used by the chart. Click any cell ',
'from the last three rows of the table to select the column for the appropriate data. ',
'Use the \'Draw\' button to render (or re-render) the chart with the current settings. ',
'</p>',
'<ul class="help-text" style="margin-left: 8px">',
'<li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>',
'<li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>',
'<li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point. ',
'It may be useful to select unique or categorical identifiers here (such as gene ids).',
'</li>',
'</ul>',
'<div class="column-selection">',
// the only dynamic thing
'<pre class="peek"><%= peek %></pre>',
'</div>',
'<p class="help-text help-text-small">',
'<b>Note</b>: If it can be determined from the dataset\'s filetype that a column is not numeric, ',
'that column choice may be disabled for either the x or y axis.',
'</p>',
'<button class="render-button btn btn-primary active">Draw</button>',
].join('')),
chartControl : _.template([
'<p class="help-text">',
'Use the following controls to how the chart is displayed. The slide controls can be moved ',
'by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys. ',
'Move the focus between controls by using the tab or shift+tab keys on your keyboard. ',
'Use the \'Draw\' button to render (or re-render) the chart with the current settings. ',
'</p>',
'<div data-config-key="datapointSize" class="form-input numeric-slider-input">',
'<label for="datapointSize">Size of data point: </label>',
'<div class="slider-output"><%- datapointSize %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'Size of the graphic representation of each data point',
'</p>',
'</div>',
'<div data-config-key="width" class="form-input numeric-slider-input">',
'<label for="width">Chart width: </label>',
'<div class="slider-output"><%- width %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'(not including chart margins and axes)',
'</p>',
'</div>',
'<div data-config-key="height" class="form-input numeric-slider-input">',
'<label for="height">Chart height: </label>',
'<div class="slider-output"><%- height %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'(not including chart margins and axes)',
'</p>',
'</div>',
'<div data-config-key="X-axis-label"class="text-input form-input">',
'<label for="X-axis-label">Re-label the X axis: </label>',
'<input type="text" name="X-axis-label" id="X-axis-label" value="<%- xLabel %>" />',
'<p class="form-help help-text-small"></p>',
'</div>',
'<div data-config-key="Y-axis-label" class="text-input form-input">',
'<label for="Y-axis-label">Re-label the Y axis: </label>',
'<input type="text" name="Y-axis-label" id="Y-axis-label" value="<%- yLabel %>" />',
'<p class="form-help help-text-small"></p>',
'</div>',
'<button class="render-button btn btn-primary active">Draw</button>',
].join('')),
// mainLayout : scatterplot.editor,
// dataControl : scatterplot.datacontrol,
// chartControl : scatterplot.chartcontrol
};
//==============================================================================
@@ -1,214 +0,0 @@
// =============================================================================
/**
* Scatterplot display control UI as a backbone view
* handles:
* fetching the data (if needed)
* computing and displaying data stats
* controls for pagination of data (if needed)
*/
var ScatterplotDisplay = Backbone.View.extend({
initialize : function( attributes ){
this.data = null,
this.dataset = attributes.dataset;
this.lineCount = this.dataset.metadata_data_lines || null;
},
fetchData : function(){
this.showLoadingIndicator();
//console.debug( 'currPage', this.config.pagination.currPage );
var view = this,
config = this.model.get( 'config' ),
//TODO: very tied to datasets - should be generalized eventually
baseUrl = ( window.parent && parent.galaxy_config )? parent.galaxy_config.root : '/',
xhr = jQuery.getJSON( baseUrl + 'api/datasets/' + this.dataset.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : config.pagination.perPage,
offset : ( config.pagination.currPage * config.pagination.perPage )
});
xhr.done( function( data ){
// no need to hide loading indicator, line info will write over that
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 );
});
return xhr;
},
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">loading...</span>',
'</div>'
].join( '' ));
},
template : function(){
var html = [
'<div class="controls clear">',
'<div class="right">',
'<p class="scatterplot-data-info"></p>',
'<button class="stats-toggle-btn">Stats</button>',
'<button class="rerender-btn">Redraw</button>',
'</div>',
'<div class="left">',
'<div class="page-control"></div>',
'</div>',
'</div>',
'<svg/>', //TODO: id
'<div class="stats-display"></div>'
].join( '' );
return html;
},
render : function(){
this.$el.addClass( 'scatterplot-display' ).html( this.template() );
if( this.data ){
this.renderData();
}
return this;
},
renderData : function(){
this.renderLeftControls();
this.renderRightControls();
this.renderPlot( this.data );
this.getStats();
},
renderLeftControls : function(){
var display = this,
config = this.model.get( 'config' );
this.$el.find( '.controls .left .page-control' ).pagination({
startingPage : config.pagination.currPage,
perPage : config.pagination.perPage,
totalDataSize: this.lineCount,
currDataSize : this.data.length
//TODO: move to named function and remove only named
}).off().on( 'pagination.page-change', function( event, page ){
//console.debug( 'pagination:page-change', page );
config.pagination.currPage = page;
display.model.set( 'config', { pagination: config.pagination });
//console.debug( pagination, display.model.get( 'config' ).pagination );
display.resetZoom();
display.fetchData();
});
return this;
},
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 );
});
},
/** 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 );
},
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;
},
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;
},
// ------------------------------------------------------------------------ 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 : this.data,
keys : [ config.xColumn, config.yColumn ]
});
meanWorker.onerror = function( event ){
meanWorker.terminate();
};
meanWorker.onmessage = function( event ){
view.renderStats( event.data );
};
},
renderStats : function( stats, error ){
//console.debug( 'renderStats:', stats, error );
//console.debug( JSON.stringify( stats, null, ' ' ) );
var config = this.model.get( 'config' ),
$statsTable = this.$el.find( '.stats-display' ),
xLabel = config.xLabel, yLabel = config.yLabel,
$table = $( '<table/>' ).addClass( 'table' )
.append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' ))
.append( _.map( stats, function( stat, key ){
return $([ '<tr><td>', key, '</td><td>', stat[0], '</td><td>', stat[1], '</td></tr>' ].join( '' ));
}));
$statsTable.empty().append( $table );
},
toggleStats : function( showStats ){
var $statsDisplay = this.$el.find( '.stats-display' );
showStats = ( showStats === undefined )?( $statsDisplay.is( ':hidden' ) ):( showStats );
if( showStats ){
this.$el.find( 'svg' ).hide();
$statsDisplay.show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Plot' );
} else {
$statsDisplay.hide();
this.$el.find( 'svg' ).show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Stats' );
}
},
toString : function(){
return 'ScatterplotView()';
}
});
@@ -1,39 +0,0 @@
var ScatterplotModel = Visualization.extend({
defaults : {
type : 'scatterplot',
config : {
// shouldn't be needed for properly saved splots - also often incorrect
//xColumn : 0,
//yColumn : 1,
pagination : {
currPage : 0,
perPage : 3000
},
// graph style
width : 400,
height : 400,
margin : {
top : 16,
right : 16,
bottom : 40,
left : 54
},
xTicks : 10,
xLabel : 'X',
yTicks : 10,
yLabel : 'Y',
datapointSize : 4,
animDuration : 500,
scale : 1,
translate : [ 0, 0 ]
}
}
});
@@ -1,11 +1,13 @@
// =============================================================================
import * as VisualizationModel from "mvc/visualization/visualization-model";
/**
* Two Variable scatterplot visualization using d3
* Uses semi transparent circles to show density of data in x, y grid
* usage :
* var plot = new scatterplot( $( 'svg' ).get(0), config, data )
*/
function scatterplot( renderTo, config, data ){
export function scatterplot( renderTo, config, data ){
//console.log( 'scatterplot', config );
var translateStr = function( x, y ){
@@ -114,7 +116,7 @@ function scatterplot( renderTo, config, data ){
.attr( 'y', ( config.height + config.margin.bottom + config.margin.top ) - padding );
//console.log( 'axis.x.label:', axis.x.label );
//TODO: anchor to left of x margin/graph
//TODO: anchor to left of x margin/graph
// y-axis label
// place 4 pixels left of the axis.y.g left edge
axis.y.label = svg.append( 'text' )
@@ -285,3 +287,646 @@ function scatterplot( renderTo, config, data ){
}
//==============================================================================
/* =============================================================================
todo:
localize
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 in chart controls
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 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' namessave
Somehow link out from info box?
Subclass on specific datatypes? (vcf, cuffdiff, etc.)
What can be common/useful to other visualizations?
============================================================================= */
/**
* Scatterplot config control UI as a backbone view
* handles:
* configuring which data will be used
* configuring the plot display
*/
var ScatterplotConfigEditor = Backbone.View.extend({
//TODO: !should be a view on a visualization model
//logger : console,
className : 'scatterplot-control-form',
/** initialize requires a configuration Object containing a dataset Object */
initialize : function( attributes ){
if( !this.model ){
this.model = new VisualizationModel.Visualization({ type: 'scatterplot' });
}
//this.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.dataset ){
throw new Error( "ScatterplotConfigEditor requires a dataset" );
}
this.dataset = attributes.dataset;
//this.log( 'dataset:', this.dataset );
this.display = new ScatterplotDisplay({
dataset : attributes.dataset,
model : this.model
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
//console.log( this + '.render' );
// render the tab controls, areas and loading indicator
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._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;
},
/** get an object with arrays keyed with possible column types (numeric, text, all)
* and if metadata_column_types is set on the dataset, add the indeces of each
* column into the appropriate array.
* Used to disable certain columns from being selected for x, y axes.
*/
_getColumnIndecesByType : function(){
//TODO: not sure these contraints are necc. now
var types = {
numeric : [],
text : [],
all : []
};
_.each( this.dataset.metadata_column_types || [], function( type, i ){
if( type === 'int' || type === 'float' ){
types.numeric.push( i );
} else if( type === 'str' || type === 'list' ){
types.text.push( i );
}
types.all.push( i );
});
if( types.numeric.length < 2 ){
types.numeric = [];
}
//console.log( 'types:', JSON.stringify( types ) );
return types;
},
/** controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) */
_render_dataControl : function( $where ){
$where = $where || this.$el;
var editor = this,
//column_names = dataset.metadata_column_names || [],
config = this.model.get( 'config' ),
columnTypes = this._getColumnIndecesByType();
// render the html
var $dataControl = $where.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotConfigEditor.templates.dataControl({
peek : this.dataset.peek
}));
$dataControl.find( '.peek' ).peekColumnSelector({
controls : [
{ label: 'X Column', id: 'xColumn', selected: config.xColumn, disabled: columnTypes.text },
{ label: 'Y Column', id: 'yColumn', selected: config.yColumn, disabled: columnTypes.text },
{ label: 'ID Column', id: 'idColumn', selected: config.idColumn }
]
//renameColumns : true
}).on( 'peek-column-selector.change', function( ev, data ){
//console.info( 'new selection:', data );
editor.model.set( 'config', data );
}).on( 'peek-column-selector.rename', function( ev, data ){
//console.info( 'new column names', data );
});
$dataControl.find( '[title]' ).tooltip();
return $dataControl;
},
/** tab content to control how the chart is rendered (data glyph size, chart size, etc.) */
_render_chartControls : function( $where ){
//TODO: as controls on actual chart
$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 );
// ---- slider controls
// limits for controls (by control/chartConfig id)
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(){
// 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 : config[ configKey ],
change : onSliderChange,
slide : onSliderChange
});
//console.debug( configKey + ' slider settings:', sliderSettings );
$this.find( '.slider' ).slider( sliderSettings );
$this.children( '.slider-output' ).text( config[ configKey ] );
});
// ---- 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)
$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 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;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'click #data-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.display.render();
});
},
toggleThirdColumnSelector : function(){
// show/hide the id selector on the data settings panel
this.$el.find( 'select[name="idColumn"]' ).parent().toggle();
},
// ------------------------------------------------------------------------- CHART/STATS RENDERING
renderChart : function(){
//console.log( this + '.renderChart' );
// fetch the data, (re-)render the chart
this.$el.find( '.nav li.disabled' ).removeClass( 'disabled' );
this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
this.display.fetchData();
//console.debug( this.display.$el );
},
toString : function(){
return 'ScatterplotConfigEditor(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotConfigEditor.templates = {
// tabbed, main layout for the editor (not used for scatterplot-display)
mainLayout : _.template([
'<div class="scatterplot-editor tabbable tabs-left">',
// tab buttons/headers using Bootstrap
'<ul class="nav nav-tabs">',
// start with the data controls as the displayed tab
'<li class="active">',
'<a title="Use this tab to change which data are used"',
'href="#data-control" data-toggle="tab">Data Controls</a>',
'</li>',
'<li>',
'<a title="Use this tab to change how the chart is drawn"',
'href="#chart-control" data-toggle="tab" >Chart Controls</a>',
'</li>',
// 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>',
// button for saving the visualization config on the server
'<li class="file-controls">',
'<button class="save-btn btn btn-default">Save</button>',
'</li>',
'</ul>',
// 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"></div>',
// tab for chart graphics control form
'<div id="chart-control" class="scatterplot-config-control tab-pane"></div>',
// tab for actual chart
'<div id="chart-display" class="scatterplot-display tab-pane"></div>',
'</div>',
'</div>',
].join('')),
// the controls for data selection (this is mostly done with column selector now)
// TODO: this could be moved to the main template above
// TODO: localize
dataControl : _.template([
'<p class="help-text">',
'Use the following control to change which columns are used by the chart. Click any cell ',
'from the last three rows of the table to select the column for the appropriate data. ',
'Use the \'Draw\' button to render (or re-render) the chart with the current settings. ',
'</p>',
'<ul class="help-text" style="margin-left: 8px">',
'<li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>',
'<li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>',
'<li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point. ',
'It may be useful to select unique or categorical identifiers here (such as gene ids).',
'</li>',
'</ul>',
'<div class="column-selection">',
// the only dynamic thing
'<pre class="peek"><%= peek %></pre>',
'</div>',
'<p class="help-text help-text-small">',
'<b>Note</b>: If it can be determined from the dataset\'s filetype that a column is not numeric, ',
'that column choice may be disabled for either the x or y axis.',
'</p>',
'<button class="render-button btn btn-primary active">Draw</button>',
].join('')),
chartControl : _.template([
'<p class="help-text">',
'Use the following controls to how the chart is displayed. The slide controls can be moved ',
'by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys. ',
'Move the focus between controls by using the tab or shift+tab keys on your keyboard. ',
'Use the \'Draw\' button to render (or re-render) the chart with the current settings. ',
'</p>',
'<div data-config-key="datapointSize" class="form-input numeric-slider-input">',
'<label for="datapointSize">Size of data point: </label>',
'<div class="slider-output"><%- datapointSize %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'Size of the graphic representation of each data point',
'</p>',
'</div>',
'<div data-config-key="width" class="form-input numeric-slider-input">',
'<label for="width">Chart width: </label>',
'<div class="slider-output"><%- width %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'(not including chart margins and axes)',
'</p>',
'</div>',
'<div data-config-key="height" class="form-input numeric-slider-input">',
'<label for="height">Chart height: </label>',
'<div class="slider-output"><%- height %></div>',
'<div class="slider"></div>',
'<p class="form-help help-text-small">',
'(not including chart margins and axes)',
'</p>',
'</div>',
'<div data-config-key="X-axis-label"class="text-input form-input">',
'<label for="X-axis-label">Re-label the X axis: </label>',
'<input type="text" name="X-axis-label" id="X-axis-label" value="<%- xLabel %>" />',
'<p class="form-help help-text-small"></p>',
'</div>',
'<div data-config-key="Y-axis-label" class="text-input form-input">',
'<label for="Y-axis-label">Re-label the Y axis: </label>',
'<input type="text" name="Y-axis-label" id="Y-axis-label" value="<%- yLabel %>" />',
'<p class="form-help help-text-small"></p>',
'</div>',
'<button class="render-button btn btn-primary active">Draw</button>',
].join('')),
// mainLayout : scatterplot.editor,
// dataControl : scatterplot.datacontrol,
// chartControl : scatterplot.chartcontrol
};
//==============================================================================
// =============================================================================
/**
* Scatterplot display control UI as a backbone view
* handles:
* fetching the data (if needed)
* computing and displaying data stats
* controls for pagination of data (if needed)
*/
var ScatterplotDisplay = Backbone.View.extend({
initialize : function( attributes ){
this.data = null,
this.dataset = attributes.dataset;
this.lineCount = this.dataset.metadata_data_lines || null;
},
fetchData : function(){
this.showLoadingIndicator();
//console.debug( 'currPage', this.config.pagination.currPage );
var view = this,
config = this.model.get( 'config' ),
//TODO: very tied to datasets - should be generalized eventually
baseUrl = ( window.parent && parent.galaxy_config )? parent.galaxy_config.root : '/',
xhr = jQuery.getJSON( baseUrl + 'api/datasets/' + this.dataset.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : config.pagination.perPage,
offset : ( config.pagination.currPage * config.pagination.perPage )
});
xhr.done( function( data ){
// no need to hide loading indicator, line info will write over that
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 );
});
return xhr;
},
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">loading...</span>',
'</div>'
].join( '' ));
},
template : function(){
var html = [
'<div class="controls clear">',
'<div class="right">',
'<p class="scatterplot-data-info"></p>',
'<button class="stats-toggle-btn">Stats</button>',
'<button class="rerender-btn">Redraw</button>',
'</div>',
'<div class="left">',
'<div class="page-control"></div>',
'</div>',
'</div>',
'<svg/>', //TODO: id
'<div class="stats-display"></div>'
].join( '' );
return html;
},
render : function(){
this.$el.addClass( 'scatterplot-display' ).html( this.template() );
if( this.data ){
this.renderData();
}
return this;
},
renderData : function(){
this.renderLeftControls();
this.renderRightControls();
this.renderPlot( this.data );
this.getStats();
},
renderLeftControls : function(){
var display = this,
config = this.model.get( 'config' );
this.$el.find( '.controls .left .page-control' ).pagination({
startingPage : config.pagination.currPage,
perPage : config.pagination.perPage,
totalDataSize: this.lineCount,
currDataSize : this.data.length
//TODO: move to named function and remove only named
}).off().on( 'pagination.page-change', function( event, page ){
//console.debug( 'pagination:page-change', page );
config.pagination.currPage = page;
display.model.set( 'config', { pagination: config.pagination });
//console.debug( pagination, display.model.get( 'config' ).pagination );
display.resetZoom();
display.fetchData();
});
return this;
},
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 );
});
},
/** 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 );
},
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;
},
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;
},
// ------------------------------------------------------------------------ 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 : this.data,
keys : [ config.xColumn, config.yColumn ]
});
meanWorker.onerror = function( event ){
meanWorker.terminate();
};
meanWorker.onmessage = function( event ){
view.renderStats( event.data );
};
},
renderStats : function( stats, error ){
//console.debug( 'renderStats:', stats, error );
//console.debug( JSON.stringify( stats, null, ' ' ) );
var config = this.model.get( 'config' ),
$statsTable = this.$el.find( '.stats-display' ),
xLabel = config.xLabel, yLabel = config.yLabel,
$table = $( '<table/>' ).addClass( 'table' )
.append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' ))
.append( _.map( stats, function( stat, key ){
return $([ '<tr><td>', key, '</td><td>', stat[0], '</td><td>', stat[1], '</td></tr>' ].join( '' ));
}));
$statsTable.empty().append( $table );
},
toggleStats : function( showStats ){
var $statsDisplay = this.$el.find( '.stats-display' );
showStats = ( showStats === undefined )?( $statsDisplay.is( ':hidden' ) ):( showStats );
if( showStats ){
this.$el.find( 'svg' ).hide();
$statsDisplay.show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Plot' );
} else {
$statsDisplay.hide();
this.$el.find( 'svg' ).show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Stats' );
}
},
toString : function(){
return 'ScatterplotView()';
}
});
var ScatterplotModel = VisualizationModel.Visualization.extend({
defaults : {
type : 'scatterplot',
config : {
// shouldn't be needed for properly saved splots - also often incorrect
//xColumn : 0,
//yColumn : 1,
pagination : {
currPage : 0,
perPage : 3000
},
// graph style
width : 400,
height : 400,
margin : {
top : 16,
right : 16,
bottom : 40,
left : 54
},
xTicks : 10,
xLabel : 'X',
yTicks : 10,
yLabel : 'Y',
datapointSize : 4,
animDuration : 500,
scale : 1,
translate : [ 0, 0 ]
}
}
});
window.ScatterplotModel = ScatterplotModel;
window.ScatterplotConfigEditor = ScatterplotConfigEditor;