mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
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:
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
Reference in New Issue
Block a user