Popupmenu mvc: fix anchor target in renderOptions; Visualizations registry: default link target to galaxy_main; Scatterplot (in registry): general refactoring and cleanup, paginate data instead of loading all, allow zoom and pan; pack scripts

This commit is contained in:
Carl Eberhard
2013-11-08 11:02:23 -05:00
parent a55fab57f8
commit 108f30d541
24 changed files with 1200 additions and 1403 deletions
@@ -6,6 +6,7 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON( 'package.json' ),
handlebars: {
// compile all hb templates into a single file in the build dir
compile: {
options: {
namespace: 'Templates',
@@ -20,6 +21,7 @@ module.exports = function(grunt) {
},
concat: {
// concat the template file and any js files in the src dir into a single file in the build dir
options: {
separator: ';\n'
},
@@ -31,16 +33,19 @@ module.exports = function(grunt) {
},
uglify: {
// uglify the concat single file directly into the static dir
options: {
//mangle : false,
//beautify : true
},
dist: {
src : 'build/scatterplot-concat.js',
// uglify directly into static dir
dest: 'static/scatterplot.js'
dest: 'static/scatterplot-edit.js'
}
},
watch: {
// watch for changes in the src dir
files: [ 'src/**.js', 'src/handlebars/*.handlebars' ],
tasks: [ 'default' ]
}
@@ -52,5 +57,6 @@ module.exports = function(grunt) {
grunt.loadNpmTasks( 'grunt-contrib-watch' );
grunt.registerTask( 'default', [ 'handlebars', 'concat', 'uglify' ]);
grunt.registerTask( 'watch', [ 'handlebars', 'concat', 'uglify', 'watch' ]);
// you can run grunt watch directly:
// grunt watch
};
@@ -19,6 +19,6 @@
"grunt-contrib-handlebars": "~0.5.10",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-watch": "~0.5.1"
"grunt-contrib-watch": "~0.5.3"
}
}
@@ -1,56 +0,0 @@
<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 id="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 id="animDuration" class="form-input checkbox-input">
<label for="animate-chart">Animate chart transitions?: </label>
<input type="checkbox" id="animate-chart"
class="checkbox control"{{#if animDuration}} checked="true"{{/if}} />
<p class="form-help help-text-small">
Uncheck this to disable the animations used on the chart
</p>
</div>
<div id="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 id="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 id="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 id="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>
<input id="render-button" type="button" value="Draw" />
@@ -1 +0,0 @@
<svg width="{{width}}" height="{{height}}"></svg>

Before

Width:  |  Height:  |  Size: 49 B

@@ -0,0 +1,47 @@
<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="{{x.label}}" />
<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="{{y.label}}" />
<p class="form-help help-text-small"></p>
</div>
<button class="render-button btn btn-primary active">Draw</button>
@@ -1,56 +0,0 @@
<p class="help-text">
Use the following controls to change the data used by the chart.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
{{! column selector containers }}
<div class="column-select">
<label for="X-select">Data column for X: </label>
<select name="X" id="X-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="column-select">
<label for="Y-select">Data column for Y: </label>
<select name="Y" id="Y-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! optional id column }}
<div id="include-id">
<label for="include-id-checkbox">Include a third column as data point IDs?</label>
<input type="checkbox" name="include-id" id="include-id-checkbox" />
<p class="help-text-small">
These will be displayed (along with the x and y values) when you hover over
a data point.
</p>
</div>
<div class="column-select" style="display: none">
<label for="ID-select">Data column for IDs: </label>
<select name="ID" id="ID-select">
{{#each allColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! if we're using generic column selection names ('column 1') - allow the user to use the first line }}
<div id="first-line-header" style="display: none;">
<p>Possible headers: {{ possibleHeaders }}
</p>
<label for="first-line-header-checkbox">Use the above as column headers?</label>
<input type="checkbox" name="include-id" id="first-line-header-checkbox"
{{#if usePossibleHeaders }}checked="true"{{/if}}/>
<p class="help-text-small">
It looks like Galaxy couldn't get proper column headers for this data.
Would you like to use the column headers above as column names to select columns?
</p>
</div>
<input id="render-button" type="button" value="Draw" />
<div class="clear"></div>
@@ -0,0 +1,55 @@
<p class="help-text">
Use the following controls to change the data used by the chart.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
{{! column selector containers }}
<div class="column-select">
<label>Data column for X: </label>
<select name="xColumn">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="column-select">
<label>Data column for Y: </label>
<select name="yColumn">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! optional id column }}
<div id="include-id">
<label for="include-id-checkbox">Include a third column as data point IDs?</label>
<input type="checkbox" name="include-id" id="include-id-checkbox" />
<p class="help-text-small">
These will be displayed (along with the x and y values) when you hover over
a data point.
</p>
</div>
<div class="column-select" style="display: none">
<label for="ID-select">Data column for IDs: </label>
<select name="idColumn">
{{#each allColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! if we're using generic column selection names ('column 1') - allow the user to use the first line }}
<div id="first-line-header" style="display: none;">
<p>Possible headers: {{ possibleHeaders }}
</p>
<label for="first-line-header-checkbox">Use the above as column headers?</label>
<input type="checkbox" name="include-id" id="first-line-header-checkbox"
{{#if usePossibleHeaders }}checked="true"{{/if}}/>
<p class="help-text-small">
It looks like Galaxy couldn't get proper column headers for this data.
Would you like to use the column headers above as column names to select columns?
</p>
</div>
<button class="render-button btn btn-primary active">Draw</button>
@@ -0,0 +1,36 @@
<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>
{{! both stats and chart start 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>
</ul>
{{! data form, chart config form, stats, and 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">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for chart graphics control form }}
<div id="chart-control" class="scatterplot-config-control tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="scatterplot-display tab-pane"></div>
</div>{{! end .tab-content }}
</div>{{! end .chart-control }}
@@ -1,46 +0,0 @@
{{! main layout }}
<div class="scatterplot-container chart-container 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 href="#data-control" data-toggle="tab" class="tooltip"
title="Use this tab to change which data are used">Data Controls</a></li>
<li><a href="#chart-control" data-toggle="tab" class="tooltip"
title="Use this tab to change how the chart is drawn">Chart Controls</a></li>
<li><a href="#stats-display" data-toggle="tab" class="tooltip"
title="This tab will display overall statistics for your data">Statistics</a></li>
<li><a href="#chart-display" data-toggle="tab" class="tooltip"
title="This tab will display the chart">Chart</a>
{{! loading indicator - initially hidden }}
<div id="loading-indicator" style="display: none;">
<img class="loading-img" src="{{loadingIndicatorImagePath}}" />
<span class="loading-message">{{message}}</span>
</div>
</li>
</ul>
{{! data form, chart config form, stats, and chart all get their own tab }}
<div class="tab-content">
{{! ---------------------------- tab for data settings form }}
<div id="data-control" class="tab-pane active">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for chart graphics control form }}
<div id="chart-control" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for data statistics }}
<div id="stats-display" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="tab-pane">
{{! chart rendered separately }}
</div>
</div>{{! end .tab-content }}
</div>{{! end .chart-control }}
@@ -1,8 +0,0 @@
<p class="help-text">By column:</p>
<table id="chart-stats-table">
<thead><th></th><th>X</th><th>Y</th></thead>
{{#each stats}}
<tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr>
</tr>
{{/each}}
</table>
@@ -0,0 +1,237 @@
/* =============================================================================
todo:
Remove 'chart' names
Make this (the config control/editor) and the ScatterplotView (in scatterplot.js) both
views onto a visualization/revision model
Move margins into wid/hi calcs (so final svg dims are w/h)
Better separation of AJAX in scatterplot.js (maybe pass in function?)
Labels should auto fill in chart control when dataset has column_names
Allow column selection/config using the peek output as a base for UI
Allow setting perPage of config
Auto render if given data and/or config
Allow option to auto set width/height based on screen real estate avail.
Use d3.nest to allow grouping, pagination/filtration by group (e.g. chromCol)
Semantic HTML (figure, caption)
Save as visualization, load from visualization
Save as SVG/png
Does it work w/ Galaxy.Frame?
Embedding
Small multiples
Drag & Drop other splots onto current (redraw with new axis and differentiate the datasets)
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 = BaseView.extend( LoggableMixin ).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 ){
//console.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.config || !attributes.config.dataset ){
throw new Error( "ScatterplotView requires a configuration and dataset" );
}
this.dataset = attributes.config.dataset;
//console.log( 'dataset:', this.dataset );
this.plotView = new ScatterplotView({
config : attributes.config
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
//console.log( this + '.render' );
// render the tab controls, areas and loading indicator
this.$el.append( ScatterplotConfigEditor.templates.mainLayout({
}));
// render the tab content
this.$el.find( '#data-control' ).append( this._render_dataControl() );
this._render_chartControls( this.$el.find( '#chart-control' ) );
//this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' );
this._render_chartDisplay();
//TODO: auto render if given both x, y column choices in query for page
// set up behaviours
this.$el.find( '[title]' ).tooltip();
// uncomment any of the following to have that tab show on initial load (for testing)
//this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
return this;
},
_render_dataControl : function(){
// controls for which columns are used to plot datapoints (and ids/additional info to attach if desired)
var dataset = this.dataset;
//console.log( 'metadata_column_types:', this.dataset.metadata_column_types );
//console.log( 'metadata_column_names:', this.dataset.metadata_column_names );
var allColumns = _.map( dataset.metadata_column_types, function( type, i ){
var column = { index: i, type: type, name: ( 'column ' + ( i + 1 ) ) };
if( dataset.metadata_column_names && dataset.metadata_column_names[ i ] ){
column.name = dataset.metadata_column_names[ i ];
}
return column;
});
var numericColumns = _.filter( allColumns, function( column, i ){
return ( ( column.type === 'int' ) || ( column.type === 'float' ) );
});
if( numericColumns < 2 ){
numericColumns = allColumns;
}
//console.log( 'allColumns:', allColumns );
//console.log( 'numericColumns:', numericColumns );
// render the html
var $dataControl = this.$el.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotConfigEditor.templates.dataControl({
allColumns : allColumns,
numericColumns : numericColumns
}));
// preset to column selectors if they were passed in the config in the query string
$dataControl.find( '[name="xColumn"]' ).val( this.plotView.config.xColumn || numericColumns[0].index );
$dataControl.find( '[name="yColumn"]' ).val( this.plotView.config.yColumn || numericColumns[1].index );
if( this.plotView.config.idColumn !== undefined ){
$dataControl.find( '#include-id-checkbox' ).prop( 'checked', true ).trigger( 'change' );
$dataControl.find( 'select[name="idColumn"]' ).val( this.plotView.config.idColumn );
}
return $dataControl;
},
_render_chartControls : function( $chartControls ){
// tab content to control how the chart is rendered (data glyph size, chart size, etc.)
$chartControls.html( ScatterplotConfigEditor.templates.chartControl( this.plotView.config ) );
//console.debug( '$chartControl:', $chartControls );
// set up behaviours, js on sliders
//console.debug( 'numeric sliders:', $chartControls.find( '.numeric-slider-input' ) );
// what to do when the slider changes: update display and update chartConfig
var view = this,
// limits for controls (by control/chartConfig id)
//TODO: move into TwoVarScatterplot
controlRanges = {
'datapointSize' : { min: 2, max: 10, step: 1 },
'width' : { min: 200, max: 800, step: 20 },
'height' : { min: 200, max: 800, step: 20 }
};
function onSliderChange(){
var $this = $( this );
$this.siblings( '.slider-output' ).text( $this.slider( 'value' ) );
}
$chartControls.find( '.numeric-slider-input' ).each( function(){
var $this = $( this ),
configKey = $this.attr( 'data-config-key' ),
sliderSettings = _.extend( controlRanges[ configKey ], {
value : view.plotView.config[ configKey ],
change : onSliderChange,
slide : onSliderChange
});
//console.debug( configKey + ' slider settings:', sliderSettings );
$this.find( '.slider' ).slider( sliderSettings );
});
//TODO: to more common area (like render)?
// set label inputs to current x, y metadata_column_names (if any)
if( this.dataset.metadata_column_names ){
//var colNames = this.dataset.metadata_column_names;
//$chartControls.find( 'input[name="X-axis-label"]' ).val( colNames );
//$chartControls.find( 'input[name="Y-axis-label"]' ).val( colNames );
//TODO: on change of x, y data controls
}
//console.debug( '$chartControls:', $chartControls );
return $chartControls;
},
_render_chartDisplay : function(){
// render the tab content where the chart is displayed (but not the chart itself)
var $chartDisplay = this.$el.find( '.tab-pane#chart-display' );
this.plotView.setElement( $chartDisplay );
this.plotView.render();
return $chartDisplay;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'click #data-control .render-button' : 'renderChart',
'click #chart-control .render-button' : 'renderChart'
},
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.updateConfigWithDataSettings();
this.updateConfigWithChartSettings();
this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
this.plotView.fetchData();
//console.debug( this.plotView.$el );
},
// ------------------------------------------------------------------------- GET DATA/CHART SETTINGS
updateConfigWithDataSettings : function(){
// parse the column values for both indeces (for the data fetch) and names (for the chart)
var $dataControls = this.$el.find( '#data-control' );
var settings = {
xColumn : $dataControls.find( '[name="xColumn"]' ).val(),
yColumn : $dataControls.find( '[name="yColumn"]' ).val()
};
if( $dataControls.find( '#include-id-checkbox' ).prop( 'checked' ) ){
settings.idColumn = $dataControls.find( '[name="idColumn"]' ).val();
}
//console.log( '\t data settings:', settings );
return _.extend( this.plotView.config, settings );
},
updateConfigWithChartSettings : function(){
// gets the user-selected chartConfig from the chart settings panel
var plotView = this.plotView,
$chartControls = this.$el.find( '#chart-control' );
// use a loop of config keys to get the form values for these sliders
[ 'datapointSize', 'width', 'height' ].forEach( function( v, i ){
plotView.config[ v ] = $chartControls.find( '.numeric-slider-input[data-config-key="' + v + '"]' )
.find( '.slider' ).slider( 'value' );
});
// update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName
plotView.config.x.label = $chartControls.find( 'input[name="X-axis-label"]' ).val();
plotView.config.y.label = $chartControls.find( 'input[name="Y-axis-label"]' ).val();
//console.log( '\t chartSettings:', settings );
return plotView.config;
},
toString : function(){
return 'ScatterplotConfigEditor(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotConfigEditor.templates = {
mainLayout : Templates.editor,
dataControl : Templates.datacontrol,
chartControl : Templates.chartcontrol
};
//==============================================================================
@@ -0,0 +1,288 @@
// =============================================================================
/**
* 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 ScatterplotView = Backbone.View.extend({
//TODO: should be a view on visualization(revision) model
defaults : {
dataset : {
},
metadata : {
dataLines : undefined
},
ajaxFn : null,
pagination : {
currPage : 0,
perPage : 3000
},
width : 400,
height : 400,
margin : {
top : 16,
right : 16,
bottom : 40,
left : 54
},
x : {
ticks : 10,
label : 'X'
},
y : {
ticks : 10,
label : 'Y'
},
datapointSize : 4,
animDuration : 500
},
initialize : function( attributes ){
this.config = _.extend( _.clone( this.defaults ), attributes.config || {});
//console.debug( this + '.config:', this.config );
},
updateConfig : function( newConfig ){
//console.log( this + '.updateConfig:', newConfig );
this.config = this.config || {};
//TODO: validate here
_.extend( this.config, newConfig );
//TODO: implement rerender flag
},
fetchData : function(){
//TODO: doesn't work bc it's rendered in render()...
this.showLoadingIndicator( 'getting data' );
//console.debug( 'currPage', this.config.pagination.currPage );
var view = this;
//TODO: very tied to datasets - should be generalized eventually
xhr = jQuery.getJSON( '/api/datasets/' + this.config.dataset.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : this.config.pagination.perPage,
offset : ( this.config.pagination.currPage * this.config.pagination.perPage )
});
xhr.done( function( data ){
view.renderData( data.data );
});
xhr.fail( function( xhr, status, message ){
alert( 'Error loading data:\n' + xhr.responseText );
console.error( xhr, status, message );
});
xhr.always( function(){
view.hideLoadingIndicator();
});
return xhr;
},
render : function( data ){
this.$el.addClass( 'scatterplot-display' ).html([
'<div class="controls clear"></div>',
'<div class="loading-indicator">',
'<span class="fa fa-spinner fa-spin"></span>',
'<span class="loading-indicator-message"></span>',
'</div>',
'<svg/>', //TODO: id
'<div class="stats-display"></div>'
].join( '' ));
this.$el.children().hide();
if( data ){
this.renderData( data );
}
return this;
},
showLoadingIndicator : function( message, speed ){
// display the loading indicator over the tab panels if hidden, update message (if passed)
//TODO: move loading indicator into data-info-text
message = message || '';
speed = speed || 'fast';
var $indicator = this.$el.find( '.loading-indicator' );
if( message ){ $indicator.find( '.loading-indicator-message' ).text( message ); }
if( !$indicator.is( ':visible' ) ){
this.toggleStats( false );
$indicator.css({ left: ( this.config.width / 2 ), top: this.config.height / 2 }).show();
}
},
hideLoadingIndicator : function( speed ){
speed = speed || 'fast';
this.$el.find( '.loading-indicator' ).hide();
},
renderData : function( data ){
this.$el.find( '.controls' ).empty().append( this.renderControls( data ) ).show();
this.renderPlot( data );
this.getStats( data );
},
renderControls : function( data ){
var view = this;
var $left = $( '<div class="left"></div>' ),
$right = $( '<div class="right"></div>' );
$left.append([
this.renderPrevNext( data ),
this.renderPagination( data )
]);
$right.append([
this.renderLineInfo( data ),
$( '<button>Stats</button>' ).addClass( 'stats-toggle-btn' )
.click( function(){
view.toggleStats();
}),
$( '<button>Redraw</button>' ).addClass( 'rerender-btn' )
.click( function(){
view.renderPlot( data );
})
]);
return [ $left, $right ];
},
renderLineInfo : function( data ){
var totalLines = this.config.dataset.metadata_data_lines || 'an unknown number of',
lineStart = ( this.config.pagination.currPage * this.config.pagination.perPage ),
lineEnd = lineStart + data.length;
return $( '<p/>' ).addClass( 'scatterplot-data-info' )
.text([ 'Displaying lines', lineStart + 1, 'to', lineEnd, 'of', totalLines, 'lines' ].join( ' ' ));
},
renderPrevNext : function( data ){
// this is cra-zazy
if( !data
|| ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; }
function makePage$Li( text ){
return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' ));
}
//TODO: cache numPages/numLines in config
var view = this,
dataLines = this.config.dataset.metadata_data_lines,
numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined );
//console.debug( 'data:', this.config.dataset.metadata_data_lines, 'numPages:', numPages );
// prev next buttons
var $prev = makePage$Li( 'Prev' ).click( function(){
if( view.config.pagination.currPage > 0 ){
view.config.pagination.currPage -= 1;
view.fetchData();
}
}),
$next = makePage$Li( 'Next' ).click( function(){
if( !numPages || view.config.pagination.currPage < ( numPages - 1 ) ){
view.config.pagination.currPage += 1;
view.fetchData();
}
}),
$prevNextList = $( '<ul/>' ).addClass( 'pagination data-prev-next' )
.append([ $prev, $next ]);
if( view.config.pagination.currPage === 0 ){
$prev.addClass( 'disabled' );
}
if( numPages && view.config.pagination.currPage === ( numPages - 1 ) ){
$next.addClass( 'disabled' );
}
return $prevNextList;
},
renderPagination : function( data ){
// this is cra-zazy
if( !data
|| ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; }
function makePage$Li( text ){
return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' ));
}
//TODO: cache numPages/numLines in config
var view = this,
dataLines = this.config.dataset.metadata_data_lines,
numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined );
//console.debug( 'data:', this.config.dataset.metadata_data_lines, 'numPages:', numPages );
// page numbers (as separate control)
//var $paginationContainer = $( '<div/>' ).addClass( 'pagination-container' ),
var $pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' );
function pageNumClick( ev ){
view.config.pagination.currPage = $( this ).data( 'page' );
view.fetchData();
}
for( var i=0; i<numPages; i+=1 ){
// add page data for later event handling
var $pageLi = makePage$Li( i + 1 ).attr( 'data-page', i ).click( pageNumClick );
if( i === this.config.pagination.currPage ){
$pageLi.addClass( 'active' );
}
$pagesList.append( $pageLi );
}
// placing the pages list in an extra container allows us to set a max-width and scroll if overflow
//$paginationContainer.append( $pagesList );
//return $paginationContainer;
return $pagesList;
},
renderPlot : function( data ){
this.toggleStats( false );
var $svg = this.$el.find( 'svg' );
$svg.off().empty().show();
scatterplot( $svg.get( 0 ), this.config, data );
},
getStats : function( data ){
var view = this;
meanWorker = new Worker( '/plugins/visualizations/scatterplot/static/worker-stats.js' );
meanWorker.postMessage({
data : data,
keys : [ this.config.xColumn, this.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 $statsTable = this.$el.find( '.stats-display' );
var xLabel = this.config.x.label, yLabel = this.config.y.label,
$table = $( '<table/>' ).addClass( 'table' )
.append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' ))
.append( _.map( stats, function( stat, key ){
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,488 +1,278 @@
/* =============================================================================
todo:
outside this:
BUG: setting width, height in plot controls doesn't re-interpolate data locations!!
BUG?: get metadata_column_names (from datatype if necessary)
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
wire label setters, anim setter
TwoVarScatterplot:
??: maybe better to do this with a canvas...
save as visualization
to seperate file?
remove underscore dependencies
add interface to change values (seperate)?
download svg -> base64 encode
incorporate glyphs, glyph state renderers
ScatterplotSettingsForm:
some css bug that lowers the width of settings form when plot-controls tab is open
causes chart to shift
what can be abstracted/reused for other graphs?
avoid direct manipulation of this.plot
allow option to put plot into seperate tab of interface (for small multiples)
provide callback in view to load data incrementally - for large sets
paginate
handle rerender
use endpoint (here and on the server (fileptr))
fetch (new?) data
handle rerender
use d3.TSV?
render warning on long data (> maxDataPoints)
adjust endpoint
selectable list of preset column comparisons (rnaseq etc.)
how to know what sort of Tabular the data is?
smarter about headers
validate columns selection (here or server)
set stats column names by selected columns
move chart into tabbed area...
Scatterplot.mako:
multiple plots on one page (small multiples)
?? ensure svg styles thru d3 or css?
d3: configable (easily)
css: standard - better maintenance
? override at config
============================================================================= */
// =============================================================================
/**
* Two Variable scatterplot visualization using d3
* Uses semi transparent circles to show density of data in x, y grid
* usage :
* var plot = new TwoVarScatterplot({ containerSelector : 'div#my-plot', ... })
* plot.render( xColumnData, yColumnData );
*
* depends on: d3, underscore
* var plot = new scatterplot( $( 'svg' ).get(0), config, data )
*/
function TwoVarScatterplot( config ){
var TICK_LINE_AND_PADDING = 10,
GUESS_AT_SVG_CHAR_WIDTH = 7,
GUESS_AT_SVG_CHAR_HEIGHT = 10,
PADDING = 8,
X_LABEL_TOO_LONG_AT = 5;
// set up logging
//this.debugging = true;
this.log = function(){
if( this.debugging && console && console.debug ){
var args = Array.prototype.slice.call( arguments );
args.unshift( this.toString() );
console.debug.apply( console, args );
}
};
this.log( 'new TwoVarScatterplot:', config );
// ........................................................ set up chart config
// config will default to these values when not passed in
//NOTE: called on new
this.defaults = {
id : 'TwoVarScatterplot',
containerSelector : 'body',
//TODO??: needed?
maxDataPoints : 30000,
datapointSize : 4,
animDuration : 500,
//TODO: variable effect (not always exactly # of ticks set to)
xNumTicks : 10,
yNumTicks : 10,
xAxisLabelBumpY : 40,
yAxisLabelBumpX : -40,
width : 400,
height : 400,
//TODO: anyway to make this a sub-obj?
marginTop : 50,
marginRight : 50,
marginBottom : 50,
marginLeft : 50,
xMin : null,
xMax : null,
yMin : null,
yMax : null,
xLabel : "X",
yLabel : "Y"
};
this.config = _.extend( {}, this.defaults, config );
this.log( 'intial config:', this.config );
this.updateConfig = function( newConfig, rerender ){
// setter for chart config
//TODO: validate here
_.extend( this.config, newConfig );
this.log( this + '.updateConfig:', this.config );
//TODO: implement rerender flag
};
// ........................................................ helpers
this.toString = function(){
return this.config.id;
};
// conv. methods for svg transforms
this.translateStr = function( x, y ){
return 'translate(' + x + ',' + y + ')';
};
this.rotateStr = function( d, x, y ){
return 'rotate(' + d + ',' + x + ',' + y + ')';
function scatterplot( renderTo, config, data ){
//console.log( 'scatterplot', config );
var translateStr = function( x, y ){
return 'translate(' + x + ',' + y + ')';
},
rotateStr = function( d, x, y ){
return 'rotate(' + d + ',' + x + ',' + y + ')';
},
getX = function( d, i ){
//console.debug( d[ config.xColumn ] );
return d[ config.xColumn ];
},
getY = function( d, i ){
//console.debug( d[ config.yColumn ] );
return d[ config.yColumn ];
};
// .................................................................... scales
var stats = {
x : { extent: d3.extent( data, getX ) },
y : { extent: d3.extent( data, getY ) }
};
//TODO: set pan/zoom limits
// from http://stackoverflow.com/questions/10422738/limiting-domain-when-zooming-or-panning-in-d3-js
//self.x.domain([Math.max(self.x.domain()[0], self.options.xmin), Math.min(self.x.domain()[1], self.options.xmax)]);
//self.y.domain([Math.max(self.y.domain()[0], self.options.ymin), Math.min(self.y.domain()[1], self.options.ymax)]);
var interpolaterFns = {
x : d3.scale.linear()
.domain( stats.x.extent )
.range([ 0, config.width ]),
y : d3.scale.linear()
.domain( stats.y.extent )
.range([ config.height, 0 ])
};
// ........................................................ initial element creation
this.adjustChartDimensions = function( top, right, bottom, left ){
//this.log( this + '.adjustChartDimensions', arguments );
top = top || 0;
right = right || 0;
bottom = bottom || 0;
left = left || 0;
this.svg
.attr( "width", this.config.width + ( this.config.marginRight + right )
+ ( this.config.marginLeft + left ) )
.attr( "height", this.config.height + ( this.config.marginTop + top )
+ ( this.config.marginBottom + bottom ) )
// initial is hidden - show it
.style( 'display', 'block' );
// move content group away from margins
//TODO: allow top, right axis
this.content = this.svg.select( "g.content" )
.attr( "transform", this.translateStr( this.config.marginLeft + left, this.config.marginTop + top ) );
};
// ........................................................ data and scales
this.preprocessData = function( data, min, max ){
//this.log( this + '.preprocessData', arguments );
//TODO: filter by min, max if set
// set a cap on the data, limit to first n points
return ( data.length > this.config.maxDataPoints )? ( data.slice( 0, this.config.maxDataPoints ) ): ( data );
};
this.findMinMaxes = function( xCol, yCol, meta ){
//this.log( this + '.findMinMaxes', arguments );
// configuration takes priority, otherwise meta (from the server) if passed, last-resort: compute it here
this.xMin = this.config.xMin || ( meta )?( meta[0].min ):( d3.min( xCol ) );
this.xMax = this.config.xMax || ( meta )?( meta[0].max ):( d3.max( xCol ) );
this.yMin = this.config.yMin || ( meta )?( meta[1].min ):( d3.min( yCol ) );
this.yMax = this.config.yMax || ( meta )?( meta[1].max ):( d3.max( yCol ) );
};
this.setUpScales = function(){
//this.log( this + '.setUpScales', arguments );
// Interpolation for x, y based on data domains
this.xScale = d3.scale.linear()
.domain([ this.xMin, this.xMax ])
.range([ 0, this.config.width ]),
this.yScale = d3.scale.linear()
.domain([ this.yMin, this.yMax ])
.range([ this.config.height, 0 ]);
};
// ........................................................ axis and ticks
this.setUpXAxis = function(){
//this.log( this + '.setUpXAxis', arguments );
// origin: bottom, left
//TODO: incoporate top, right
this.xAxisFn = d3.svg.axis()
.scale( this.xScale )
.ticks( this.config.xNumTicks )
.orient( 'bottom' );
this.xAxis// = content.select( 'g#x-axis' )
.attr( 'transform', this.translateStr( 0, this.config.height ) )
.call( this.xAxisFn );
//this.log( 'xAxis:', this.xAxis );
//TODO: adjust ticks when tick labels are long - move odds down and extend tick line
// (for now) hide them
var xLongestTickLabel = d3.max( _.map( [ this.xMin, this.xMax ],
function( number ){ return ( String( number ) ).length; } ) );
//this.log( 'xLongestTickLabel:', xLongestTickLabel );
if( xLongestTickLabel >= X_LABEL_TOO_LONG_AT ){
this.xAxis.selectAll( 'g' ).filter( ':nth-child(odd)' ).style( 'display', 'none' );
}
this.log( 'this.config.xLabel:', this.config.xLabel );
this.xAxisLabel// = xAxis.select( 'text#x-axis-label' )
.attr( 'x', this.config.width / 2 )
.attr( 'y', this.config.xAxisLabelBumpY )
.attr( 'text-anchor', 'middle' )
.text( this.config.xLabel );
this.log( 'xAxisLabel:', this.xAxisLabel );
// .................................................................... main components
var zoom = d3.behavior.zoom()
.x( interpolaterFns.x )
.y( interpolaterFns.y )
.scaleExtent([ 1, 10 ]);
//TODO: you can prog. set the zoom and pan with zoom.scale( val ) and zoom.translate([ x, y ])...
//console.debug( renderTo );
var svg = d3.select( renderTo )
.attr( "class", "scatterplot" )
//.attr( "width", config.width + ( config.margin.right + config.margin.left ) )
.attr( "width", '100%' )
.attr( "height", config.height + ( config.margin.top + config.margin.bottom ) );
var content = svg.append( "g" )
.attr( "class", "content" )
.attr( "transform", translateStr( config.margin.left, config.margin.top ) )
.call( zoom );
// a BIG gotcha - zoom (or any mouse/touch event in SVG?) requires the pointer to be over an object
// create a transparent rect to be that object here
content.append( 'rect' )
.attr( "class", "zoom-rect" )
.attr( "width", config.width ).attr( "height", config.height )
.style( "fill", "transparent" );
//console.log( 'svg:', svg, 'content:', content );
// .................................................................... axes
var axis = { x : {}, y : {} };
//console.log( 'x.ticks:', config.x.ticks );
//console.log( 'y.ticks:', config.y.ticks );
axis.x.fn = d3.svg.axis()
.orient( 'bottom' )
.scale( interpolaterFns.x )
.ticks( config.x.ticks )
// this will convert thousands -> k, millions -> M, etc.
.tickFormat( d3.format( 's' ) );
axis.y.fn = d3.svg.axis()
.orient( 'left' )
.scale( interpolaterFns.y )
.ticks( config.y.ticks )
.tickFormat( d3.format( 's' ) );
axis.x.g = content.append( 'g' )
.attr( 'class', 'x axis' )
.attr( 'transform', translateStr( 0, config.height ) )
.call( axis.x.fn );
//console.log( 'axis.x.g:', axis.x.g );
axis.y.g = content.append( 'g' )
.attr( 'class', 'y axis' )
.call( axis.y.fn );
//console.log( 'axis.y.g:', axis.y.g );
// ................................ axis labels
var padding = 4;
// x-axis label
axis.x.label = svg.append( 'text' )
.attr( 'class', 'axis-label' )
.text( config.x.label )
// align to the top-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-after-edge' )
.attr( 'x', ( config.width / 2 ) + config.margin.left )
// place 4 pixels below the axis bounds
.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
// y-axis label
// place 4 pixels left of the axis.y.g left edge
axis.y.label = svg.append( 'text' )
.attr( 'class', 'axis-label' )
.text( config.y.label )
// align to bottom-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-before-edge' )
.attr( 'x', padding )
.attr( 'y', config.height / 2 )
// rotate around the alignment point
.attr( 'transform', rotateStr( -90, padding, config.height / 2 ) );
//console.log( 'axis.y.label:', axis.y.label );
axis.redraw = function _redrawAxis(){
svg.select( ".x.axis" ).call( axis.x.fn );
svg.select( ".y.axis" ).call( axis.y.fn );
};
this.setUpYAxis = function(){
//this.log( this + '.setUpYAxis', arguments );
this.yAxisFn = d3.svg.axis()
.scale( this.yScale )
.ticks( this.config.yNumTicks )
.orient( 'left' );
this.yAxis// = content.select( 'g#y-axis' )
.call( this.yAxisFn );
//this.log( 'yAxis:', this.yAxis );
// a too complicated section for increasing the left margin when tick labels are long
// get the tick labels for the y axis
var yTickLabels = this.yAxis.selectAll( 'text' ).filter( function( e, i ){ return i !== 0; } );
this.log( 'yTickLabels:', yTickLabels );
// get the longest label length (or 0 if no labels)
this.yLongestLabel = d3.max(
//NOTE: d3 returns an nested array - use the plain array inside ([0])
yTickLabels[0].map( function( e, i ){
return ( d3.select( e ).text() ).length;
})
) || 0;
//this.log( 'yLongestLabel:', this.yLongestLabel );
//TODO: lose the guessing if possible
var neededY = TICK_LINE_AND_PADDING + ( this.yLongestLabel * GUESS_AT_SVG_CHAR_WIDTH )
+ PADDING + GUESS_AT_SVG_CHAR_HEIGHT;
//this.log( 'neededY:', neededY );
// increase width for yLongerStr, increase margin for y
//TODO??: (or transform each number: 2k)
this.config.yAxisLabelBumpX = -( neededY - GUESS_AT_SVG_CHAR_HEIGHT );
if( this.config.marginLeft < neededY ){
var adjusting = ( neededY ) - this.config.marginLeft;
adjusting = ( adjusting < 0 )?( 0 ):( adjusting );
//this.log( 'adjusting:', adjusting );
// update dimensions, translations
this.adjustChartDimensions( 0, 0, 0, adjusting );
}
//this.log( 'this.config.yAxisLableBumpx, this.config.marginLeft:',
// this.config.yAxisLabelBumpX, this.config.marginLeft );
this.yAxisLabel// = yAxis.select( 'text#y-axis-label' )
.attr( 'x', this.config.yAxisLabelBumpX )
.attr( 'y', this.config.height / 2 )
.attr( 'text-anchor', 'middle' )
.attr( 'transform', this.rotateStr( -90, this.config.yAxisLabelBumpX, this.config.height / 2 ) )
.text( this.config.yLabel );
//this.log( 'yAxisLabel:', this.yAxisLabel );
};
// ........................................................ grid lines
this.renderGrid = function(){
//this.log( this + '.renderGrid', arguments );
// VERTICAL
// select existing
this.vGridLines = this.content.selectAll( 'line.v-grid-line' )
.data( this.xScale.ticks( this.xAxisFn.ticks()[0] ) );
// append any extra lines needed (more ticks)
this.vGridLines.enter().append( 'svg:line' )
// .................................................................... grid
function renderGrid(){
var grid = { v : {}, h: {} };
// vertical
grid.v.lines = content.selectAll( 'line.v-grid-line' )
// data are the axis ticks; enter, update, exit
.data( interpolaterFns.x.ticks( axis.x.fn.ticks()[0] ) );
// enter: append any extra lines needed (more ticks)
grid.v.lines.enter()
.append( 'svg:line' )
.classed( 'grid-line v-grid-line', true );
// update the attributes of existing and appended
this.vGridLines
.attr( 'x1', this.xScale )
// update: set coords
grid.v.lines
.attr( 'x1', interpolaterFns.x )
.attr( 'x2', interpolaterFns.x )
.attr( 'y1', 0 )
.attr( 'x2', this.xScale )
.attr( 'y2', this.config.height );
// remove unneeded (less ticks)
this.vGridLines.exit().remove();
//this.log( 'vGridLines:', this.vGridLines );
.attr( 'y2', config.height );
// exit: just remove them
grid.v.lines.exit().remove();
//console.log( 'grid.v.lines:', grid.v.lines );
// HORIZONTAL
this.hGridLines = this.content.selectAll( 'line.h-grid-line' )
.data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) );
this.hGridLines.enter().append( 'svg:line' )
// horizontal
grid.h.lines = content.selectAll( 'line.h-grid-line' )
.data( interpolaterFns.y.ticks( axis.y.fn.ticks()[0] ) );
grid.h.lines.enter()
.append( 'svg:line' )
.classed( 'grid-line h-grid-line', true );
this.hGridLines
grid.h.lines
.attr( 'x1', 0 )
.attr( 'y1', this.yScale )
.attr( 'x2', this.config.width )
.attr( 'y2', this.yScale );
this.hGridLines.exit().remove();
//this.log( 'hGridLines:', this.hGridLines );
};
// ........................................................ data points
this.renderDatapoints = function( xCol, yCol, ids ){
this.log( this + '.renderDatapoints', arguments );
var count = 0,
plot = this,
xPosFn = function( d, i ){
//if( d ){ this.log( 'x.data:', newXCol[ i ], 'plotted:', plot.xScale( newXCol[ i ] ) ); }
return plot.xScale( xCol[ i ] );
},
yPosFn = function( d, i ){
//if( d ){ this.log( 'y.data:', newYCol[ i ], 'plotted:', plot.yScale( newYCol[ i ] ) ); }
return plot.yScale( yCol[ i ] );
};
.attr( 'x2', config.width )
.attr( 'y1', interpolaterFns.y )
.attr( 'y2', interpolaterFns.y );
grid.h.lines.exit().remove();
//console.log( 'grid.h.lines:', grid.h.lines );
return grid;
}
var grid = renderGrid();
//this.datapoints = this.addDatapoints( xCol, yCol, ids, ".glyph" );
var datapoints = this.content.selectAll( '.glyph' ).data( xCol );
//// .................................................................... datapoints
var datapoints = content.selectAll( '.glyph' ).data( data )
// enter - NEW data to be added as glyphs
.enter().append( 'svg:circle' )
.classed( "glyph", true )
.attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); })
// give them a 'entry' position and style
.attr( "cy", config.height )
.attr( "r", 0 );
// enter - NEW data to be added as glyphs: give them a 'entry' position and style
count = 0;
datapoints.enter()
.append( 'svg:circle' )
.each( function(){ count += 1; } )
.classed( "glyph", true )
.attr( "cx", 0 )
.attr( "cy", this.config.height )
// start all bubbles small...
.attr( "r", 0 );
this.log( count, ' new glyphs created' );
// for all EXISTING glyphs and those that need to be added: transition anim to final state
count = 0;
datapoints
// ...animate to final position
.transition().duration( this.config.animDuration )
.each( function(){ count += 1; } )
.attr( "cx", xPosFn )
.attr( "cy", yPosFn )
.attr( "r", plot.config.datapointSize );
this.log( count, ' existing glyphs transitioned' );
// events
// glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM
datapoints.exit()
.each( function(){ count += 1; } )
.transition().duration( this.config.animDuration )
.attr( "cy", this.config.height )
.attr( "r", 0 )
.remove();
this.log( count, ' glyphs removed' );
// for all EXISTING glyphs and those that need to be added: transition anim to final state
datapoints.transition().duration( config.animDuration )
.attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); })
.attr( "r", config.datapointSize );
//console.log( 'datapoints:', datapoints );
this._addDatapointEventhandlers( datapoints, xCol, yCol, ids );
};
function _redrawDatapointsClipped(){
return datapoints
.attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); })
.attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); })
.style( 'display', 'block' )
// filter out points now outside the graph content area and hide them
.filter( function( d, i ){
var cx = d3.select( this ).attr( "cx" ),
cy = d3.select( this ).attr( "cy" );
if( cx < 0 || cx > config.width ){ return true; }
if( cy < 0 || cy > config.height ){ return true; }
return false;
}).style( 'display', 'none' );
}
this._addDatapointEventhandlers = function( datapoints, xCol, yCol, ids ){
var plot = this;
datapoints
//TODO: remove magic numbers
.on( 'mouseover', function( d, i ){
var datapoint = d3.select( this );
datapoint
.style( 'fill', 'red' )
.style( 'fill-opacity', 1 );
// create horiz, vert lines to axis
plot.content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
// start not at center, but at the edge of the circle - to prevent mouseover thrashing
.attr( 'x1', datapoint.attr( 'cx' ) - plot.config.datapointSize )
.attr( 'y1', datapoint.attr( 'cy' ) )
.attr( 'x2', 0 )
.attr( 'y2', datapoint.attr( 'cy' ) )
.classed( 'hoverline', true );
// .................................................................... behaviors
function zoomed( scale, translateX, translateY ){
//console.debug( 'zoom', this, scale, translateX, translateY, arguments );
// re-render axis, grid, and datapoints
axis.redraw();
_redrawDatapointsClipped();
grid = renderGrid();
$( '.chart-info-box' ).remove();
$( svg.node() ).trigger( 'zoom.scatterplot', [] );
}
//TODO: programmatically set zoom/pan and save in config
//TODO: set pan/zoom limits
zoom.on( "zoom", zoomed );
// if the vertical hoverline
if( datapoint.attr( 'cy' ) < plot.config.height ){
plot.content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
.attr( 'x1', datapoint.attr( 'cx' ) )
.attr( 'y1', datapoint.attr( 'cy' ) + plot.config.datapointSize )
.attr( 'x2', datapoint.attr( 'cx' ) )
.attr( 'y2', plot.config.height )
.classed( 'hoverline', true );
}
var datapointWindowPos = $( this ).offset();
plot.datapointInfoBox = plot.infoBox(
datapointWindowPos.top, datapointWindowPos.left,
plot.infoHtml( xCol[ i ], yCol[ i ], ( ids )?( ids[ i ] ):( undefined ) )
);
$( 'body' ).append( plot.datapointInfoBox );
})
.on( 'mouseout', function(){
d3.select( this )
.style( 'fill', 'black' )
.style( 'fill-opacity', 0.2 );
plot.content.selectAll( '.hoverline' ).remove();
if( plot.datapointInfoBox ){
plot.datapointInfoBox.remove();
}
});
},
this.render = function( columnData, meta ){
this.log( this + '.render', arguments );
this.log( '\t config:', this.config );
function infoBox( top, left, d ){
// create an abs pos. element containing datapoint data (d) near the point (top, left)
// with added padding to clear the mouse pointer
left += 8;
return $([
'<div class="chart-info-box" style="position: absolute">',
(( config.idColumn )?( '<div>' + d[ config.idColumn ] + '</div>' ):( '' )),
'<div>', getX( d ), '</div>',
'<div>', getY( d ), '</div>',
'</div>'
].join( '' ) ).css({ top: top, left: left, 'z-index': 2 });
}
// prepare the data
//pre: columns passed are numeric
//pre: at least two columns are passed
//assume: first column is x, second column is y, any remaining aren't used
var xCol = columnData[0],
yCol = columnData[1],
ids = ( columnData.length > 2 )?( columnData[2] ):( undefined );
//this.log( this + '.render', xCol.length, yCol.length, this.config );
//pre: xCol.len == yCol.len
xCol = this.preprocessData( xCol );
yCol = this.preprocessData( yCol );
this.log( 'xCol len', xCol.length, 'yCol len', yCol.length );
this.findMinMaxes( xCol, yCol, meta );
//this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax );
this.setUpScales();
datapoints.on( 'mouseover', function( d, i ){
var datapoint = d3.select( this );
datapoint
.style( 'fill', 'red' )
.style( 'fill-opacity', 1 );
// find (or build if it doesn't exist) the svg dom infrastructure
if( !this.svg ){ this.svg = d3.select( 'svg' ).attr( "class", "chart" ); }
if( !this.content ){
this.content = this.svg.append( "svg:g" ).attr( "class", "content" ).attr( 'id', this.config.id );
// create horiz line to axis
content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
// start not at center, but at the edge of the circle - to prevent mouseover thrashing
.attr( 'x1', datapoint.attr( 'cx' ) - config.datapointSize )
.attr( 'y1', datapoint.attr( 'cy' ) )
.attr( 'x2', 0 )
.attr( 'y2', datapoint.attr( 'cy' ) )
.classed( 'hoverline', true );
// create vertical line to axis - if not on the x axis
if( datapoint.attr( 'cy' ) < config.height ){
content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
.attr( 'x1', datapoint.attr( 'cx' ) )
// attributes are strings so, (accrd. to js) '3' - 1 = 2 but '3' + 1 = '31': coerce
.attr( 'y1', +datapoint.attr( 'cy' ) + config.datapointSize )
.attr( 'x2', datapoint.attr( 'cx' ) )
.attr( 'y2', config.height )
.classed( 'hoverline', true );
}
//this.log( 'svg:', this.svg );
//this.log( 'content:', this.content );
this.adjustChartDimensions();
if( !this.xAxis ){ this.xAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' ); }
if( !this.xAxisLabel ){
this.xAxisLabel = this.xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' );
}
//this.log( 'xAxis:', this.xAxis, 'xAxisLabel:', this.xAxisLabel );
if( !this.yAxis ){ this.yAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' ); }
if( !this.yAxisLabel ){
this.yAxisLabel = this.yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' );
}
//this.log( 'yAxis:', this.yAxis, 'yAxisLabel:', this.yAxisLabel );
this.setUpXAxis();
this.setUpYAxis();
this.renderGrid();
this.renderDatapoints( xCol, yCol, ids );
};
this.infoHtml = function( x, y, id ){
var retDiv = $( '<div/>' );
if( id ){
$( '<div/>' ).text( id ).css( 'font-weight', 'bold' ).appendTo( retDiv );
}
$( '<div/>' ).text( x ).appendTo( retDiv );
$( '<div/>' ).text( y ).appendTo( retDiv );
return retDiv.html();
};
//TODO: html for now
this.infoBox = function( top, left, html, adjTop, adjLeft ){
adjTop = adjTop || 0;
adjLeft = adjLeft || 20;
var infoBox = $( '<div />' )
.addClass( 'chart-info-box' )
.css({
'position' : 'absolute',
'top' : top + adjTop,
'left' : left + adjLeft
});
infoBox.html( html );
return infoBox;
};
// show the info box and trigger an event
var bbox = this.getBoundingClientRect();
$( 'body' ).append( infoBox( bbox.top, bbox.right, d ) );
$( svg.node() ).trigger( 'mouseover-datapoint.scatterplot', [ this, d, i ] );
});
datapoints.on( 'mouseout', function(){
// return the point to normal, remove hoverlines and info box
d3.select( this )
.style( 'fill', 'black' )
.style( 'fill-opacity', 0.2 );
content.selectAll( '.hoverline' ).remove();
$( '.chart-info-box' ).remove();
});
}
//==============================================================================
@@ -1,631 +0,0 @@
/* =============================================================================
todo:
I'd like to move the svg creation out of the splot constr. to:
allow adding splots to an existing canvas
allow mult. splots sharing a canvas
outside this:
BUG: setting width, height in plot controls doesn't re-interpolate data locations!!
BUG?: get metadata_column_names (from datatype if necessary)
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
wire label setters, anim setter
TwoVarScatterplot:
??: maybe better to do this with a canvas...
save as visualization
to seperate file?
remove underscore dependencies
add interface to change values (seperate)?
download svg -> base64 encode
incorporate glyphs, glyph state renderers
ScatterplotSettingsForm:
some css bug that lowers the width of settings form when plot-controls tab is open
causes chart to shift
what can be abstracted/reused for other graphs?
avoid direct manipulation of this.plot
allow option to put plot into seperate tab of interface (for small multiples)
provide callback in view to load data incrementally - for large sets
paginate
handle rerender
use endpoint (here and on the server (fileptr))
fetch (new?) data
handle rerender
use d3.TSV?
render warning on long data (> maxDataPoints)
adjust endpoint
selectable list of preset column comparisons (rnaseq etc.)
how to know what sort of Tabular the data is?
smarter about headers
validate columns selection (here or server)
set stats column names by selected columns
move chart into tabbed area...
Scatterplot.mako:
multiple plots on one page (small multiples)
?? ensure svg styles thru d3 or css?
d3: configable (easily)
css: standard - better maintenance
? override at config
============================================================================= */
/**
* Scatterplot control UI as a backbone view
* handles:
* getting the desired data
* configuring the plot display
* showing (general) statistics
*
* initialize attributes REQUIRES a dataset and an apiDatasetsURL
*/
var ScatterplotControlForm = BaseView.extend( LoggableMixin ).extend({
//logger : console,
className : 'scatterplot-control-form',
//NOTE: should include time needed to render
dataLoadDelay : 4000,
dataLoadSize : 5000,
loadingIndicatorImage : 'loading_small_white_bg.gif',
fetchMsg : 'Fetching data...',
renderMsg : 'Rendering...',
initialize : function( attributes ){
this.log( this + '.initialize, attributes:', attributes );
this.dataset = null;
this.chartConfig = null;
this.chart = null;
this.loader = null;
// set up refs to the four tab areas
this.$dataControl = null;
this.$chartControl = null;
this.$statsDisplay = null;
this.$chartDisplay = null;
this.dataFetch = null;
this.initializeFromAttributes( attributes );
this.initializeChart( attributes );
this.initializeDataLoader( attributes );
},
initializeFromAttributes : function( attributes ){
// required settings: ensure certain vars we need are passed in attributes
if( !attributes || !attributes.dataset ){
throw( "ScatterplotView requires a dataset" );
} else {
this.dataset = attributes.dataset;
}
if( jQuery.type( this.dataset.metadata_column_types ) === 'string' ){
this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' );
}
this.log( '\t dataset:', this.dataset );
// attempt to get possible headers from the data's first line
if( this.dataset.comment_lines && this.dataset.comment_lines.length ){
//TODO:??
var firstLine = this.dataset.comment_lines[0],
possibleHeaders = firstLine.split( '\t' );
if( possibleHeaders.length === this.dataset.metadata_column_types.length ){
this.possibleHeaders = possibleHeaders;
}
}
// passed from mako helper
//TODO: integrate to galaxyPaths
//TODO: ?? seems like data loader section would be better
if( !attributes.apiDatasetsURL ){
throw( "ScatterplotView requires a apiDatasetsURL" );
} else {
this.dataURL = attributes.apiDatasetsURL + '/' + this.dataset.id + '?';
}
this.log( '\t dataURL:', this.dataURL );
},
initializeChart : function( attributes ){
// set up the basic chart infrastructure and config (if any)
this.chartConfig = attributes.chartConfig || {};
//if( this.logger ){ this.chartConfig.debugging = true; }
this.log( '\t initial chartConfig:', this.chartConfig );
this.chart = new TwoVarScatterplot( this.chartConfig );
//TODO: remove 2nd ref, use this.chart.config
this.chartConfig = this.chart.config;
},
initializeDataLoader : function( attributes ){
// set up data loader
var view = this;
this.loader = new LazyDataLoader({
//logger : ( this.logger )?( this.logger ):( null ),
// we'll generate this when columns are chosen
url : null,
start : attributes.start || 0,
//NOTE: metadata_data_lines can be null (so we won't know the total)
total : attributes.total || this.dataset.metadata_data_lines,
delay : this.dataLoadDelay,
size : this.dataLoadSize,
buildUrl : function( start, size ){
// currently VERY SPECIFIC to using data_providers.py start_val, max_vals params
return this.url + '&' + jQuery.param({
start_val: start,
max_vals: size
});
}
});
$( this.loader ).bind( 'error', function( event, status, error ){
view.log( 'ERROR:', status, error );
alert( 'ERROR fetching data:\n' + status + '\n' + error );
view.hideLoadingIndicator();
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
this.log( this + '.render' );
// render the tab controls, areas and loading indicator
this.$el.append( ScatterplotControlForm.templates.mainLayout({
loadingIndicatorImagePath : '/static/images/' + this.loadingIndicatorImage,
message : ''
}));
// render the tab content
this.$dataControl = this._render_dataControl();
this.$chartControl = this._render_chartControl();
this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' );
this.$chartDisplay = this._render_chartDisplay();
// auto render if given both x, y column choices in query for page
//TODO:?? add autoRender=1 to query maybe?
if( this.chartConfig.xColumn && this.chartConfig.yColumn ){
this.renderChart();
}
// set up behaviours
this.$el.find( '.tooltip' ).tooltip();
// uncomment any of the following to have that tab show on initial load (for testing)
//this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
return this;
},
_render_dataControl : function(){
// controls for which columns are used to plot datapoints (and ids/additional info to attach if desired)
var view = this,
allColumns = [],
numericColumns = [],
usePossibleHeaders = ( this.possibleHeaders && this.$dataControl )?
( this.$dataControl.find( '#first-line-header-checkbox' ).is( ':checked' ) ):( false );
// gather column indeces (from metadata_column_types) and names (from metadata_columnnames)
_.each( this.dataset.metadata_column_types, function( type, index ){
// use a 1 based index in names/values within the form (will be dec. when parsed out)
var oneBasedIndex = index + 1,
// default name is 'column <index>'...
name = 'column ' + oneBasedIndex;
// ...but label with the name if available...
if( view.dataset.metadata_column_names ){
name = view.dataset.metadata_column_names[ index ];
// ...or, use the first line as headers if the user wants
} else if( usePossibleHeaders ){
name = view.possibleHeaders[ index ];
}
// cache all columns here
allColumns.push({ index: oneBasedIndex, name: name });
// filter numeric columns to their own list
if( type === 'int' || type === 'float' ){
numericColumns.push({ index: oneBasedIndex, name: name });
}
});
//TODO: other vals: max_vals, start_val, pagination (chart-settings)
// render the html
var $dataControl = this.$el.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotControlForm.templates.dataControl({
allColumns : allColumns,
numericColumns : numericColumns,
possibleHeaders : ( this.possibleHeaders )?( this.possibleHeaders.join( ', ' ) ):( '' ),
usePossibleHeaders : usePossibleHeaders
}));
if( !this.dataset.metadata_column_names && this.possibleHeaders ){
$dataControl.find( '#first-line-header' ).show();
}
// preset to column selectors if they were passed in the config in the query string
$dataControl.find( '#X-select' ).val( this.chartConfig.xColumn );
$dataControl.find( '#Y-select' ).val( this.chartConfig.yColumn );
if( this.chartConfig.idColumn !== undefined ){
$dataControl.find( '#include-id-checkbox' )
.attr( 'checked', true ).trigger( 'change' );
$dataControl.find( '#ID-select' ).val( this.chartConfig.idColumn );
}
return $dataControl;
},
_render_chartControl : function(){
// tab content to control how the chart is rendered (data glyph size, chart size, etc.)
var view = this,
$chartControl = this.$el.find( '.tab-pane#chart-control' ),
// limits for controls (by control/chartConfig id)
//TODO: move into TwoVarScatterplot
controlRanges = {
'datapointSize' : { min: 2, max: 10, step: 1 },
'width' : { min: 200, max: 800, step: 20 },
'height' : { min: 200, max: 800, step: 20 }
};
// render the html
$chartControl.append( ScatterplotControlForm.templates.chartControl( this.chartConfig ) );
// set up behaviours, js on sliders
$chartControl.find( '.numeric-slider-input' ).each( function(){
var $this = $( this ),
$output = $this.find( '.slider-output' ),
$slider = $this.find( '.slider' ),
id = $this.attr( 'id' );
//chartControl.log( 'slider set up', 'this:', $this, 'slider:', $slider, 'id', id );
// what to do when the slider changes: update display and update chartConfig
//TODO: move out of loop
function onSliderChange(){
var $this = $( this ),
newValue = $this.slider( 'value' );
//chartControl.log( 'slider change', 'this:', $this, 'output:', $output, 'value', newValue );
$output.text( newValue );
//chartControl.chartConfig[ id ] = newValue;
}
$slider.slider( _.extend( controlRanges[ id ], {
value : view.chartConfig[ id ],
change : onSliderChange,
slide : onSliderChange
}));
});
return $chartControl;
},
_render_chartDisplay : function(){
// render the tab content where the chart is displayed (but not the chart itself)
var $chartDisplay = this.$el.find( '.tab-pane#chart-display' );
$chartDisplay.append( ScatterplotControlForm.templates.chartDisplay( this.chartConfig ) );
return $chartDisplay;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'change #first-line-header-checkbox' : 'rerenderDataControl',
'click #data-control #render-button' : 'renderChart',
'click #chart-control #render-button' : 'changeChartSettings'
},
toggleThirdColumnSelector : function(){
// show/hide the id selector on the data settings panel
this.$el.find( 'select[name="ID"]' ).parent().toggle();
},
rerenderDataControl : function(){
this.$dataControl = this._render_dataControl();
},
showLoadingIndicator : function( message, callback ){
// display the loading indicator over the tab panels if hidden, update message (if passed)
message = message || '';
var indicator = this.$el.find( 'div#loading-indicator' );
messageBox = indicator.find( '.loading-message' );
if( indicator.is( ':visible' ) ){
if( message ){
messageBox.fadeOut( 'fast', function(){
messageBox.text( message );
messageBox.fadeIn( 'fast', callback );
});
} else {
callback();
}
} else {
if( message ){ messageBox.text( message ); }
indicator.fadeIn( 'fast', callback );
}
},
hideLoadingIndicator : function( callback ){
this.$el.find( 'div#loading-indicator' ).fadeOut( 'fast', callback );
},
// ------------------------------------------------------------------------- CHART/STATS RENDERING
renderChart : function(){
// fetch the data, (re-)render the chart
this.log( this + '.renderChart' );
//TODO: separate data fetch
// this is a complete re-render, so clear the prev. data
this.data = null;
this.meta = null;
// update the chartConfig (here and chart) using chart settings
//TODO: separate and improve (used in changeChartSettings too)
_.extend( this.chartConfig, this.getChartSettings() );
this.log( '\t chartConfig:', this.chartConfig );
this.chart.updateConfig( this.chartConfig, false );
// build the url with the current data settings
this.loader.url = this.dataURL + '&' + jQuery.param( this.getDataSettings() );
this.log( '\t loader: total lines:', this.loader.total, ' url:', this.loader.url );
// bind the new data event to: aggregate data, update the chart and stats with new data
var view = this;
$( this.loader ).bind( 'loaded.new', function( event, response ){
view.log( view + ' loaded.new', response );
// aggregate data and meta
view.postProcessDataFetchResponse( response );
view.log( '\t postprocessed data:', view.data );
view.log( '\t postprocessed meta:', view.meta );
// update the chart and stats
view.showLoadingIndicator( view.renderMsg, function(){
view.chart.render( view.data, view.meta );
view.renderStats( view.data, view.meta );
view.hideLoadingIndicator();
});
});
// when all data loaded - unbind (or we'll start doubling event handlers)
$( this.loader ).bind( 'complete', function( event, data ){
view.log( view + ' complete', data );
$( view.loader ).unbind();
});
// begin loading the data, switch to the chart display tab
view.showLoadingIndicator( view.fetchMsg, function(){
view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
view.loader.load();
});
},
renderStats : function(){
this.log( this + '.renderStats' );
// render the stats table in the stats panel
//TODO: there's a better way
this.$statsDisplay.html( ScatterplotControlForm.templates.statsDisplay({
stats: [
{ name: 'Count', xval: this.meta[0].count, yval: this.meta[1].count },
{ name: 'Min', xval: this.meta[0].min, yval: this.meta[1].min },
{ name: 'Max', xval: this.meta[0].max, yval: this.meta[1].max },
{ name: 'Sum', xval: this.meta[0].sum, yval: this.meta[1].sum },
{ name: 'Mean', xval: this.meta[0].mean, yval: this.meta[1].mean },
{ name: 'Median', xval: this.meta[0].median, yval: this.meta[1].median }
]
}));
},
changeChartSettings : function(){
// re-render the chart with new chart settings and OLD data
var view = this;
newChartSettings = this.getChartSettings();
// update the chart config from the chartSettings panel controls
_.extend( this.chartConfig, newChartSettings );
this.log( 'this.chartConfig:', this.chartConfig );
this.chart.updateConfig( this.chartConfig, false );
// if there's current data, call chart.render with it (no data fetch)
if( view.data && view.meta ){
view.showLoadingIndicator( view.renderMsg, function(){
view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
view.chart.render( view.data, view.meta );
view.hideLoadingIndicator();
});
// no current data, call renderChart instead (which will fetch data)
} else {
this.renderChart();
}
},
// ------------------------------------------------------------------------- DATA AGGREGATION
postProcessDataFetchResponse : function( response ){
// the loader only returns new data - it's up to this to munge the fetches together properly
//TODO: we're now storing data in two places: loader and here
// can't we reduce incoming data into loader.data[0]? are there concurrency problems?
this.postProcessData( response.data );
this.postProcessMeta( response.meta );
},
postProcessData : function( newData ){
// stack the column data on top of each other into this.data
//this.log( this + '.postProcessData:', newData );
var view = this;
// if we already have data: aggregate
if( view.data ){
_.each( newData, function( newColData, colIndex ){
//view.log( colIndex + ' data:', newColData );
//TODO??: time, space efficiency of this?
view.data[ colIndex ] = view.data[ colIndex ].concat( newColData );
});
// otherwise: assign (first load)
} else {
view.data = newData;
}
},
postProcessMeta : function( newMeta ){
// munge the meta data (stats) from the server fetches together
//pre: this.data must be preprocessed (needed for medians)
//this.log( this + '.postProcessMeta:', newMeta );
var view = this,
colTypes = this.dataset.metadata_column_types;
// if we already have meta: aggregate
if( view.meta ){
_.each( newMeta, function( newColMeta, colIndex ){
var colMeta = view.meta[ colIndex ],
colType = colTypes[ colIndex ];
//view.log( '\t ' + colIndex + ' postprocessing meta:', newColMeta );
//view.log( colIndex + ' old meta:',
// 'min:', colMeta.min,
// 'max:', colMeta.max,
// 'sum:', colMeta.sum,
// 'mean:', colMeta.mean,
// 'median:', colMeta.median
//);
//!TODO: at what point are we getting int/float overflow on these?!
//??: need to be null safe?
colMeta.count += ( newColMeta.count )?( newColMeta.count ):( 0 );
//view.log( colIndex, 'count:', colMeta.count );
if( ( colType === 'int' ) || ( colType === 'float' ) ){
//view.log( colIndex + ' incoming meta:',
// 'min:', newColMeta.min,
// 'max:', newColMeta.max,
// 'sum:', newColMeta.sum,
// 'mean:', newColMeta.mean,
// 'median:', newColMeta.median
//);
colMeta.min = Math.min( newColMeta.min, colMeta.min );
colMeta.max = Math.max( newColMeta.max, colMeta.max );
colMeta.sum = newColMeta.sum + colMeta.sum;
colMeta.mean = ( colMeta.count )?( colMeta.sum / colMeta.count ):( null );
// median's a pain bc of sorting (requires the data as well)
var sortedCol = view.data[ colIndex ].slice().sort(),
middleIndex = Math.floor( sortedCol.length / 2 );
if( sortedCol.length % 2 === 0 ){
colMeta.median = ( ( sortedCol[ middleIndex ] + sortedCol[( middleIndex + 1 )] ) / 2 );
} else {
colMeta.median = sortedCol[ middleIndex ];
}
//view.log( colIndex + ' new meta:',
// 'min:', colMeta.min,
// 'max:', colMeta.max,
// 'sum:', colMeta.sum,
// 'mean:', colMeta.mean,
// 'median:', colMeta.median
//);
}
});
// otherwise: assign (first load)
} else {
view.meta = newMeta;
//view.log( '\t meta (first load):', view.meta );
}
},
// ------------------------------------------------------------------------- GET DATA/CHART SETTINGS
getDataSettings : function(){
// parse the column values for both indeces (for the data fetch) and names (for the chart)
var columnSelections = this.getColumnSelections(),
columns = [];
this.log( '\t columnSelections:', columnSelections );
//TODO: validate columns - minimally: we can assume either set by selectors or via a good query string
// get column indices for params, include the desired ID column (if any)
//NOTE: these are presented in human-readable 1 base index (to match the data.peek) - adjust
columns = [
columnSelections.X.colIndex - 1,
columnSelections.Y.colIndex - 1
];
if( this.$dataControl.find( '#include-id-checkbox' ).attr( 'checked' ) ){
columns.push( columnSelections.ID.colIndex - 1 );
}
//TODO: other vals: max, start, page
var params = {
data_type : 'raw_data',
provider : 'column_with_stats',
columns : '[' + columns + ']'
};
this.log( '\t data settings (url params):', params );
return params;
},
getColumnSelections : function(){
// gets the current user-selected values for which columns to fetch from the data settings panel
// returns a map: { column-select name (eg. X) : { colIndex : column-selector val,
// colName : selected option text }, ... }
var selections = {};
this.$dataControl.find( 'div.column-select select' ).each( function(){
var $this = $( this ),
val = $this.val();
selections[ $this.attr( 'name' ) ] = {
colIndex : val,
colName : $this.children( '[value="' + val + '"]' ).text()
};
});
return selections;
},
getChartSettings : function(){
// gets the user-selected chartConfig from the chart settings panel
var settings = {},
colSelections = this.getColumnSelections();
//this.log( 'colSelections:', colSelections );
//TODO: simplify with keys and loop
settings.datapointSize = this.$chartControl.find( '#datapointSize.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
settings.width = this.$chartControl.find( '#width.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
settings.height = this.$chartControl.find( '#height.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
// update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName
//TODO: a little confusing
var chartSettingsXLabel = this.$chartControl.find( 'input#X-axis-label' ).val(),
chartSettingsYLabel = this.$chartControl.find( 'input#Y-axis-label' ).val();
settings.xLabel = ( chartSettingsXLabel === 'X' )?
( colSelections.X.colName ):( chartSettingsXLabel );
settings.yLabel = ( chartSettingsYLabel === 'Y' )?
( colSelections.Y.colName ):( chartSettingsYLabel );
settings.animDuration = ( this.$chartControl.find( '#animate-chart' ).is( ':checked' ) )?
( this.chart.defaults.animDuration ):( 0 );
this.log( '\t chartSettings:', settings );
return settings;
},
toString : function(){
return 'ScatterplotControlForm(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotControlForm.templates = {
mainLayout : Templates.scatterplotControlForm,
dataControl : Templates.dataControl,
chartControl : Templates.chartControl,
statsDisplay : Templates.statsDisplay,
chartDisplay : Templates.chartDisplay
};
//==============================================================================
@@ -1,19 +1,27 @@
<script type="text/template" class="template-visualization" id="template-visualization-scatterplotControlForm">
{{! main layout }}
<h1>WHAAAAA?</h1>
<div class="scatterplot-container chart-container 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 href="#data-control" data-toggle="tab" class="tooltip"
title="Use this tab to change which data are used">Data Controls</a></li>
<li><a href="#chart-control" data-toggle="tab" class="tooltip"
title="Use this tab to change how the chart is drawn">Chart Controls</a></li>
<li><a href="#stats-display" data-toggle="tab" class="tooltip"
title="This tab will display overall statistics for your data">Statistics</a></li>
<li><a href="#chart-display" data-toggle="tab" class="tooltip"
title="This tab will display the chart">Chart</a>
{{! loading indicator - initially hidden }}
<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>
<li>
<a title="This tab will display overall statistics for your data"
href="#stats-display" data-toggle="tab">Statistics</a>
</li>
<li>
<a title="This tab will display the chart"
href="#chart-display" data-toggle="tab">Chart</a>
<div id="loading-indicator" style="display: none;">
<img class="loading-img" src="{{loadingIndicatorImagePath}}" />
<span class="loading-message">{{message}}</span>
@@ -35,12 +43,19 @@
{{! ---------------------------- tab for data statistics }}
<div id="stats-display" class="tab-pane">
{{! rendered separately }}
<p class="help-text">By column:</p>
<table id="chart-stats-table">
<thead><th></th><th>X</th><th>Y</th></thead>
{{#each stats}}
<tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr>
</tr>
{{/each}}
</table>
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="tab-pane">
{{! chart rendered separately }}
<svg width="{{width}}" height="{{height}}"></svg>
</div>
</div>{{! end .tab-content }}
@@ -0,0 +1,89 @@
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( !data || !data.length ){
return stats;
}
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' );
}
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 );
});
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 ] = [];
});
// 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( 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 ){
continue;
}
// separate the columns
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;
}
}
// get counts, mean, median
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 );
// sort columns for median
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 ];
} else {
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
@@ -1,171 +1,199 @@
/*TODO: use/move into base.less*/
* { margin: 0px; padding: 0px; }
/* -------------------------------------------- general layout */
div.tab-pane {
padding: 8px;
}
* { margin: 0; padding: 0; }
/* -------------------------------------------- header */
.header {
margin-bottom: 8px;
}
.chart-header > * { margin: 0; padding: 0; }
#chart-header {
padding : 8px;
background-color: #ebd9b2;
.chart-header {
margin-bottom: 16px;
overflow: auto;
background-color: #ebd9b2;
padding : 16px 12px 12px 12px;
}
#chart-header .subtitle {
margin: -4px 0px 0px 4px;
padding : 0;
.chart-header h2 {
margin-bottom: 4px;
}
.chart-header p {
color: white;
font-size: small;
}
/* -------------------------------------------- main layout */
#scatterplot {
/*from width + margin of chart?*/
.scatterplot-editor > * { margin: 0; padding: 0; }
.scatterplot-editor .tab-pane {
padding: 8px;
}
.scatterplot-container .tab-pane {
.scatterplot-editor .help-text,
.scatterplot-editor .help-text-small {
color: grey;
margin-bottom: 8px;
}
/* -------------------------------------------- all controls */
.scatterplot-editor .help-text {
margin-top: 4px;
}
#scatterplot input[type=button],
#scatterplot select {
.scatterplot-editor .help-text-small {
font-size: smaller;
}
/* ============================================ config controls */
.scatterplot-config-control {
max-width: 768px;
}
.scatterplot-config-control input[type="button"],
.scatterplot-config-control button,
.scatterplot-config-control select {
width: 100%;
max-width: 256px;
margin-bottom: 8px;
}
#scatterplot .help-text,
#scatterplot .help-text-small {
color: grey;
.scatterplot-config-control input[type="text"] {
width: 100%;
max-width: 512px;
border: 1px solid lightgrey;
}
#scatterplot .help-text {
padding-bottom: 16px;
.scatterplot-config-control input[type="checkbox"] {
display: inline-block;
margin-top: -3px;
}
#scatterplot .help-text-small {
padding: 4px;
font-size: smaller;
}
#scatterplot > * {
}
#scatterplot input[value=Draw] {
.scatterplot-config-control .render-button {
display: block;
margin-top: 16px;
}
#scatterplot .numeric-slider-input {
max-width: 70%;
}
/* -------------------------------------------- data controls */
/* -------------------------------------------- chart controls */
#chart-control .form-input {
/*display: table-row;*/
}
#chart-control label {
/*text-align: right;*/
.scatterplot-config-control .form-input {
margin-bottom: 8px;
/*display: table-cell;*/
}
#chart-control .slider {
/*display: table-cell;*/
.scatterplot-config-control .slider {
height: 8px;
display: block;
margin: 8px 0px 0px 8px;
margin-bottom: 8px;
}
#chart-control .slider-output {
/*display: table-cell;*/
.scatterplot-config-control .slider-output {
float: right;
}
#chart-control input[type="text"] {
border: 1px solid lightgrey;
}
/* -------------------------------------------- statistics */
#stats-display table#chart-stats-table {
width: 100%;
}
#stats-display #chart-stats-table th {
width: 30%;
padding: 4px;
font-weight: bold;
color: grey;
}
#stats-display #chart-stats-table td {
border: solid lightgrey;
border-width: 1px 0px 0px 1px;
padding: 4px;
}
#stats-display #chart-stats-table td:nth-child(1) {
border-width: 1px 0px 0px 0px;
padding-right: 1em;
text-align: right;
font-weight: bold;
color: grey;
}
/* ============================================ plot display */
/* -------------------------------------------- load indicators */
#loading-indicator {
margin: 12px 0px 0px 8px;
.scatterplot-display .loading-indicator {
position: absolute;
margin: 10px 0px 0px 10px;
color: grey;
}
#scatterplot #loading-indicator .loading-message {
.scatterplot-display .loading-indicator .loading-indicator-message {
display: inline-block;
margin-left: 4px;
font-style: italic;
font-size: smaller;
color: grey;
}
/* -------------------------------------------- display controls */
.scatterplot-display .controls {
width: 100%;
margin-bottom: 4px;
border: 1px solid lightgrey;
border-radius: 3px;
padding: 8px 8px 4px 8px;
}
.scatterplot-display .controls button {
display: inline-block;
min-width: 0;
width: auto;
margin: 0px;
}
.scatterplot-display .controls .left {
float: left;
}
.scatterplot-display .controls .pagination {
display: inline-block;
margin: 0px;
}
.scatterplot-display .controls .data-prev-next {
display: inline-block;
margin: 0px 4px 0px 0px;
}
.scatterplot-display .controls .pagination-container {
display: inline-block;
overflow: auto;
max-width: 256px;
/*very tweaky*/
margin: 0px 8px 0px 0px;
height: 27px;
padding-top: 5px;
}
.scatterplot-display .controls .pagination-container a {
display: inline;
/* WTH, bootstrap? */
position: static;
float: none;
/* up from 4px -> 5px */
padding: 5px 10px 5px 10px;
}
.scatterplot-display .controls .right {
float: right;
margin-bottom: 4px;
}
.scatterplot-display .controls p {
display: inline-block;
margin: 0px 4px 0px 4px;
/*to better match the buttons*/
vertical-align: middle;
line-height: 24px;
}
.scatterplot-display button.rerender-btn {
margin-left: 4px;
}
.scatterplot-display button.stats-toggle-btn {
margin-left: 4px;
}
/* -------------------------------------------- chart area */
#chart-holder {
overflow: auto;
margin-left: 8px;
svg.scatterplot {
border: 1px solid lightgrey;
border-radius: 3px;
}
svg .grid-line {
svg.scatterplot .grid-line {
fill: none;
stroke: lightgrey;
stroke-opacity: 0.5;
stroke-opacity: 0.2;
shape-rendering: crispEdges;
stroke-dasharray: 3, 3;
}
svg .axis path, svg .axis line {
svg.scatterplot .axis path,
svg.scatterplot .axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
svg .axis text {
font-family: monospace;
font-size: 12px;
svg.scatterplot .axis text {
font-size: 9px;
}
svg #x-axis-label, svg #y-axis-label {
svg.scatterplot .axis-label {
font-family: sans-serif;
font-size: 10px;
font-size: 13px;
}
svg .glyph {
svg.scatterplot .glyph {
stroke: none;
fill: black;
fill-opacity: 0.2;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
onmessage = function( event ){
importScripts( 'numeric-column-stats.js' );
postMessage( numericColumnStats( event.data.data, event.data.keys ) );
self.close();
};
@@ -23,45 +23,43 @@
<script type="text/javascript" src="/static/scripts/utils/LazyDataLoader.js"></script>
<script type="text/javascript" src="/static/scripts/mvc/base-mvc.js"></script>
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot.js"></script>
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot-edit.js"></script>
</head>
## ----------------------------------------------------------------------------
<body>
%if not embedded:
## dataset info: only show if on own page
<div id="chart-header" class="header">
<h2 class="title">Scatterplot of '${hda.name}'</h2>
<p class="subtitle">${hda.info}</p>
<div class="chart-header">
<h2>Scatterplot of '${hda.name}'</h2>
<p>${hda.info}</p>
</div>
%endif
<div id="scatterplot" class="scatterplot-control-form"></div>
<div class="scatterplot-editor"></div>
<script type="text/javascript">
$(function(){
<%
# optionally, bootstrap data from dprov
data = None
##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) )
%>
var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )},
data = ${h.to_json_string( data )},
querySettings = ${h.to_json_string( query_args )},
chartConfig = _.extend( querySettings, {
config = _.extend( querySettings, {
containerSelector : '#chart',
//TODO: move to ScatterplotControlForm.initialize
marginTop : ( querySettings.marginTop > 20 )?( querySettings.marginTop ):( 20 ),
xColumn : querySettings.xColumn,
yColumn : querySettings.yColumn,
idColumn : querySettings.idColumn
dataset : hda,
});
//console.debug( querySettings );
var settingsForm = new ScatterplotControlForm({
dataset : hda,
apiDatasetsURL : "${h.url_for( controller='/api/datasets', action='index' )}",
el : $( '#scatterplot' ),
chartConfig : chartConfig
var editor = new ScatterplotConfigEditor({
el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hda.id ),
config : config
}).render();
// uncomment to auto render for development
//$( '.render-button:visible' ).click();
});
</script>
%endif
</body>