diff options
author | Igor Sfiligoi <isfiligoi@ucsd.edu> | 2013-11-07 12:50:35 -0500 |
---|---|---|
committer | Igor Sfiligoi <isfiligoi@ucsd.edu> | 2013-11-07 12:50:35 -0500 |
commit | 680241cbb652ee29088baa6cde2da02002e5ec47 (patch) | |
tree | 3f0c17df67c0ab0e91542ae75fff175750581859 /jquery.flot.pie.js | |
parent | f361d0498d284c6c7f3f366bf88e49b93e1245f0 (diff) |
Diffstat (limited to 'jquery.flot.pie.js')
-rw-r--r-- | jquery.flot.pie.js | 1567 |
1 files changed, 817 insertions, 750 deletions
diff --git a/jquery.flot.pie.js b/jquery.flot.pie.js index 70941dd..6553c8e 100644 --- a/jquery.flot.pie.js +++ b/jquery.flot.pie.js @@ -1,750 +1,817 @@ -/*
-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 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);
+/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + 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($) { + + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + + var canvas = null, + target = null, + maxRadius = null, + centerLeft = null, + centerTop = null, + processed = false, + ctx = null; + + // interactive variables + + var highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function(plot, options) { + if (options.series.pie.show) { + + 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; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (var i = 0; i < data.length; ++i) { + + var value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length == 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (var i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push({ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }); + } + } + + if (numCombined > 1) { + 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 + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 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 > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + 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, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 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() { + + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.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 + + if (options.series.pie.stroke.width > 0) { + 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); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + + if (angle <= 0 || isNaN(angle)) { + 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 + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius,currentAngle + angle / 2, 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; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.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) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + + if (slice.data[0][1] == 0) { + return true; + } + + // 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 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + 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>") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // 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) { + if (options.series.pie.innerRadius > 0) { + + // subtract the center + + layer.save(); + var 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, + x, y; + + 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 / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + 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. + + var 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(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var 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) { + + var options = plot.getOptions(); + + 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 (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + + if (series.angle <= 0 || isNaN(series.angle)) { + 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 / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, 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, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + 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.1" + }); + +})(jQuery); |