/**
* Scatterplot creates a new Scatterplot
* @version 1.0
* @author Jonas Marcello <rujmarcello@gmail.com>
* @instance
* @module EsbmePlots
* @memberof module:EsbmePlots
* @augments module:EsbmePlots
* @constructor module:EsbmePlots.Scatterplot
* @example
* var x = Scatterplot()
* .parent(d3.selectAll(".container"))
* .seriesNames(["blue","pink"])
* .margin("auto")
* .data([
* [
* [30,26],
* [10,15],
* [12,0]
* ],
* [
* [3,2],
* [1,1],
* [1,0]
* ]
* ])
* .draw()
*/
function Scatterplot(){
var highlights = [],
radius = 1,
highlightRadius = 0,
highlightColor = "#000000",
canvas = "auto",
maxDots = 100,
scaleX = d3.scaleLinear()
.range([0,0]),
scaleY = d3.scaleLinear()
.range([0,0]),
colors = ["#D32F2F","#388e3c","#1976D2","#ffeb3b","#ff9800","#673ab7"]
labelShiftX = d3.scaleLinear()
.range([0.5,-9])
.domain([0,-90]),
labelShiftY = d3.scaleLinear()
.range([9,-0.5])
.domain([0,-90]),
labelShiftDY = d3.scaleLinear()
.range([0.71,0.29])
.domain([0,-90])
// creates a new EsbmePlot instance to use as Scatterplot
var plot = new EsbmePlot()
// updates the default classname to still include the id, but
// also contain "piechart"
var id = plot.className()
plot.className("scatterplot "+id)
/**
* <p>Defines data points to be highlighted</p>
* Data needs to be passed an Array of highlight<br>
* Each highlight is [x-Value, y-Value, Text, labelPosition]
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Array} [highlights]
* @method highlights
*/
plot.highlights = function(_){
if (arguments.length){
highlights = _
return plot
} else {
return highlights
}
}
/**
* <p>Defines radius of highlighted points</p>
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Int} radius
* @method highlightRadius
*/
plot.highlightRadius = function(_){
if (arguments.length){
highlightRadius = +_
return plot
} else {
return highlightRadius || (radius * 2)
}
}
/**
* <p>Defines color of highlighted points</p>
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {String} RGB_Color
* @method highlightColor
*/
plot.highlightColor = function(_){
if (arguments.length){
highlightColor = _
return plot
} else {
return highlightColor
}
}
/**
* <p>Defines the radius of the dots</p>
* If value is specified, sets the value.<br>
* If value is not specified, returns the current value
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Integer} [value=0]
* @method radius
*/
plot.radius = function(_){
if (arguments.length){
radius = +_
return plot
} else {
return radius
}
}
/**
* <p>Defines whether to draw on a canvas or SVG</p>
* If value is specified, sets the value.<br>
* If value is not specified, returns the current value
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {String} [value="auto"] Allowed values: auto|svg|canvas
* @method radius
*/
plot.canvas = function(_){
if (arguments.length){
canvas = _
return plot
} else {
return canvas
}
}
/**
* <p>If `canvas` is set to `auto` sets the limit of data points to draw on an SVG</p>
* If more data points than `maxDots` are present, plot is drawn as canvas.<br>
* This might not work well with adding more data etc. In this case use canvas as default
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Integer} [value=100]
* @method radius
*/
plot.maxDots = function(_){
if (arguments.length){
maxDots = _
return plot
} else {
return maxDots
}
}
/**
* <p>Defines the custom Scatterplot specifc scale for the x-axis</p>
* If value is specified, sets the x-axis scale.<br>
* If value is not specified, return the current scale
* Should not be used as setter in most cases
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Function} [value]
* @method scaleX
*/
plot.scaleX = function(_){
if (arguments.length){
scaleX = _
return plot
} else {
return scaleX
}
}
/**
* <p>Defines the custom Scatterplot specifc scale for the y-axis</p>
* If value is specified, sets the y-axis scale.<br>
* If value is not specified, return the current scale
* Should not be used as setter in most cases
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Function} [value]
* @method scaleY
*/
plot.scaleY = function(_){
if (arguments.length){
scaleY = _
return plot
} else {
return scaleY
}
}
/**
* <p>Defines the custom colors for dots on the canvas</p>
* If value is specified, sets the color groups.<br>
* If value is not specified, return the current color groups
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @param {Function} [value]
* @method colors
*/
plot.colors = function(_){
if (arguments.length){
colors = _
return plot
} else {
return colors
}
}
/**
* <p>Sets the domain for the x- and y-axis scales</p>
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @private
* @param {Function} [value]
* @method setScaleDomains
*/
plot.setScaleDomains = function(){
scaleX.domain(plot.xDomain())
scaleY.domain(plot.yDomain())
}
/**
* <p>Sets the range for the x- and y-axis scales</p>
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @private
* @method setScaleRanges
*/
plot.setScaleRanges = function(dimensions){
scaleX.range([0,dimensions.plotWidth])
scaleY.range([dimensions.plotHeight,0])
}
/**
* <p>Adds the X- and Y-axes to the plot</p>
* Both axes will get automated tickValues
* @memberof module:EsbmePlots.Scatterplot
* @instance
* @private
* @method addAxes
*/
plot.addAxes = function(){
plot.addXAxis()
plot.addYAxis()
}
plot.checkData = checkData
plot.drawData = drawData
return plot
function drawData(canvas, dimensions){
// Always remove previous 2D <canvas>
plot.parent().selectAll("canvas").remove()
var dataSize = calculateTotalDataSize()
if (plotOnCanvas(dataSize)) {
canvas.selectAll("*").remove()
// If plot should be plotted on a 2D <canvas>
// the `canvas` object is changed to be virtual and not added to the DOM
var virtualCanvas = d3.select(document.createElement("customContainer"))
virtualCanvas.attr("transform", canvas.attr("transform"))
canvas = virtualCanvas
}
var className = plot.className()
plot.seriesNames().forEach(function(singleSeries,i){
var seriesData = (plot.data()[i] || []).map(function(datum,j){
return {
x: datum[0],
y: datum[1],
seriesName: singleSeries
}
})
var dots = canvas.selectAll("circle."+singleSeries)
.data(seriesData)
dots.exit().remove()
dots = dots.enter()
.append("circle")
.merge(dots)
.attr("class",function(d){
return "esbmeplot series "+className +" " +d.seriesName
})
.attr("cx",function(d){
return scaleX(d.x)
})
.attr("cy",function(d){
return scaleY(d.y)
})
.attr("r",radius)
.attr("filLStyle",colors[i] || "#000000")
})
if (highlights.length){
plotHighlights(canvas)
}
if (plotOnCanvas(dataSize)) {
// draw the 2D <canvas> using the virtual DOM <svg> canvas
// as blueprint
drawCanvas(canvas)
}
}
function plotHighlights(canvas){
var highlightData = highlights.map(function(highlight){
return {
x: highlight[0],
y: highlight[1],
seriesname: "highlight",
label: highlight[2],
labelpos: highlight[3]
}
})
var dots = canvas.selectAll("circle.highlights")
.data(highlightData)
dots.exit().remove()
dots = dots.enter()
.append("circle")
.merge(dots)
.attr("class",function(d){
return "esbmeplot series "+plot.className() +" " +d.seriesName
})
.attr("cx",function(d){
return scaleX(d.x)
})
.attr("cy",function(d){
return scaleY(d.y)
})
.attr("r",highlightRadius || radius * 2)
.attr("filLStyle",highlightColor || "#FF0000")
// Add the labels as well
var labels = canvas.selectAll("text.highlight")
.data(highlightData)
labels.exit().remove()
labels = labels.enter()
.append("text")
.merge(labels)
.attr("class",function(d){
return "esbmeplot label highlight "+ plot.className()
})
.attr("transform",function(d){
return "translate("+ scaleX(d.x)+","+scaleY(d.y)+")"
})
.attr("dy","0.355em")
.attr("dx", (highlightRadius || radius * 2) + 3 + "px" )
.attr("text-anchor",function(d){
return d.labelpos === "left" ? "end" : "start"
})
.text(function(d){return d.label})
}
function drawCanvas(virtualCanvas){
// The 2d <canvas> should be placed directly over the SVG
var margin = plot.margin()
var leftMargin = + virtualCanvas.attr("transform").slice(10).split(",")[0]
var topMargin = + virtualCanvas.attr("transform").slice(10).split(",")[1].slice(0,-1)
var chart = plot.parent().append("canvas")
.attr("width",plot.width()+"px")
.attr("height",plot.height()+"px")
var context = chart.node().getContext("2d");
var dots = virtualCanvas.selectAll("circle")
dots.each(function(dot){
var node = d3.select(this)
var x = +node.attr("cx") + leftMargin
var y = +node.attr("cy") + topMargin
var radius = +node.attr("r")
context.beginPath()
context.fillStyle = node.attr("fillStyle")
context.arc(x, y, radius, 0, 2 * Math.PI)
context.fill()
context.closePath()
})
virtualCanvas.selectAll("text").each(function(label){
context.beginPath()
context.font = (highlightRadius || radius * 2 ) + 10 +"px Helvetica sans-serif";
context.textBaseline = "middle";
context.textAlign = label.labelpos === "left" ? "right" : "left"
context.fillStyle = "#000000"
context.fillText(
label.label,
scaleX(label.x) + leftMargin + 3 + (highlightRadius || radius * 2),
scaleY(label.y) + topMargin)
context.closePath()
})
}
function checkData(){
if (!(plot.data() instanceof Array)){
throw new TypeError("Data has to be an Array")
}
// ensures that we have a nested Array with one or multiple series
plot.data(plot.data().map(function(datum){
return datum instanceof Array ?
datum :
[datum]
}))
if (plot.yDomain() === "auto"){
plot.yDomain([0,getMaxY()])
}
if (plot.xDomain() === "auto"){
plot.xDomain([0,getMaxX()])
}
}
function calculateTotalDataSize(){
return plot.data().reduce(function(total, series){
return total + series.length
},0)
}
function plotOnCanvas(dataSize){
if (canvas === "canvas") return true
if (canvas === "auto" && dataSize > maxDots) return true
return false
}
function getMaxX(){
return getMax(0)
}
function getMaxY(){
return getMax(1)
}
function getMax(idx){
return plot.data().reduce(function(max,series){
var seriesMax = series.reduce(function(localMax, datum){
return datum[idx] > localMax ? datum[idx] : localMax
},0)
return seriesMax > max ? seriesMax : max
},0)
}
}