1 /* Flot plugin for rendering pie charts.
3 Copyright (c) 2007-2013 IOLA and Ole Laursen.
4 Licensed under the MIT license.
6 The plugin assumes that each series has a single data value, and that each
7 value is a positive integer or zero. Negative numbers don't make sense for a
8 pie chart, and have unpredictable results. The values do NOT need to be
9 passed in as percentages; the plugin will calculate the total and per-slice
10 percentages internally.
12 * Created by Brian Medendorp
14 * Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
16 The plugin supports these options:
21 radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
22 innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
23 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
24 tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
26 top: integer value to move the pie up or down
27 left: integer value to move the pie left or right, or 'auto'
30 color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
31 width: integer pixel width of the stroke
34 show: true/false, or 'auto'
35 formatter: a user-defined function that modifies the text/style of the label text
36 radius: 0-1 for percentage of fullsize, or a specified pixel length
38 color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
41 threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
44 threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
45 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
46 label: any text value of what the combined slice should be labeled
54 More detail and specific examples can be found in the included HTML file.
60 // Maximum redraw attempts when fitting labels within the plot
62 var REDRAW_ATTEMPTS
= 10;
64 // Factor by which to shrink the pie when fitting labels within the plot
66 var REDRAW_SHRINK
= 0.95;
79 // interactive variables
83 // add hook to determine if pie plugin in enabled, and then perform necessary operations
85 plot
.hooks
.processOptions
.push(function(plot
, options
) {
86 if (options
.series
.pie
.show
) {
88 options
.grid
.show
= false;
92 if (options
.series
.pie
.label
.show
== "auto") {
93 if (options
.legend
.show
) {
94 options
.series
.pie
.label
.show
= false;
96 options
.series
.pie
.label
.show
= true;
102 if (options
.series
.pie
.radius
== "auto") {
103 if (options
.series
.pie
.label
.show
) {
104 options
.series
.pie
.radius
= 3/4;
106 options
.series
.pie
.radius
= 1;
112 if (options
.series
.pie
.tilt
> 1) {
113 options
.series
.pie
.tilt
= 1;
114 } else if (options
.series
.pie
.tilt
< 0) {
115 options
.series
.pie
.tilt
= 0;
120 plot
.hooks
.bindEvents
.push(function(plot
, eventHolder
) {
121 var options
= plot
.getOptions();
122 if (options
.series
.pie
.show
) {
123 if (options
.grid
.hoverable
) {
124 eventHolder
.unbind("mousemove").mousemove(onMouseMove
);
126 if (options
.grid
.clickable
) {
127 eventHolder
.unbind("click").click(onClick
);
132 plot
.hooks
.processDatapoints
.push(function(plot
, series
, data
, datapoints
) {
133 var options
= plot
.getOptions();
134 if (options
.series
.pie
.show
) {
135 processDatapoints(plot
, series
, data
, datapoints
);
139 plot
.hooks
.drawOverlay
.push(function(plot
, octx
) {
140 var options
= plot
.getOptions();
141 if (options
.series
.pie
.show
) {
142 drawOverlay(plot
, octx
);
146 plot
.hooks
.draw
.push(function(plot
, newCtx
) {
147 var options
= plot
.getOptions();
148 if (options
.series
.pie
.show
) {
153 function processDatapoints(plot
, series
, datapoints
) {
156 canvas
= plot
.getCanvas();
157 target
= $(canvas
).parent();
158 options
= plot
.getOptions();
159 plot
.setData(combine(plot
.getData()));
163 function combine(data
) {
168 color
= options
.series
.pie
.combine
.color
,
171 // Fix up the raw data from Flot, ensuring the data is numeric
173 for (var i
= 0; i
< data
.length
; ++i
) {
175 var value
= data
[i
].data
;
177 // If the data is an array, we'll assume that it's a standard
178 // Flot x-y pair, and are concerned only with the second value.
180 // Note how we use the original array, rather than creating a
181 // new one; this is more efficient and preserves any extra data
182 // that the user may have stored in higher indexes.
184 if ($.isArray(value
) && value
.length
== 1) {
188 if ($.isArray(value
)) {
189 // Equivalent to $.isNumeric() but compatible with jQuery < 1.7
190 if (!isNaN(parseFloat(value
[1])) && isFinite(value
[1])) {
191 value
[1] = +value
[1];
195 } else if (!isNaN(parseFloat(value
)) && isFinite(value
)) {
201 data
[i
].data
= [value
];
204 // Sum up all the slices, so we can calculate percentages for each
206 for (var i
= 0; i
< data
.length
; ++i
) {
207 total
+= data
[i
].data
[0][1];
210 // Count the number of slices with percentages below the combine
211 // threshold; if it turns out to be just one, we won't combine.
213 for (var i
= 0; i
< data
.length
; ++i
) {
214 var value
= data
[i
].data
[0][1];
215 if (value
/ total
<= options
.series
.pie
.combine
.threshold
) {
219 color
= data
[i
].color
;
224 for (var i
= 0; i
< data
.length
; ++i
) {
225 var value
= data
[i
].data
[0][1];
226 if (numCombined
< 2 || value
/ total
> options
.series
.pie
.combine
.threshold
) {
229 color
: data
[i
].color
,
230 label
: data
[i
].label
,
231 angle
: value
* Math
.PI
* 2 / total
,
232 percent
: value
/ (total
/ 100)
237 if (numCombined
> 1) {
239 data
: [[1, combined
]],
241 label
: options
.series
.pie
.combine
.label
,
242 angle
: combined
* Math
.PI
* 2 / total
,
243 percent
: combined
/ (total
/ 100)
250 function draw(plot
, newCtx
) {
253 return; // if no series were passed
256 var canvasWidth
= plot
.getPlaceholder().width(),
257 canvasHeight
= plot
.getPlaceholder().height(),
258 legendWidth
= target
.children().filter(".legend").children().width() || 0;
262 // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
264 // When combining smaller slices into an 'other' slice, we need to
265 // add a new series. Since Flot gives plugins no way to modify the
266 // list of series, the pie plugin uses a hack where the first call
267 // to processDatapoints results in a call to setData with the new
268 // list of series, then subsequent processDatapoints do nothing.
270 // The plugin-global 'processed' flag is used to control this hack;
271 // it starts out false, and is set to true after the first call to
272 // processDatapoints.
274 // Unfortunately this turns future setData calls into no-ops; they
275 // call processDatapoints, the flag is true, and nothing happens.
277 // To fix this we'll set the flag back to false here in draw, when
278 // all series have been processed, so the next sequence of calls to
279 // processDatapoints once again starts out with a slice-combine.
280 // This is really a hack; in 0.9 we need to give plugins a proper
281 // way to modify series before any processing begins.
285 // calculate maximum radius and center point
287 maxRadius
= Math
.min(canvasWidth
, canvasHeight
/ options
.series
.pie
.tilt
) / 2;
288 centerTop
= canvasHeight
/ 2 + options
.series
.pie
.offset
.top
;
289 centerLeft
= canvasWidth
/ 2;
291 if (options
.series
.pie
.offset
.left
== "auto") {
292 if (options
.legend
.position
.match("w")) {
293 centerLeft
+= legendWidth
/ 2;
295 centerLeft
-= legendWidth
/ 2;
297 if (centerLeft
< maxRadius
) {
298 centerLeft
= maxRadius
;
299 } else if (centerLeft
> canvasWidth
- maxRadius
) {
300 centerLeft
= canvasWidth
- maxRadius
;
303 centerLeft
+= options
.series
.pie
.offset
.left
;
306 var slices
= plot
.getData(),
309 // Keep shrinking the pie's radius until drawPie returns true,
310 // indicating that all the labels fit, or we try too many times.
314 maxRadius
*= REDRAW_SHRINK
;
318 if (options
.series
.pie
.tilt
<= 0.8) {
321 } while (!drawPie() && attempts
< REDRAW_ATTEMPTS
)
323 if (attempts
>= REDRAW_ATTEMPTS
) {
325 target
.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
328 if (plot
.setSeries
&& plot
.insertLegend
) {
329 plot
.setSeries(slices
);
333 // we're actually done at this point, just defining internal functions at this point
336 ctx
.clearRect(0, 0, canvasWidth
, canvasHeight
);
337 target
.children().filter(".pieLabel, .pieLabelBackground").remove();
340 function drawShadow() {
342 var shadowLeft
= options
.series
.pie
.shadow
.left
;
343 var shadowTop
= options
.series
.pie
.shadow
.top
;
345 var alpha
= options
.series
.pie
.shadow
.alpha
;
346 var radius
= options
.series
.pie
.radius
> 1 ? options
.series
.pie
.radius
: maxRadius
* options
.series
.pie
.radius
;
348 if (radius
>= canvasWidth
/ 2 - shadowLeft
|| radius
* options
.series
.pie
.tilt
>= canvasHeight
/ 2 - shadowTop
|| radius
<= edge
) {
349 return; // shadow would be outside canvas, so don't draw it
353 ctx
.translate(shadowLeft
,shadowTop
);
354 ctx
.globalAlpha
= alpha
;
355 ctx
.fillStyle
= "#000";
357 // center and rotate to starting position
359 ctx
.translate(centerLeft
,centerTop
);
360 ctx
.scale(1, options
.series
.pie
.tilt
);
364 for (var i
= 1; i
<= edge
; i
++) {
366 ctx
.arc(0, 0, radius
, 0, Math
.PI
* 2, false);
376 var startAngle
= Math
.PI
* options
.series
.pie
.startAngle
;
377 var radius
= options
.series
.pie
.radius
> 1 ? options
.series
.pie
.radius
: maxRadius
* options
.series
.pie
.radius
;
379 // center and rotate to starting position
382 ctx
.translate(centerLeft
,centerTop
);
383 ctx
.scale(1, options
.series
.pie
.tilt
);
384 //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
389 var currentAngle
= startAngle
;
390 for (var i
= 0; i
< slices
.length
; ++i
) {
391 slices
[i
].startAngle
= currentAngle
;
392 drawSlice(slices
[i
].angle
, slices
[i
].color
, true);
396 // draw slice outlines
398 if (options
.series
.pie
.stroke
.width
> 0) {
400 ctx
.lineWidth
= options
.series
.pie
.stroke
.width
;
401 currentAngle
= startAngle
;
402 for (var i
= 0; i
< slices
.length
; ++i
) {
403 drawSlice(slices
[i
].angle
, options
.series
.pie
.stroke
.color
, false);
414 // Draw the labels, returning true if they fit within the plot
416 if (options
.series
.pie
.label
.show
) {
420 function drawSlice(angle
, color
, fill
) {
422 if (angle
<= 0 || isNaN(angle
)) {
427 ctx
.fillStyle
= color
;
429 ctx
.strokeStyle
= color
;
430 ctx
.lineJoin
= "round";
434 if (Math
.abs(angle
- Math
.PI
* 2) > 0.000000001) {
435 ctx
.moveTo(0, 0); // Center of the pie
438 //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
439 ctx
.arc(0, 0, radius
,currentAngle
, currentAngle
+ angle
/ 2, false);
440 ctx
.arc(0, 0, radius
,currentAngle
+ angle
/ 2, currentAngle
+ angle
, false);
442 //ctx.rotate(angle); // This doesn't work properly in Opera
443 currentAngle
+= angle
;
452 function drawLabels() {
454 var currentAngle
= startAngle
;
455 var radius
= options
.series
.pie
.label
.radius
> 1 ? options
.series
.pie
.label
.radius
: maxRadius
* options
.series
.pie
.label
.radius
;
457 for (var i
= 0; i
< slices
.length
; ++i
) {
458 if (slices
[i
].percent
>= options
.series
.pie
.label
.threshold
* 100) {
459 if (!drawLabel(slices
[i
], currentAngle
, i
)) {
463 currentAngle
+= slices
[i
].angle
;
468 function drawLabel(slice
, startAngle
, index
) {
470 if (slice
.data
[0][1] == 0) {
476 var lf
= options
.legend
.labelFormatter
, text
, plf
= options
.series
.pie
.label
.formatter
;
479 text
= lf(slice
.label
, slice
);
485 text
= plf(text
, slice
);
488 var halfAngle
= ((startAngle
+ slice
.angle
) + startAngle
) / 2;
489 var x
= centerLeft
+ Math
.round(Math
.cos(halfAngle
) * radius
);
490 var y
= centerTop
+ Math
.round(Math
.sin(halfAngle
) * radius
) * options
.series
.pie
.tilt
;
492 var html
= "<span class='pieLabel' id='pieLabel" + index
+ "' style='position:absolute;top:" + y
+ "px;left:" + x
+ "px;'>" + text
+ "</span>";
495 var label
= target
.children("#pieLabel" + index
);
496 var labelTop
= (y
- label
.height() / 2);
497 var labelLeft
= (x
- label
.width() / 2);
499 label
.css("top", labelTop
);
500 label
.css("left", labelLeft
);
502 // check to make sure that the label is not outside the canvas
504 if (0 - labelTop
> 0 || 0 - labelLeft
> 0 || canvasHeight
- (labelTop
+ label
.height()) < 0 || canvasWidth
- (labelLeft
+ label
.width()) < 0) {
508 if (options
.series
.pie
.label
.background
.opacity
!= 0) {
510 // put in the transparent background separately to avoid blended labels and label boxes
512 var c
= options
.series
.pie
.label
.background
.color
;
518 var pos
= "top:" + labelTop
+ "px;left:" + labelLeft
+ "px;";
519 $("<div class='pieLabelBackground' style='position:absolute;width:" + label
.width() + "px;height:" + label
.height() + "px;" + pos
+ "background-color:" + c
+ ";'></div>")
520 .css("opacity", options
.series
.pie
.label
.background
.opacity
)
521 .insertBefore(label
);
525 } // end individual label function
526 } // end drawLabels function
527 } // end drawPie function
528 } // end draw function
530 // Placed here because it needs to be accessed from multiple locations
532 function drawDonutHole(layer
) {
533 if (options
.series
.pie
.innerRadius
> 0) {
535 // subtract the center
538 var innerRadius
= options
.series
.pie
.innerRadius
> 1 ? options
.series
.pie
.innerRadius
: maxRadius
* options
.series
.pie
.innerRadius
;
539 layer
.globalCompositeOperation
= "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
541 layer
.fillStyle
= options
.series
.pie
.stroke
.color
;
542 layer
.arc(0, 0, innerRadius
, 0, Math
.PI
* 2, false);
551 layer
.strokeStyle
= options
.series
.pie
.stroke
.color
;
552 layer
.arc(0, 0, innerRadius
, 0, Math
.PI
* 2, false);
557 // TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
561 //-- Additional Interactive related functions --
563 function isPointInPoly(poly
, pt
) {
564 for(var c
= false, i
= -1, l
= poly
.length
, j
= l
- 1; ++i
< l
; j
= i
)
565 ((poly
[i
][1] <= pt
[1] && pt
[1] < poly
[j
][1]) || (poly
[j
][1] <= pt
[1] && pt
[1]< poly
[i
][1]))
566 && (pt
[0] < (poly
[j
][0] - poly
[i
][0]) * (pt
[1] - poly
[i
][1]) / (poly
[j
][1] - poly
[i
][1]) + poly
[i
][0])
571 function findNearbySlice(mouseX
, mouseY
) {
573 var slices
= plot
.getData(),
574 options
= plot
.getOptions(),
575 radius
= options
.series
.pie
.radius
> 1 ? options
.series
.pie
.radius
: maxRadius
* options
.series
.pie
.radius
,
578 for (var i
= 0; i
< slices
.length
; ++i
) {
586 ctx
.moveTo(0, 0); // Center of the pie
587 //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
588 ctx
.arc(0, 0, radius
, s
.startAngle
, s
.startAngle
+ s
.angle
/ 2, false);
589 ctx
.arc(0, 0, radius
, s
.startAngle
+ s
.angle
/ 2, s
.startAngle
+ s
.angle
, false);
591 x
= mouseX
- centerLeft
;
592 y
= mouseY
- centerTop
;
594 if (ctx
.isPointInPath
) {
595 if (ctx
.isPointInPath(mouseX
- centerLeft
, mouseY
- centerTop
)) {
598 datapoint
: [s
.percent
, s
.data
],
606 // excanvas for IE doesn;t support isPointInPath, this is a workaround.
608 var p1X
= radius
* Math
.cos(s
.startAngle
),
609 p1Y
= radius
* Math
.sin(s
.startAngle
),
610 p2X
= radius
* Math
.cos(s
.startAngle
+ s
.angle
/ 4),
611 p2Y
= radius
* Math
.sin(s
.startAngle
+ s
.angle
/ 4),
612 p3X
= radius
* Math
.cos(s
.startAngle
+ s
.angle
/ 2),
613 p3Y
= radius
* Math
.sin(s
.startAngle
+ s
.angle
/ 2),
614 p4X
= radius
* Math
.cos(s
.startAngle
+ s
.angle
/ 1.5),
615 p4Y
= radius
* Math
.sin(s
.startAngle
+ s
.angle
/ 1.5),
616 p5X
= radius
* Math
.cos(s
.startAngle
+ s
.angle
),
617 p5Y
= radius
* Math
.sin(s
.startAngle
+ s
.angle
),
618 arrPoly
= [[0, 0], [p1X
, p1Y
], [p2X
, p2Y
], [p3X
, p3Y
], [p4X
, p4Y
], [p5X
, p5Y
]],
621 // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
623 if (isPointInPoly(arrPoly
, arrPoint
)) {
626 datapoint
: [s
.percent
, s
.data
],
641 function onMouseMove(e
) {
642 triggerClickHoverEvent("plothover", e
);
645 function onClick(e
) {
646 triggerClickHoverEvent("plotclick", e
);
649 // trigger click or hover event (they send the same parameters so we share their code)
651 function triggerClickHoverEvent(eventname
, e
) {
653 var offset
= plot
.offset();
654 var canvasX
= parseInt(e
.pageX
- offset
.left
);
655 var canvasY
= parseInt(e
.pageY
- offset
.top
);
656 var item
= findNearbySlice(canvasX
, canvasY
);
658 if (options
.grid
.autoHighlight
) {
660 // clear auto-highlights
662 for (var i
= 0; i
< highlights
.length
; ++i
) {
663 var h
= highlights
[i
];
664 if (h
.auto
== eventname
&& !(item
&& h
.series
== item
.series
)) {
665 unhighlight(h
.series
);
670 // highlight the slice
673 highlight(item
.series
, eventname
);
676 // trigger any hover bind events
678 var pos
= { pageX
: e
.pageX
, pageY
: e
.pageY
};
679 target
.trigger(eventname
, [pos
, item
]);
682 function highlight(s
, auto
) {
683 //if (typeof s == "number") {
687 var i
= indexOfHighlight(s
);
690 highlights
.push({ series
: s
, auto
: auto
});
691 plot
.triggerRedrawOverlay();
693 highlights
[i
].auto
= false;
697 function unhighlight(s
) {
700 plot
.triggerRedrawOverlay();
703 //if (typeof s == "number") {
707 var i
= indexOfHighlight(s
);
710 highlights
.splice(i
, 1);
711 plot
.triggerRedrawOverlay();
715 function indexOfHighlight(s
) {
716 for (var i
= 0; i
< highlights
.length
; ++i
) {
717 var h
= highlights
[i
];
724 function drawOverlay(plot
, octx
) {
726 var options
= plot
.getOptions();
728 var radius
= options
.series
.pie
.radius
> 1 ? options
.series
.pie
.radius
: maxRadius
* options
.series
.pie
.radius
;
731 octx
.translate(centerLeft
, centerTop
);
732 octx
.scale(1, options
.series
.pie
.tilt
);
734 for (var i
= 0; i
< highlights
.length
; ++i
) {
735 drawHighlight(highlights
[i
].series
);
742 function drawHighlight(series
) {
744 if (series
.angle
<= 0 || isNaN(series
.angle
)) {
748 //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
749 octx
.fillStyle
= "rgba(255, 255, 255, " + options
.series
.pie
.highlight
.opacity
+ ")"; // this is temporary until we have access to parseColor
751 if (Math
.abs(series
.angle
- Math
.PI
* 2) > 0.000000001) {
752 octx
.moveTo(0, 0); // Center of the pie
754 octx
.arc(0, 0, radius
, series
.startAngle
, series
.startAngle
+ series
.angle
/ 2, false);
755 octx
.arc(0, 0, radius
, series
.startAngle
+ series
.angle
/ 2, series
.startAngle
+ series
.angle
, false);
760 } // end init (plugin body)
762 // define pie specific options and their default values
768 radius
: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
769 innerRadius
: 0, /* for donut */
773 left
: 5, // shadow left offset
774 top
: 15, // shadow top offset
775 alpha
: 0.02 // shadow alpha
787 formatter: function(label
, slice
) {
788 return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice
.color
+ ";'>" + label
+ "<br/>" + Math
.round(slice
.percent
) + "%</div>";
789 }, // formatter function
790 radius
: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
795 threshold
: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
798 threshold
: -1, // percentage at which to combine little slices into one larger slice
799 color
: null, // color to give the new slice (auto-generated if null)
800 label
: "Other" // label to give the new slice
803 //color: "#fff", // will add this functionality once parseColor is available
810 $.plot
.plugins
.push({