mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 07:12:04 +08:00
279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
// =============================================================================
|
|
/**
|
|
* Two Variable scatterplot visualization using d3
|
|
* Uses semi transparent circles to show density of data in x, y grid
|
|
* usage :
|
|
* var plot = new scatterplot( $( 'svg' ).get(0), config, data )
|
|
*/
|
|
function scatterplot( renderTo, config, data ){
|
|
//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 ])
|
|
};
|
|
|
|
// .................................................................... 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 );
|
|
};
|
|
|
|
// .................................................................... 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: set coords
|
|
grid.v.lines
|
|
.attr( 'x1', interpolaterFns.x )
|
|
.attr( 'x2', interpolaterFns.x )
|
|
.attr( 'y1', 0 )
|
|
.attr( 'y2', config.height );
|
|
// exit: just remove them
|
|
grid.v.lines.exit().remove();
|
|
//console.log( 'grid.v.lines:', grid.v.lines );
|
|
|
|
// 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 );
|
|
grid.h.lines
|
|
.attr( 'x1', 0 )
|
|
.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();
|
|
|
|
//// .................................................................... 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 );
|
|
|
|
// for all EXISTING glyphs and those that need to be added: transition anim to final state
|
|
datapoints.transition().duration( config.animDuration )
|
|
.attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); })
|
|
.attr( "r", config.datapointSize );
|
|
//console.log( 'datapoints:', datapoints );
|
|
|
|
function _redrawDatapointsClipped(){
|
|
return datapoints
|
|
.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' );
|
|
}
|
|
|
|
// .................................................................... 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 );
|
|
|
|
|
|
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 });
|
|
}
|
|
|
|
datapoints.on( 'mouseover', function( d, i ){
|
|
var datapoint = d3.select( this );
|
|
datapoint
|
|
.style( 'fill', 'red' )
|
|
.style( 'fill-opacity', 1 );
|
|
|
|
// 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 );
|
|
}
|
|
|
|
// 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();
|
|
});
|
|
}
|
|
|
|
//==============================================================================
|