|
/* |
|
Flot plugin for rendering pie charts. The plugin assumes the data is |
|
coming is as a single data value for each series, and each of those |
|
values is a positive value or zero (negative numbers don't make |
|
any sense and will cause strange effects). The data values do |
|
NOT need to be passed in as percentage values because it |
|
internally calculates the total and percentages. |
|
|
|
* Created by Brian Medendorp, June 2009 |
|
* Updated November 2009 with contributions from: btburnett3, Anthony Aragues and Xavi Ivars |
|
|
|
* Changes: |
|
2009-10-22: lineJoin set to round |
|
2009-10-23: IE full circle fix, donut |
|
2009-11-11: Added basic hover from btburnett3 - does not work in IE, and center is off in Chrome and Opera |
|
2009-11-17: Added IE hover capability submitted by Anthony Aragues |
|
2009-11-18: Added bug fix submitted by Xavi Ivars (issues with arrays when other JS libraries are included as well) |
|
|
|
|
|
Available options are: |
|
series: { |
|
pie: { |
|
show: true/false |
|
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' |
|
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect |
|
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result |
|
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) |
|
offset: { |
|
top: integer value to move the pie up or down |
|
left: integer value to move the pie left or right, or 'auto' |
|
}, |
|
stroke: { |
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') |
|
width: integer pixel width of the stroke |
|
}, |
|
label: { |
|
show: true/false, or 'auto' |
|
formatter: a user-defined function that modifies the text/style of the label text |
|
radius: 0-1 for percentage of fullsize, or a specified pixel length |
|
background: { |
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') |
|
opacity: 0-1 |
|
}, |
|
threshold: 0-1 for the percentage value at which to hide labels (if they're too small) |
|
}, |
|
combine: { |
|
threshold: 0-1 for the percentage value at which to combine slices (if they're too small) |
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined |
|
label: any text value of what the combined slice should be labeled |
|
} |
|
highlight: { |
|
opacity: 0-1 |
|
} |
|
} |
|
} |
|
|
|
More detail and specific examples can be found in the included HTML file. |
|
|
|
*/ |
|
|
|
(function ($) |
|
{ |
|
function init(plot) // this is the "body" of the plugin |
|
{ |
|
var canvas = null; |
|
var target = null; |
|
var maxRadius = null; |
|
var centerLeft = null; |
|
var centerTop = null; |
|
var total = 0; |
|
var redraw = true; |
|
var redrawAttempts = 10; |
|
var shrink = 0.95; |
|
var legendWidth = 0; |
|
var processed = false; |
|
var raw = false; |
|
|
|
// interactive variables |
|
var highlights = []; |
|
|
|
// add hook to determine if pie plugin in enabled, and then perform necessary operations |
|
plot.hooks.processOptions.push(checkPieEnabled); |
|
plot.hooks.bindEvents.push(bindEvents); |
|
|
|
// check to see if the pie plugin is enabled |
|
function checkPieEnabled(plot, options) |
|
{ |
|
if (options.series.pie.show) |
|
{ |
|
//disable grid |
|
options.grid.show = false; |
|
|
|
// set labels.show |
|
if (options.series.pie.label.show=='auto') |
|
if (options.legend.show) |
|
options.series.pie.label.show = false; |
|
else |
|
options.series.pie.label.show = true; |
|
|
|
// set radius |
|
if (options.series.pie.radius=='auto') |
|
if (options.series.pie.label.show) |
|
options.series.pie.radius = 3/4; |
|
else |
|
options.series.pie.radius = 1; |
|
|
|
// ensure sane tilt |
|
if (options.series.pie.tilt>1) |
|
options.series.pie.tilt=1; |
|
if (options.series.pie.tilt<0) |
|
options.series.pie.tilt=0; |
|
|
|
// add processData hook to do transformations on the data |
|
plot.hooks.processDatapoints.push(processDatapoints); |
|
plot.hooks.drawOverlay.push(drawOverlay); |
|
|
|
// add draw hook |
|
plot.hooks.draw.push(draw); |
|
} |
|
} |
|
|
|
// bind hoverable events |
|
function bindEvents(plot, eventHolder) |
|
{ |
|
var options = plot.getOptions(); |
|
|
|
if (options.series.pie.show && options.grid.hoverable) |
|
eventHolder.unbind('mousemove').mousemove(onMouseMove); |
|
|
|
if (options.series.pie.show && options.grid.clickable) |
|
eventHolder.unbind('click').click(onClick); |
|
} |
|
|
|
|
|
// debugging function that prints out an object |
|
function alertObject(obj) |
|
{ |
|
var msg = ''; |
|
function traverse(obj, depth) |
|
{ |
|
if (!depth) |
|
depth = 0; |
|
for (var i = 0; i < obj.length; ++i) |
|
{ |
|
for (var j=0; j<depth; j++) |
|
msg += '\t'; |
|
|
|
if( typeof obj[i] == "object") |
|
{ // its an object |
|
msg += ''+i+':\n'; |
|
traverse(obj[i], depth+1); |
|
} |
|
else |
|
{ // its a value |
|
msg += ''+i+': '+obj[i]+'\n'; |
|
} |
|
} |
|
} |
|
traverse(obj); |
|
alert(msg); |
|
} |
|
|
|
function calcTotal(data) |
|
{ |
|
for (var i = 0; i < data.length; ++i) |
|
{ |
|
var item = parseFloat(data[i].data[0][1]); |
|
if (item) |
|
total += item; |
|
} |
|
} |
|
|
|
function processDatapoints(plot, series, data, datapoints) |
|
{ |
|
if (!processed) |
|
{ |
|
processed = true; |
|
|
|
canvas = plot.getCanvas(); |
|
target = $(canvas).parent(); |
|
options = plot.getOptions(); |
|
|
|
plot.setData(combine(plot.getData())); |
|
} |
|
} |
|
|
|
function setupPie() |
|
{ |
|
legendWidth = target.children().filter('.legend').children().width(); |
|
|
|
// calculate maximum radius and center point |
|
maxRadius = Math.min(canvas.width,(canvas.height/options.series.pie.tilt))/2; |
|
centerTop = (canvas.height/2)+options.series.pie.offset.top; |
|
centerLeft = (canvas.width/2); |
|
|
|
if (options.series.pie.offset.left=='auto') |
|
if (options.legend.position.match('w')) |
|
centerLeft += legendWidth/2; |
|
else |
|
centerLeft -= legendWidth/2; |
|
else |
|
centerLeft += options.series.pie.offset.left; |
|
|
|
if (centerLeft<maxRadius) |
|
centerLeft = maxRadius; |
|
else if (centerLeft>canvas.width-maxRadius) |
|
centerLeft = canvas.width-maxRadius; |
|
} |
|
|
|
function fixData(data) |
|
{ |
|
for (var i = 0; i < data.length; ++i) |
|
{ |
|
if (typeof(data[i].data)=='number') |
|
data[i].data = [[1,data[i].data]]; |
|
else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined') |
|
{ |
|
if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined') |
|
data[i].label = data[i].data.label; // fix weirdness coming from flot |
|
data[i].data = [[1,0]]; |
|
|
|
} |
|
} |
|
return data; |
|
} |
|
|
|
function combine(data) |
|
{ |
|
data = fixData(data); |
|
calcTotal(data); |
|
var combined = 0; |
|
var numCombined = 0; |
|
var color = options.series.pie.combine.color; |
|
|
|
var newdata = []; |
|
for (var i = 0; i < data.length; ++i) |
|
{ |
|
// make sure its a number |
|
data[i].data[0][1] = parseFloat(data[i].data[0][1]); |
|
if (!data[i].data[0][1]) |
|
data[i].data[0][1] = 0; |
|
|
|
if (data[i].data[0][1]/total<=options.series.pie.combine.threshold) |
|
{ |
|
combined += data[i].data[0][1]; |
|
numCombined++; |
|
if (!color) |
|
color = data[i].color; |
|
} |
|
else |
|
{ |
|
newdata.push({ |
|
data: [[1,data[i].data[0][1]]], |
|
color: data[i].color, |
|
label: data[i].label, |
|
angle: (data[i].data[0][1]*(Math.PI*2))/total, |
|
percent: (data[i].data[0][1]/total*100) |
|
}); |
|
} |
|
} |
|
if (numCombined>0) |
|
newdata.push({ |
|
data: [[1,combined]], |
|
color: color, |
|
label: options.series.pie.combine.label, |
|
angle: (combined*(Math.PI*2))/total, |
|
percent: (combined/total*100) |
|
}); |
|
return newdata; |
|
} |
|
|
|
function draw(plot, newCtx) |
|
{ |
|
if (!target) return; // if no series were passed |
|
ctx = newCtx; |
|
|
|
setupPie(); |
|
var slices = plot.getData(); |
|
|
|
var attempts = 0; |
|
while (redraw && attempts<redrawAttempts) |
|
{ |
|
redraw = false; |
|
if (attempts>0) |
|
maxRadius *= shrink; |
|
attempts += 1; |
|
clear(); |
|
if (options.series.pie.tilt<=0.8) |
|
drawShadow(); |
|
drawPie(); |
|
} |
|
if (attempts >= redrawAttempts) { |
|
clear(); |
|
target.prepend('<div class="error">Could not draw pie with labels contained inside canvas</div>'); |
|
} |
|
|
|
if ( plot.setSeries && plot.insertLegend ) |
|
{ |
|
plot.setSeries(slices); |
|
plot.insertLegend(); |
|
} |
|
|
|
// we're actually done at this point, just defining internal functions at this point |
|
|
|
function clear() |
|
{ |
|
ctx.clearRect(0,0,canvas.width,canvas.height); |
|
target.children().filter('.pieLabel, .pieLabelBackground').remove(); |
|
} |
|
|
|
function drawShadow() |
|
{ |
|
var shadowLeft = 5; |
|
var shadowTop = 15; |
|
var edge = 10; |
|
var alpha = 0.02; |
|
|
|
// set radius |
|
if (options.series.pie.radius>1) |
|
var radius = options.series.pie.radius; |
|
else |
|
var radius = maxRadius * options.series.pie.radius; |
|
|
|
if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/2)-shadowTop || radius<=edge) |
|
return; // shadow would be outside canvas, so don't draw it |
|
|
|
ctx.save(); |
|
ctx.translate(shadowLeft,shadowTop); |
|
ctx.globalAlpha = alpha; |
|
ctx.fillStyle = '#000'; |
|
|
|
// center and rotate to starting position |
|
ctx.translate(centerLeft,centerTop); |
|
ctx.scale(1, options.series.pie.tilt); |
|
|
|
//radius -= edge; |
|
for (var i=1; i<=edge; i++) |
|
{ |
|
ctx.beginPath(); |
|
ctx.arc(0,0,radius,0,Math.PI*2,false); |
|
ctx.fill(); |
|
radius -= i; |
|
} |
|
|
|
ctx.restore(); |
|
} |
|
|
|
function drawPie() |
|
{ |
|
startAngle = Math.PI*options.series.pie.startAngle; |
|
|
|
// set radius |
|
if (options.series.pie.radius>1) |
|
var radius = options.series.pie.radius; |
|
else |
|
var radius = maxRadius * options.series.pie.radius; |
|
|
|
// center and rotate to starting position |
|
ctx.save(); |
|
ctx.translate(centerLeft,centerTop); |
|
ctx.scale(1, options.series.pie.tilt); |
|
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera |
|
|
|
// draw slices |
|
ctx.save(); |
|
var currentAngle = startAngle; |
|
for (var i = 0; i < slices.length; ++i) |
|
{ |
|
slices[i].startAngle = currentAngle; |
|
drawSlice(slices[i].angle, slices[i].color, true); |
|
} |
|
ctx.restore(); |
|
|
|
// draw slice outlines |
|
ctx.save(); |
|
ctx.lineWidth = options.series.pie.stroke.width; |
|
currentAngle = startAngle; |
|
for (var i = 0; i < slices.length; ++i) |
|
drawSlice(slices[i].angle, options.series.pie.stroke.color, false); |
|
ctx.restore(); |
|
|
|
// draw don |