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