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
@@ -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();
});
}
//==============================================================================