mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
467 lines
18 KiB
JavaScript
467 lines
18 KiB
JavaScript
/* =============================================================================
|
|
todo:
|
|
outside this:
|
|
BUG: visualization menu doesn't disappear
|
|
BUG?: get column_names (from datatype if necessary)
|
|
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
|
|
|
|
??: maybe better to do this with a canvas...
|
|
|
|
move renderScatter plot to obj, possibly view?
|
|
|
|
fix x axis - adjust ticks when tick labels are long - move odds down and extend tick line
|
|
|
|
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
|
|
|
|
selectable list of preset column comparisons (rnaseq etc.)
|
|
multiple plots on one page (small multiples)
|
|
|
|
where are bad dataprovider params handled? api/datasets?
|
|
|
|
config changes to the graph
|
|
render stats on the data (max, min, count)
|
|
render warning on long data (> maxDataPoints)
|
|
adjust endpoint
|
|
|
|
loading indicator
|
|
|
|
download svg -> base64 encode
|
|
dress up ui
|
|
|
|
incorporate glyphs, glyph state renderers
|
|
|
|
validate columns selection (here or server)
|
|
|
|
?? 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
|
|
*/
|
|
function TwoVarScatterplot( config ){
|
|
var plot = this,
|
|
GUESS_AT_SVG_CHAR_WIDTH = 10,
|
|
GUESS_AT_SVG_CHAR_HEIGHT = 12,
|
|
PADDING = 8,
|
|
X_LABEL_TOO_LONG_AT = 5;
|
|
|
|
//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( null, 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',
|
|
maxDataPoints : 30000,
|
|
bubbleRadius : 4,
|
|
entryAnimDuration : 500,
|
|
//TODO: no effect?
|
|
xNumTicks : 10,
|
|
yNumTicks : 10,
|
|
xAxisLabelBumpY : 40,
|
|
yAxisLabelBumpX : -35,
|
|
width : 500,
|
|
height : 500,
|
|
//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.updateConfig = function( newConfig ){
|
|
_.extend( this.config, newConfig );
|
|
};
|
|
|
|
// ........................................................ helpers
|
|
this.toString = function(){
|
|
return this.config.id;
|
|
};
|
|
this.translateStr = function( x, y ){
|
|
return 'translate(' + x + ',' + y + ')';
|
|
};
|
|
this.rotateStr = function( d, x, y ){
|
|
return 'rotate(' + d + ',' + x + ',' + y + ')';
|
|
};
|
|
|
|
// ........................................................ initial element creation
|
|
//NOTE: called on new
|
|
this.svg = d3.select( this.config.containerSelector )
|
|
.append( "svg:svg" ).attr( "class", "chart" ).style( 'display', 'none' );
|
|
this.content = this.svg.append( "svg:g" ).attr( "class", "content" );
|
|
|
|
this.xAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' );
|
|
this.xAxisLabel = this.xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' );
|
|
|
|
this.yAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' );
|
|
this.yAxisLabel = this.yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' );
|
|
|
|
this.log( 'built svg:', d3.selectAll( 'svg' ) );
|
|
|
|
this.adjustChartDimensions = function(){
|
|
this.svg
|
|
.attr( "width", this.config.width + ( this.config.marginRight + this.config.marginLeft ) )
|
|
.attr( "height", this.config.height + ( this.config.marginTop + this.config.marginBottom ) )
|
|
// initial is hidden - show it
|
|
.style( 'display', 'block' );
|
|
|
|
// move content group away from margins
|
|
this.content = this.svg.select( "g.content" )
|
|
.attr( "transform", this.translateStr( this.config.marginLeft, this.config.marginTop ) );
|
|
};
|
|
|
|
// ........................................................ data and scales
|
|
this.preprocessData = function( data ){
|
|
// set a cap on the data, limit to first n points
|
|
return data.slice( 0, this.config.maxDataPoints );
|
|
};
|
|
|
|
this.setUpDomains = function( xCol, yCol ){
|
|
this.xMin = this.config.xMin || d3.min( xCol );
|
|
this.xMax = this.config.xMax || d3.max( xCol );
|
|
this.yMin = this.config.yMin || d3.min( yCol );
|
|
this.yMax = this.config.yMax || d3.max( yCol );
|
|
};
|
|
|
|
this.setUpScales = function(){
|
|
// 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(){
|
|
// 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: this isn't reliable with -/+ ranges - better to go thru each tick
|
|
this.xLongestLabel = d3.max( _.map( [ this.xMin, this.xMax ],
|
|
function( number ){ return ( String( number ) ).length; } ) );
|
|
this.log( 'xLongestLabel:', this.xLongestLabel );
|
|
|
|
if( this.xLongestLabel >= X_LABEL_TOO_LONG_AT ){
|
|
//TODO: adjust ticks when tick labels are long - move odds down and extend tick line
|
|
// (for now) hide them
|
|
this.xAxis.selectAll( 'g' ).filter( ':nth-child(odd)' ).style( 'display', 'none' );
|
|
}
|
|
|
|
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 );
|
|
};
|
|
|
|
this.setUpYAxis = function(){
|
|
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 );
|
|
|
|
this.yLongestLabel = d3.max( _.map( [ this.yMin, this.yMax ],
|
|
function( number ){ return ( String( number ) ).length; } ) );
|
|
this.log( 'yLongestLabel:', this.yLongestLabel );
|
|
|
|
//TODO: ugh ... so clumsy
|
|
//TODO: this isn't reliable with -/+ ranges - better to go thru each tick
|
|
var neededY = this.yLongestLabel * GUESS_AT_SVG_CHAR_WIDTH + ( PADDING );
|
|
|
|
// increase width for xLongerStr, increase margin for y
|
|
//TODO??: (or transform each number: 2k)
|
|
if( this.config.yAxisLabelBumpX > -( neededY ) ){
|
|
this.config.yAxisLabelBumpX = -( neededY );
|
|
}
|
|
if( this.config.marginLeft < neededY ){
|
|
this.config.marginLeft = neededY + GUESS_AT_SVG_CHAR_HEIGHT;
|
|
// update dimensions, translations
|
|
this.adjustChartDimensions();
|
|
}
|
|
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(){
|
|
// 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' )
|
|
.classed( 'grid-line v-grid-line', true );
|
|
|
|
// update the attributes of existing and appended
|
|
this.vGridLines
|
|
.attr( 'x1', this.xScale )
|
|
.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 );
|
|
|
|
// HORIZONTAL
|
|
this.hGridLines = this.content.selectAll( 'line.h-grid-line' )
|
|
.data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) );
|
|
|
|
this.hGridLines.enter().append( 'svg:line' )
|
|
.classed( 'grid-line h-grid-line', true );
|
|
|
|
this.hGridLines
|
|
.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
|
|
//TODO: these to config ...somehow
|
|
//TODO: use these in renderDatapoints ...somehow
|
|
this.glyphEnterState = function( d3Elem ){
|
|
|
|
};
|
|
this.glyphFinalState = function( d3Elem ){
|
|
|
|
};
|
|
this.glyphExitState = function( d3Elem ){
|
|
|
|
};
|
|
|
|
this.renderDatapoints = function( xCol, yCol ){
|
|
|
|
var xPosFn = function( d, i ){
|
|
return plot.xScale( xCol[ i ] );
|
|
};
|
|
var yPosFn = function( d, i ){
|
|
return plot.yScale( yCol[ i ] );
|
|
};
|
|
|
|
// select all existing glyphs and compare to incoming data
|
|
// enter() will yield those glyphs that need to be added
|
|
// exit() will yield existing glyphs that need to be removed
|
|
this.datapoints = this.content.selectAll( ".glyph" )
|
|
.data( xCol );
|
|
|
|
// enter - new data to be added as glyphs: give them a 'entry' position and style
|
|
this.datapoints.enter()
|
|
.append( "svg:circle" ).attr( "class", "glyph" )
|
|
// start all bubbles at corner...
|
|
.attr( "cx", xPosFn )
|
|
.attr( "cy", 0 )
|
|
.attr( "r", 0 );
|
|
|
|
// for all existing glyphs and those that need to be added: transition anim to final state
|
|
this.datapoints
|
|
// ...animate to final position
|
|
.transition().duration( this.config.entryAnimDuration )
|
|
.attr( "cx", xPosFn )
|
|
.attr( "cy", yPosFn )
|
|
.attr( "r", this.config.bubbleRadius );
|
|
|
|
// glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM
|
|
this.datapoints.exit()
|
|
.transition().duration( this.config.entryAnimDuration )
|
|
.attr( "cy", this.config.height )
|
|
.attr( "r", 0 )
|
|
.style( "fill-opacity", 0 )
|
|
.remove();
|
|
|
|
this.log( this.datapoints, 'glyphs rendered' );
|
|
};
|
|
|
|
this.render = function( xCol, yCol ){
|
|
//pre: columns passed are numeric
|
|
this.log( 'renderScatterplot', xCol.length, yCol.length, this.config );
|
|
|
|
//pre: xCol.len == yCol.len
|
|
//TODO: ^^ isn't necessarily true with current ColumnDataProvider
|
|
xCol = this.preprocessData( xCol );
|
|
yCol = this.preprocessData( yCol );
|
|
this.log( 'xCol len', xCol.length, 'yCol len', yCol.length );
|
|
|
|
//TODO: compute min, max on server.
|
|
this.setUpDomains( xCol, yCol );
|
|
this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax );
|
|
|
|
this.setUpScales();
|
|
this.adjustChartDimensions();
|
|
|
|
this.setUpXAxis();
|
|
this.setUpYAxis();
|
|
|
|
this.renderGrid();
|
|
this.renderDatapoints( xCol, yCol );
|
|
//TODO: on hover red line to axes, display values
|
|
};
|
|
}
|
|
|
|
//// ugh...this seems like the wrong way to use models
|
|
//var ScatterplotModel = BaseModel.extend( LoggableMixin ).extend({
|
|
// logger : console
|
|
//});
|
|
|
|
var ScatterplotView = BaseView.extend( LoggableMixin ).extend({
|
|
//logger : console,
|
|
tagName : 'form',
|
|
className : 'scatterplot-settings-form',
|
|
|
|
events : {
|
|
'click #render-button' : 'renderScatterplot'
|
|
},
|
|
|
|
initialize : function( attributes ){
|
|
if( !attributes.dataset ){
|
|
throw( "ScatterplotView requires a dataset" );
|
|
} else {
|
|
this.dataset = attributes.dataset;
|
|
}
|
|
this.apiDatasetsURL = attributes.apiDatasetsURL;
|
|
this.chartConfig = attributes.chartConfig || {};
|
|
this.log( 'this.chartConfig:', this.chartConfig );
|
|
|
|
this.plot = new TwoVarScatterplot( this.chartConfig );
|
|
},
|
|
|
|
render : function(){
|
|
//TODO: to template
|
|
var view = this,
|
|
html = '',
|
|
columnHtml = '';
|
|
// build column select controls for each x, y (based on name if available)
|
|
// ugh...hafta preprocess
|
|
this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' );
|
|
_.each( this.dataset.metadata_column_types, function( type, index ){
|
|
// use only numeric columns
|
|
if( type === 'int' || type === 'float' ){
|
|
var name = 'column ' + index;
|
|
// label with the name if available
|
|
if( view.dataset.metadata_column_names ){
|
|
name = view.dataset.metadata_column_names[ index ];
|
|
}
|
|
columnHtml += '<option value="' + index + '">' + name + '</column>';
|
|
}
|
|
});
|
|
|
|
// column selector containers
|
|
html += '<div id="x-column-input">';
|
|
html += '<label for="">Data column for X: </label><select name="x-column">' + columnHtml + '</select>';
|
|
html += '</div>';
|
|
|
|
html += '<div id="y-column-input">';
|
|
html += '<label for="">Data column for Y: </label><select name="y-column">' + columnHtml + '</select>';
|
|
html += '</div>';
|
|
|
|
html += '<input id="render-button" type="button" value="Draw" />';
|
|
html += '<div class="clear"></div>';
|
|
|
|
//TODO: other vals: max_vals, start_val, pagination
|
|
|
|
this.$el.append( html );
|
|
this.$el.find( '#render-button' );
|
|
return this;
|
|
},
|
|
|
|
renderScatterplot : function(){
|
|
var view = this,
|
|
url = this.apiDatasetsURL + '/' + this.dataset.id + '?data_type=raw_data&';
|
|
xSelector = this.$el.find( '[name="x-column"]' ),
|
|
xVal = xSelector.val(),
|
|
xName = xSelector.children( '[value="' + xVal + '"]' ).text(),
|
|
ySelector = this.$el.find( '[name="y-column"]' ),
|
|
yVal = ySelector.val(),
|
|
yName = ySelector.children( '[value="' + yVal + '"]' ).text();
|
|
//TODO
|
|
this.log( xName, yName );
|
|
this.chartConfig.xLabel = xName;
|
|
this.chartConfig.yLabel = yName;
|
|
//TODO: alter directly
|
|
view.plot.updateConfig( this.chartConfig );
|
|
|
|
//TODO: validate columns - minimally: we can assume either set by selectors or via a good query string
|
|
//TODO: other vals: max, start, page
|
|
//TODO: chart config
|
|
|
|
url += jQuery.param({
|
|
columns : '[' + [ xVal, yVal ] + ']'
|
|
});
|
|
this.log( 'url:', url );
|
|
|
|
jQuery.ajax({
|
|
url : url,
|
|
dataType : 'json',
|
|
success : function( response ){
|
|
view.endpoint = response.endpoint;
|
|
view.plot.render(
|
|
// pull apart first two regardless of number of columns
|
|
_.map( response.data, function( columns ){ return columns[0]; } ),
|
|
_.map( response.data, function( columns ){ return columns[1]; } )
|
|
);
|
|
},
|
|
error : function( xhr, status, error ){
|
|
alert( 'ERROR:' + status + '\n' + error );
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|