Spaces:
Runtime error
Runtime error
/* | |
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() | |
{ | |
var placeholder = plot.getPlaceholder(), | |
width = placeholder.width(), | |
height = placeholder.height(); | |
legendWidth = target.children().filter('.legend').children().width(); | |
// calculate maximum radius and center point | |
maxRadius = Math.min(width,(height/options.series.pie.tilt))/2; | |
centerTop = (height/2)+options.series.pie.offset.top; | |
centerLeft = (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>width-maxRadius) | |
centerLeft = 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 donut hole | |
drawDonutHole(ctx); | |
// draw labels | |
if (options.series.pie.label.show) | |
drawLabels(); | |
// restore to original state | |
ctx.restore(); | |
function drawSlice(angle, color, fill) | |
{ | |
if (angle<=0) | |
return; | |
if (fill) | |
ctx.fillStyle = color; | |
else | |
{ | |
ctx.strokeStyle = color; | |
ctx.lineJoin = 'round'; | |
} | |
ctx.beginPath(); | |
if (Math.abs(angle - Math.PI*2) > 0.000000001) | |
ctx.moveTo(0,0); // Center of the pie | |
else if ($.browser.msie) | |
angle -= 0.0001; | |
//ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera | |
ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false); | |
ctx.closePath(); | |
//ctx.rotate(angle); // This doesn't work properly in Opera | |
currentAngle += angle; | |
if (fill) | |
ctx.fill(); | |
else | |
ctx.stroke(); | |
} | |
function drawLabels() | |
{ | |
var currentAngle = startAngle; | |
// set radius | |
if (options.series.pie.label.radius>1) | |
var radius = options.series.pie.label.radius; | |
else | |
var radius = maxRadius * options.series.pie.label.radius; | |
for (var i = 0; i < slices.length; ++i) | |
{ | |
if (slices[i].percent >= options.series.pie.label.threshold*100) | |
drawLabel(slices[i], currentAngle, i); | |
currentAngle += slices[i].angle; | |
} | |
function drawLabel(slice, startAngle, index) | |
{ | |
if (slice.data[0][1]==0) | |
return; | |
// format label text | |
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; | |
if (lf) | |
text = lf(slice.label, slice); | |
else | |
text = slice.label; | |
if (plf) | |
text = plf(text, slice); | |
var halfAngle = ((startAngle+slice.angle) + startAngle)/2; | |
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); | |
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; | |
var html = '<span class="pieLabel" id="pieLabel'+index+'" style="position:absolute;top:' + y + 'px;left:' + x + 'px;">' + text + "</span>"; | |
target.append(html); | |
var label = target.children('#pieLabel'+index); | |
var labelTop = (y - label.height()/2); | |
var labelLeft = (x - label.width()/2); | |
label.css('top', labelTop); | |
label.css('left', labelLeft); | |
// check to make sure that the label is not outside the canvas | |
if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0) | |
redraw = true; | |
if (options.series.pie.label.background.opacity != 0) { | |
// put in the transparent background separately to avoid blended labels and label boxes | |
var c = options.series.pie.label.background.color; | |
if (c == null) { | |
c = slice.color; | |
} | |
var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;'; | |
$('<div class="pieLabelBackground" style="position:absolute;width:' + label.width() + 'px;height:' + label.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').insertBefore(label).css('opacity', options.series.pie.label.background.opacity); | |
} | |
} // end individual label function | |
} // end drawLabels function | |
} // end drawPie function | |
} // end draw function | |
// Placed here because it needs to be accessed from multiple locations | |
function drawDonutHole(layer) | |
{ | |
// draw donut hole | |
if(options.series.pie.innerRadius > 0) | |
{ | |
// subtract the center | |
layer.save(); | |
innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; | |
layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color | |
layer.beginPath(); | |
layer.fillStyle = options.series.pie.stroke.color; | |
layer.arc(0,0,innerRadius,0,Math.PI*2,false); | |
layer.fill(); | |
layer.closePath(); | |
layer.restore(); | |
// add inner stroke | |
layer.save(); | |
layer.beginPath(); | |
layer.strokeStyle = options.series.pie.stroke.color; | |
layer.arc(0,0,innerRadius,0,Math.PI*2,false); | |
layer.stroke(); | |
layer.closePath(); | |
layer.restore(); | |
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted. | |
} | |
} | |
//-- Additional Interactive related functions -- | |
function isPointInPoly(poly, pt) | |
{ | |
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) | |
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) | |
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) | |
&& (c = !c); | |
return c; | |
} | |
function findNearbySlice(mouseX, mouseY) | |
{ | |
var slices = plot.getData(), | |
options = plot.getOptions(), | |
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; | |
for (var i = 0; i < slices.length; ++i) | |
{ | |
var s = slices[i]; | |
if(s.pie.show) | |
{ | |
ctx.save(); | |
ctx.beginPath(); | |
ctx.moveTo(0,0); // Center of the pie | |
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. | |
ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false); | |
ctx.closePath(); | |
x = mouseX-centerLeft; | |
y = mouseY-centerTop; | |
if(ctx.isPointInPath) | |
{ | |
if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop)) | |
{ | |
//alert('found slice!'); | |
ctx.restore(); | |
return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; | |
} | |
} | |
else | |
{ | |
// excanvas for IE doesn;t support isPointInPath, this is a workaround. | |
p1X = (radius * Math.cos(s.startAngle)); | |
p1Y = (radius * Math.sin(s.startAngle)); | |
p2X = (radius * Math.cos(s.startAngle+(s.angle/4))); | |
p2Y = (radius * Math.sin(s.startAngle+(s.angle/4))); | |
p3X = (radius * Math.cos(s.startAngle+(s.angle/2))); | |
p3Y = (radius * Math.sin(s.startAngle+(s.angle/2))); | |
p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5))); | |
p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5))); | |
p5X = (radius * Math.cos(s.startAngle+s.angle)); | |
p5Y = (radius * Math.sin(s.startAngle+s.angle)); | |
arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]]; | |
arrPoint = [x,y]; | |
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? | |
if(isPointInPoly(arrPoly, arrPoint)) | |
{ | |
ctx.restore(); | |
return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; | |
} | |
} | |
ctx.restore(); | |
} | |
} | |
return null; | |
} | |
function onMouseMove(e) | |
{ | |
triggerClickHoverEvent('plothover', e); | |
} | |
function onClick(e) | |
{ | |
triggerClickHoverEvent('plotclick', e); | |
} | |
// trigger click or hover event (they send the same parameters so we share their code) | |
function triggerClickHoverEvent(eventname, e) | |
{ | |
var offset = plot.offset(), | |
canvasX = parseInt(e.pageX - offset.left), | |
canvasY = parseInt(e.pageY - offset.top), | |
item = findNearbySlice(canvasX, canvasY); | |
if (options.grid.autoHighlight) | |
{ | |
// clear auto-highlights | |
for (var i = 0; i < highlights.length; ++i) | |
{ | |
var h = highlights[i]; | |
if (h.auto == eventname && !(item && h.series == item.series)) | |
unhighlight(h.series); | |
} | |
} | |
// highlight the slice | |
if (item) | |
highlight(item.series, eventname); | |
// trigger any hover bind events | |
var pos = { pageX: e.pageX, pageY: e.pageY }; | |
target.trigger(eventname, [ pos, item ]); | |
} | |
function highlight(s, auto) | |
{ | |
if (typeof s == "number") | |
s = series[s]; | |
var i = indexOfHighlight(s); | |
if (i == -1) | |
{ | |
highlights.push({ series: s, auto: auto }); | |
plot.triggerRedrawOverlay(); | |
} | |
else if (!auto) | |
highlights[i].auto = false; | |
} | |
function unhighlight(s) | |
{ | |
if (s == null) | |
{ | |
highlights = []; | |
plot.triggerRedrawOverlay(); | |
} | |
if (typeof s == "number") | |
s = series[s]; | |
var i = indexOfHighlight(s); | |
if (i != -1) | |
{ | |
highlights.splice(i, 1); | |
plot.triggerRedrawOverlay(); | |
} | |
} | |
function indexOfHighlight(s) | |
{ | |
for (var i = 0; i < highlights.length; ++i) | |
{ | |
var h = highlights[i]; | |
if (h.series == s) | |
return i; | |
} | |
return -1; | |
} | |
function drawOverlay(plot, octx) | |
{ | |
//alert(options.series.pie.radius); | |
var options = plot.getOptions(); | |
//alert(options.series.pie.radius); | |
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; | |
octx.save(); | |
octx.translate(centerLeft, centerTop); | |
octx.scale(1, options.series.pie.tilt); | |
for (i = 0; i < highlights.length; ++i) | |
drawHighlight(highlights[i].series); | |
drawDonutHole(octx); | |
octx.restore(); | |
function drawHighlight(series) | |
{ | |
if (series.angle < 0) return; | |
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); | |
octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor | |
octx.beginPath(); | |
if (Math.abs(series.angle - Math.PI*2) > 0.000000001) | |
octx.moveTo(0,0); // Center of the pie | |
octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false); | |
octx.closePath(); | |
octx.fill(); | |
} | |
} | |
} // end init (plugin body) | |
// define pie specific options and their default values | |
var options = { | |
series: { | |
pie: { | |
show: false, | |
radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) | |
innerRadius:0, /* for donut */ | |
startAngle: 3/2, | |
tilt: 1, | |
offset: { | |
top: 0, | |
left: 'auto' | |
}, | |
stroke: { | |
color: '#FFF', | |
width: 1 | |
}, | |
label: { | |
show: 'auto', | |
formatter: function(label, slice){ | |
return '<div style="font-size:x-small;text-align:center;padding:2px;color:'+slice.color+';">'+label+'<br/>'+Math.round(slice.percent)+'%</div>'; | |
}, // formatter function | |
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) | |
background: { | |
color: null, | |
opacity: 0 | |
}, | |
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) | |
}, | |
combine: { | |
threshold: -1, // percentage at which to combine little slices into one larger slice | |
color: null, // color to give the new slice (auto-generated if null) | |
label: 'Other' // label to give the new slice | |
}, | |
highlight: { | |
//color: '#FFF', // will add this functionality once parseColor is available | |
opacity: 0.5 | |
} | |
} | |
} | |
}; | |
$.plot.plugins.push({ | |
init: init, | |
options: options, | |
name: "pie", | |
version: "1.0" | |
}); | |
})(jQuery); | |