mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Scatterplot overhaul progress. Should just need to fix bootstrap tooltip loading and it's good to go.
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
// NOTE: use 'npm install' from this directory, then 'grunt' to use this file
|
||||
// to build scatterplot-edit.js
|
||||
module.exports = function(grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON( 'package.json' ),
|
||||
|
||||
concat: {
|
||||
// concat any js files in the src dir into a single file in the build dir
|
||||
options: {
|
||||
separator: ';\n'
|
||||
},
|
||||
dist: {
|
||||
src : [ 'src/**/*.js' ],
|
||||
dest: 'build/scatterplot-concat.js'
|
||||
}
|
||||
},
|
||||
|
||||
uglify: {
|
||||
// uglify the concat single file directly into the static dir
|
||||
options: {
|
||||
// uncomment these to allow better source mapping during development
|
||||
//mangle : false,
|
||||
//beautify : true
|
||||
},
|
||||
dist: {
|
||||
src : 'build/scatterplot-concat.js',
|
||||
dest: 'static/scatterplot-edit.js'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// watch for changes in the src dir
|
||||
files: [ 'src/**.js' ],
|
||||
tasks: [ 'default' ]
|
||||
}
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks( 'grunt-contrib-concat' );
|
||||
grunt.loadNpmTasks( 'grunt-contrib-uglify' );
|
||||
grunt.loadNpmTasks( 'grunt-contrib-watch' );
|
||||
|
||||
grunt.registerTask( 'default', [ 'concat', 'uglify' ]);
|
||||
// you can run grunt watch directly:
|
||||
// grunt watch
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -1,88 +1,92 @@
|
||||
var numericColumnStats = function( data, keys ){
|
||||
var console = console || { debug: function(){} },
|
||||
var numericColumnStats = function(data, keys) {
|
||||
var console = console || { debug: function() {} },
|
||||
stats = {};
|
||||
|
||||
// precondition: keys is an array of keys accessible on each data in data
|
||||
if( !keys || !keys.length ){
|
||||
throw new Error( 'keys is a required parameter and must be an array:' + keys );
|
||||
if (!keys || !keys.length) {
|
||||
throw new Error("keys is a required parameter and must be an array:" + keys);
|
||||
}
|
||||
if( !data || !data.length ){
|
||||
if (!data || !data.length) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
function parseVal( val ){
|
||||
if( val === null ){
|
||||
throw new Error( 'Null value' );
|
||||
function parseVal(val) {
|
||||
if (val === null) {
|
||||
throw new Error("Null value");
|
||||
}
|
||||
val = Number( val );
|
||||
if( isNaN( val ) || !isFinite( val ) ){
|
||||
throw new Error( 'NaN or non-finite number' );
|
||||
val = Number(val);
|
||||
if (isNaN(val) || !isFinite(val)) {
|
||||
throw new Error("NaN or non-finite number");
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// does this effect the data in the other thread?
|
||||
[ 'min', 'max', 'sum', 'mean', 'median', 'count' ].forEach( function( name, i ){
|
||||
stats[ name ] = new Array( keys.length );
|
||||
["min", "max", "sum", "mean", "median", "count"].forEach(function(name, i) {
|
||||
stats[name] = new Array(keys.length);
|
||||
});
|
||||
|
||||
var keyIndex = 0,
|
||||
separatedCols = new Array( keys.length );
|
||||
keys.forEach( function( key, keyIndex ){
|
||||
stats.min[ keyIndex ] = 0;
|
||||
stats.max[ keyIndex ] = 0;
|
||||
stats.sum[ keyIndex ] = 0;
|
||||
separatedCols[ keyIndex ] = [];
|
||||
separatedCols = new Array(keys.length);
|
||||
keys.forEach(function(key, keyIndex) {
|
||||
stats.min[keyIndex] = 0;
|
||||
stats.max[keyIndex] = 0;
|
||||
stats.sum[keyIndex] = 0;
|
||||
separatedCols[keyIndex] = [];
|
||||
});
|
||||
|
||||
// work backwards to prevent co-modification problems while splitting up data into columns
|
||||
for( var dataIndex=( data.length - 1 ); dataIndex>=0; dataIndex-=1 ){
|
||||
var datum = data.pop( dataIndex );
|
||||
for (var dataIndex = data.length - 1; dataIndex >= 0; dataIndex -= 1) {
|
||||
var datum = data.pop(dataIndex);
|
||||
|
||||
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
|
||||
var key = keys[ keyIndex ],
|
||||
datumColVal = datum[ key ];
|
||||
for (keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
||||
var key = keys[keyIndex],
|
||||
datumColVal = datum[key];
|
||||
|
||||
try {
|
||||
//NOTE: parsing value as number
|
||||
datumColVal = parseVal( datumColVal );
|
||||
} catch( e ){
|
||||
datumColVal = parseVal(datumColVal);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// separate the columns
|
||||
separatedCols[ keyIndex ].unshift( datumColVal );
|
||||
separatedCols[keyIndex].unshift(datumColVal);
|
||||
// get the other stats
|
||||
stats.min[ keyIndex ] = Math.min( stats.min[ keyIndex ], datumColVal );
|
||||
stats.max[ keyIndex ] = Math.max( stats.max[ keyIndex ], datumColVal );
|
||||
stats.sum[ keyIndex ] += datumColVal;
|
||||
stats.min[keyIndex] = Math.min(stats.min[keyIndex], datumColVal);
|
||||
stats.max[keyIndex] = Math.max(stats.max[keyIndex], datumColVal);
|
||||
stats.sum[keyIndex] += datumColVal;
|
||||
}
|
||||
}
|
||||
|
||||
// get counts, mean, median
|
||||
function comparator( a, b ){
|
||||
if( a < b ){ return -1; }
|
||||
if( a < b ){ return 1; }
|
||||
function comparator(a, b) {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
|
||||
var count = separatedCols[ keyIndex ].length,
|
||||
sum = stats.sum[ keyIndex ];
|
||||
stats.count[ keyIndex ] = count;
|
||||
stats.mean[ keyIndex ] = ( sum / count );
|
||||
for (keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
||||
var count = separatedCols[keyIndex].length,
|
||||
sum = stats.sum[keyIndex];
|
||||
stats.count[keyIndex] = count;
|
||||
stats.mean[keyIndex] = sum / count;
|
||||
|
||||
// sort columns for median
|
||||
separatedCols[ keyIndex ].sort( comparator );
|
||||
separatedCols[keyIndex].sort(comparator);
|
||||
|
||||
// odd count -> straight forward median
|
||||
var middleDataIndex = Math.floor( count / 2 );
|
||||
if( count % 2 === 1 ){
|
||||
stats.median[ keyIndex ] = separatedCols[ keyIndex ][ middleDataIndex ];
|
||||
var middleDataIndex = Math.floor(count / 2);
|
||||
if (count % 2 === 1) {
|
||||
stats.median[keyIndex] = separatedCols[keyIndex][middleDataIndex];
|
||||
} else {
|
||||
var middleValA = separatedCols[ keyIndex ][ middleDataIndex ],
|
||||
middleValB = separatedCols[ keyIndex ][( middleDataIndex + 1 )];
|
||||
stats.median[ keyIndex ] = ( middleValA + middleValB ) / 2;
|
||||
var middleValA = separatedCols[keyIndex][middleDataIndex],
|
||||
middleValB = separatedCols[keyIndex][middleDataIndex + 1];
|
||||
stats.median[keyIndex] = (middleValA + middleValB) / 2;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
onmessage = function( event ){
|
||||
importScripts( 'numeric-column-stats.js' );
|
||||
postMessage( numericColumnStats( event.data.data, event.data.keys ) );
|
||||
onmessage = function(event) {
|
||||
importScripts("numeric-column-stats.js");
|
||||
postMessage(numericColumnStats(event.data.data, event.data.keys));
|
||||
self.close();
|
||||
};
|
||||
|
||||
@@ -28,31 +28,27 @@ ${h.stylesheet_link( root + 'plugins/visualizations/scatterplot/static/scatterpl
|
||||
window.Galaxy = { root: '${ root }' };
|
||||
</script>
|
||||
${h.js( 'libs/jquery/jquery',
|
||||
'libs/require',
|
||||
'libs/jquery/jquery.migrate',
|
||||
'libs/jquery/jquery-ui',
|
||||
'libs/require',
|
||||
'libs/bootstrap',
|
||||
'libs/underscore',
|
||||
'libs/backbone',
|
||||
'libs/d3',
|
||||
'ui/peek-column-selector',
|
||||
'ui/pagination',
|
||||
'mvc/visualization/visualization-model' )}
|
||||
'libs/d3')}
|
||||
|
||||
${h.javascript_link( root + 'plugins/visualizations/scatterplot/static/scatterplot-edit.js' )}
|
||||
|
||||
<script type="text/javascript">
|
||||
function getModel(){
|
||||
return new ScatterplotModel({
|
||||
id : ${h.dumps( visualization_id )} || undefined,
|
||||
title : "${title or default_title}",
|
||||
config : ${h.dumps( config, indent=2 )}
|
||||
});
|
||||
}
|
||||
function getHDAJSON(){
|
||||
return ${h.dumps( trans.security.encode_dict_ids( hda.to_dict() ), indent=2 )};
|
||||
}
|
||||
</script>
|
||||
${h.javascript_link( root + 'plugins/visualizations/scatterplot/static/scatterplot.bundle.js' )}
|
||||
<script type="text/javascript">
|
||||
function getModel(){
|
||||
return new ScatterplotModel({
|
||||
id : ${h.dumps( visualization_id )} || undefined,
|
||||
title : "${title or default_title}",
|
||||
config : ${h.dumps( config, indent=2 )}
|
||||
});
|
||||
}
|
||||
function getHDAJSON(){
|
||||
return ${h.dumps( trans.security.encode_dict_ids( hda.to_dict() ), indent=2 )};
|
||||
}
|
||||
window.jQuery = window.jquery = window.$;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
var path = require("path");
|
||||
var croot = path.resolve(__dirname, "../../../../");
|
||||
var webpack = require("webpack");
|
||||
var lroot = path.resolve(croot, "client/galaxy/scripts/libs");
|
||||
|
||||
module.exports = {
|
||||
entry: path.resolve(__dirname, "./src/scatterplot.js"),
|
||||
output: {
|
||||
path: path.resolve(__dirname, "static"),
|
||||
filename: "scatterplot.bundle.js"
|
||||
},
|
||||
resolve: {
|
||||
modules: [
|
||||
path.resolve(croot, "client/galaxy/scripts"),
|
||||
path.resolve(croot, "client/node_modules")
|
||||
],
|
||||
alias: {
|
||||
jquery: path.join(lroot, "jquery/jquery"),
|
||||
underscore: path.join(lroot, "underscore.js"),
|
||||
backbone: path.join(lroot, "backbone.js")
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: [/(node_modules|bower_components)/, lroot],
|
||||
loader: "babel-loader"
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
_: "underscore",
|
||||
Backbone: "backbone",
|
||||
})
|
||||
]
|
||||
};
|
||||
Reference in New Issue
Block a user