hjg
2023-11-18 bb48edb3d9faaaeab0088151c86fc24137acdb08
提交 | 用户 | 时间
58d006 1 /* Flot plugin for rendering pie charts.
A 2
3 Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 Licensed under the MIT license.
5
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.
11
12 * Created by Brian Medendorp
13
14 * Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
15
16 The plugin supports these options:
17
18     series: {
19         pie: {
20             show: true/false
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)
25             offset: {
26                 top: integer value to move the pie up or down
27                 left: integer value to move the pie left or right, or 'auto'
28             },
29             stroke: {
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
32             },
33             label: {
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
37                 background: {
38                     color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
39                     opacity: 0-1
40                 },
41                 threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
42             },
43             combine: {
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
47             }
48             highlight: {
49                 opacity: 0-1
50             }
51         }
52     }
53
54 More detail and specific examples can be found in the included HTML file.
55
56 */
57
58 (function($) {
59
60     // Maximum redraw attempts when fitting labels within the plot
61
62     var REDRAW_ATTEMPTS = 10;
63
64     // Factor by which to shrink the pie when fitting labels within the plot
65
66     var REDRAW_SHRINK = 0.95;
67
68     function init(plot) {
69
70         var canvas = null,
71             target = null,
72             options = null,
73             maxRadius = null,
74             centerLeft = null,
75             centerTop = null,
76             processed = false,
77             ctx = null;
78
79         // interactive variables
80
81         var highlights = [];
82
83         // add hook to determine if pie plugin in enabled, and then perform necessary operations
84
85         plot.hooks.processOptions.push(function(plot, options) {
86             if (options.series.pie.show) {
87
88                 options.grid.show = false;
89
90                 // set labels.show
91
92                 if (options.series.pie.label.show == "auto") {
93                     if (options.legend.show) {
94                         options.series.pie.label.show = false;
95                     } else {
96                         options.series.pie.label.show = true;
97                     }
98                 }
99
100                 // set radius
101
102                 if (options.series.pie.radius == "auto") {
103                     if (options.series.pie.label.show) {
104                         options.series.pie.radius = 3/4;
105                     } else {
106                         options.series.pie.radius = 1;
107                     }
108                 }
109
110                 // ensure sane tilt
111
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;
116                 }
117             }
118         });
119
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);
125                 }
126                 if (options.grid.clickable) {
127                     eventHolder.unbind("click").click(onClick);
128                 }
129             }
130         });
131
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);
136             }
137         });
138
139         plot.hooks.drawOverlay.push(function(plot, octx) {
140             var options = plot.getOptions();
141             if (options.series.pie.show) {
142                 drawOverlay(plot, octx);
143             }
144         });
145
146         plot.hooks.draw.push(function(plot, newCtx) {
147             var options = plot.getOptions();
148             if (options.series.pie.show) {
149                 draw(plot, newCtx);
150             }
151         });
152
153         function processDatapoints(plot, series, datapoints) {
154             if (!processed)    {
155                 processed = true;
156                 canvas = plot.getCanvas();
157                 target = $(canvas).parent();
158                 options = plot.getOptions();
159                 plot.setData(combine(plot.getData()));
160             }
161         }
162
163         function combine(data) {
164
165             var total = 0,
166                 combined = 0,
167                 numCombined = 0,
168                 color = options.series.pie.combine.color,
169                 newdata = [];
170
171             // Fix up the raw data from Flot, ensuring the data is numeric
172
173             for (var i = 0; i < data.length; ++i) {
174
175                 var value = data[i].data;
176
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.
179
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.
183
184                 if ($.isArray(value) && value.length == 1) {
185                     value = value[0];
186                 }
187
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];
192                     } else {
193                         value[1] = 0;
194                     }
195                 } else if (!isNaN(parseFloat(value)) && isFinite(value)) {
196                     value = [1, +value];
197                 } else {
198                     value = [1, 0];
199                 }
200
201                 data[i].data = [value];
202             }
203
204             // Sum up all the slices, so we can calculate percentages for each
205
206             for (var i = 0; i < data.length; ++i) {
207                 total += data[i].data[0][1];
208             }
209
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.
212
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) {
216                     combined += value;
217                     numCombined++;
218                     if (!color) {
219                         color = data[i].color;
220                     }
221                 }
222             }
223
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) {
227                     newdata.push(
228                         $.extend(data[i], {     /* extend to allow keeping all other original data values
229                                                    and using them e.g. in labelFormatter. */
230                             data: [[1, value]],
231                             color: data[i].color,
232                             label: data[i].label,
233                             angle: value * Math.PI * 2 / total,
234                             percent: value / (total / 100)
235                         })
236                     );
237                 }
238             }
239
240             if (numCombined > 1) {
241                 newdata.push({
242                     data: [[1, combined]],
243                     color: color,
244                     label: options.series.pie.combine.label,
245                     angle: combined * Math.PI * 2 / total,
246                     percent: combined / (total / 100)
247                 });
248             }
249
250             return newdata;
251         }
252
253         function draw(plot, newCtx) {
254
255             if (!target) {
256                 return; // if no series were passed
257             }
258
259             var canvasWidth = plot.getPlaceholder().width(),
260                 canvasHeight = plot.getPlaceholder().height(),
261                 legendWidth = target.children().filter(".legend").children().width() || 0;
262
263             ctx = newCtx;
264
265             // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
266
267             // When combining smaller slices into an 'other' slice, we need to
268             // add a new series.  Since Flot gives plugins no way to modify the
269             // list of series, the pie plugin uses a hack where the first call
270             // to processDatapoints results in a call to setData with the new
271             // list of series, then subsequent processDatapoints do nothing.
272
273             // The plugin-global 'processed' flag is used to control this hack;
274             // it starts out false, and is set to true after the first call to
275             // processDatapoints.
276
277             // Unfortunately this turns future setData calls into no-ops; they
278             // call processDatapoints, the flag is true, and nothing happens.
279
280             // To fix this we'll set the flag back to false here in draw, when
281             // all series have been processed, so the next sequence of calls to
282             // processDatapoints once again starts out with a slice-combine.
283             // This is really a hack; in 0.9 we need to give plugins a proper
284             // way to modify series before any processing begins.
285
286             processed = false;
287
288             // calculate maximum radius and center point
289
290             maxRadius =  Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
291             centerTop = canvasHeight / 2 + options.series.pie.offset.top;
292             centerLeft = canvasWidth / 2;
293
294             if (options.series.pie.offset.left == "auto") {
295                 if (options.legend.position.match("w")) {
296                     centerLeft += legendWidth / 2;
297                 } else {
298                     centerLeft -= legendWidth / 2;
299                 }
300                 if (centerLeft < maxRadius) {
301                     centerLeft = maxRadius;
302                 } else if (centerLeft > canvasWidth - maxRadius) {
303                     centerLeft = canvasWidth - maxRadius;
304                 }
305             } else {
306                 centerLeft += options.series.pie.offset.left;
307             }
308
309             var slices = plot.getData(),
310                 attempts = 0;
311
312             // Keep shrinking the pie's radius until drawPie returns true,
313             // indicating that all the labels fit, or we try too many times.
314
315             do {
316                 if (attempts > 0) {
317                     maxRadius *= REDRAW_SHRINK;
318                 }
319                 attempts += 1;
320                 clear();
321                 if (options.series.pie.tilt <= 0.8) {
322                     drawShadow();
323                 }
324             } while (!drawPie() && attempts < REDRAW_ATTEMPTS)
325
326             if (attempts >= REDRAW_ATTEMPTS) {
327                 clear();
328                 target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
329             }
330
331             if (plot.setSeries && plot.insertLegend) {
332                 plot.setSeries(slices);
333                 plot.insertLegend();
334             }
335
336             // we're actually done at this point, just defining internal functions at this point
337
338             function clear() {
339                 ctx.clearRect(0, 0, canvasWidth, canvasHeight);
340                 target.children().filter(".pieLabel, .pieLabelBackground").remove();
341             }
342
343             function drawShadow() {
344
345                 var shadowLeft = options.series.pie.shadow.left;
346                 var shadowTop = options.series.pie.shadow.top;
347                 var edge = 10;
348                 var alpha = options.series.pie.shadow.alpha;
349                 var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
350
351                 if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
352                     return;    // shadow would be outside canvas, so don't draw it
353                 }
354
355                 ctx.save();
356                 ctx.translate(shadowLeft,shadowTop);
357                 ctx.globalAlpha = alpha;
358                 ctx.fillStyle = "#000";
359
360                 // center and rotate to starting position
361
362                 ctx.translate(centerLeft,centerTop);
363                 ctx.scale(1, options.series.pie.tilt);
364
365                 //radius -= edge;
366
367                 for (var i = 1; i <= edge; i++) {
368                     ctx.beginPath();
369                     ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
370                     ctx.fill();
371                     radius -= i;
372                 }
373
374                 ctx.restore();
375             }
376
377             function drawPie() {
378
379                 var startAngle = Math.PI * options.series.pie.startAngle;
380                 var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
381
382                 // center and rotate to starting position
383
384                 ctx.save();
385                 ctx.translate(centerLeft,centerTop);
386                 ctx.scale(1, options.series.pie.tilt);
387                 //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
388
389                 // draw slices
390
391                 ctx.save();
392                 var currentAngle = startAngle;
393                 for (var i = 0; i < slices.length; ++i) {
394                     slices[i].startAngle = currentAngle;
395                     drawSlice(slices[i].angle, slices[i].color, true);
396                 }
397                 ctx.restore();
398
399                 // draw slice outlines
400
401                 if (options.series.pie.stroke.width > 0) {
402                     ctx.save();
403                     ctx.lineWidth = options.series.pie.stroke.width;
404                     currentAngle = startAngle;
405                     for (var i = 0; i < slices.length; ++i) {
406                         drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
407                     }
408                     ctx.restore();
409                 }
410
411                 // draw donut hole
412
413                 drawDonutHole(ctx);
414
415                 ctx.restore();
416
417                 // Draw the labels, returning true if they fit within the plot
418
419                 if (options.series.pie.label.show) {
420                     return drawLabels();
421                 } else return true;
422
423                 function drawSlice(angle, color, fill) {
424
425                     if (angle <= 0 || isNaN(angle)) {
426                         return;
427                     }
428
429                     if (fill) {
430                         ctx.fillStyle = color;
431                     } else {
432                         ctx.strokeStyle = color;
433                         ctx.lineJoin = "round";
434                     }
435
436                     ctx.beginPath();
437                     if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
438                         ctx.moveTo(0, 0); // Center of the pie
439                     }
440
441                     //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
442                     ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
443                     ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
444                     ctx.closePath();
445                     //ctx.rotate(angle); // This doesn't work properly in Opera
446                     currentAngle += angle;
447
448                     if (fill) {
449                         ctx.fill();
450                     } else {
451                         ctx.stroke();
452                     }
453                 }
454
455                 function drawLabels() {
456
457                     var currentAngle = startAngle;
458                     var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
459
460                     for (var i = 0; i < slices.length; ++i) {
461                         if (slices[i].percent >= options.series.pie.label.threshold * 100) {
462                             if (!drawLabel(slices[i], currentAngle, i)) {
463                                 return false;
464                             }
465                         }
466                         currentAngle += slices[i].angle;
467                     }
468
469                     return true;
470
471                     function drawLabel(slice, startAngle, index) {
472
473                         if (slice.data[0][1] == 0) {
474                             return true;
475                         }
476
477                         // format label text
478
479                         var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
480
481                         if (lf) {
482                             text = lf(slice.label, slice);
483                         } else {
484                             text = slice.label;
485                         }
486
487                         if (plf) {
488                             text = plf(text, slice);
489                         }
490
491                         var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
492                         var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
493                         var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
494
495                         var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
496                         target.append(html);
497
498                         var label = target.children("#pieLabel" + index);
499                         var labelTop = (y - label.height() / 2);
500                         var labelLeft = (x - label.width() / 2);
501
502                         label.css("top", labelTop);
503                         label.css("left", labelLeft);
504
505                         // check to make sure that the label is not outside the canvas
506
507                         if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
508                             return false;
509                         }
510
511                         if (options.series.pie.label.background.opacity != 0) {
512
513                             // put in the transparent background separately to avoid blended labels and label boxes
514
515                             var c = options.series.pie.label.background.color;
516
517                             if (c == null) {
518                                 c = slice.color;
519                             }
520
521                             var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
522                             $("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
523                                 .css("opacity", options.series.pie.label.background.opacity)
524                                 .insertBefore(label);
525                         }
526
527                         return true;
528                     } // end individual label function
529                 } // end drawLabels function
530             } // end drawPie function
531         } // end draw function
532
533         // Placed here because it needs to be accessed from multiple locations
534
535         function drawDonutHole(layer) {
536             if (options.series.pie.innerRadius > 0) {
537
538                 // subtract the center
539
540                 layer.save();
541                 var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
542                 layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
543                 layer.beginPath();
544                 layer.fillStyle = options.series.pie.stroke.color;
545                 layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
546                 layer.fill();
547                 layer.closePath();
548                 layer.restore();
549
550                 // add inner stroke
551
552                 layer.save();
553                 layer.beginPath();
554                 layer.strokeStyle = options.series.pie.stroke.color;
555                 layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
556                 layer.stroke();
557                 layer.closePath();
558                 layer.restore();
559
560                 // TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
561             }
562         }
563
564         //-- Additional Interactive related functions --
565
566         function isPointInPoly(poly, pt) {
567             for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
568                 ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
569                 && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
570                 && (c = !c);
571             return c;
572         }
573
574         function findNearbySlice(mouseX, mouseY) {
575
576             var slices = plot.getData(),
577                 options = plot.getOptions(),
578                 radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
579                 x, y;
580
581             for (var i = 0; i < slices.length; ++i) {
582
583                 var s = slices[i];
584
585                 if (s.pie.show) {
586
587                     ctx.save();
588                     ctx.beginPath();
589                     ctx.moveTo(0, 0); // Center of the pie
590                     //ctx.scale(1, options.series.pie.tilt);    // this actually seems to break everything when here.
591                     ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
592                     ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
593                     ctx.closePath();
594                     x = mouseX - centerLeft;
595                     y = mouseY - centerTop;
596
597                     if (ctx.isPointInPath) {
598                         if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
599                             ctx.restore();
600                             return {
601                                 datapoint: [s.percent, s.data],
602                                 dataIndex: 0,
603                                 series: s,
604                                 seriesIndex: i
605                             };
606                         }
607                     } else {
608
609                         // excanvas for IE doesn;t support isPointInPath, this is a workaround.
610
611                         var p1X = radius * Math.cos(s.startAngle),
612                             p1Y = radius * Math.sin(s.startAngle),
613                             p2X = radius * Math.cos(s.startAngle + s.angle / 4),
614                             p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
615                             p3X = radius * Math.cos(s.startAngle + s.angle / 2),
616                             p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
617                             p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
618                             p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
619                             p5X = radius * Math.cos(s.startAngle + s.angle),
620                             p5Y = radius * Math.sin(s.startAngle + s.angle),
621                             arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
622                             arrPoint = [x, y];
623
624                         // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
625
626                         if (isPointInPoly(arrPoly, arrPoint)) {
627                             ctx.restore();
628                             return {
629                                 datapoint: [s.percent, s.data],
630                                 dataIndex: 0,
631                                 series: s,
632                                 seriesIndex: i
633                             };
634                         }
635                     }
636
637                     ctx.restore();
638                 }
639             }
640
641             return null;
642         }
643
644         function onMouseMove(e) {
645             triggerClickHoverEvent("plothover", e);
646         }
647
648         function onClick(e) {
649             triggerClickHoverEvent("plotclick", e);
650         }
651
652         // trigger click or hover event (they send the same parameters so we share their code)
653
654         function triggerClickHoverEvent(eventname, e) {
655
656             var offset = plot.offset();
657             var canvasX = parseInt(e.pageX - offset.left);
658             var canvasY =  parseInt(e.pageY - offset.top);
659             var item = findNearbySlice(canvasX, canvasY);
660
661             if (options.grid.autoHighlight) {
662
663                 // clear auto-highlights
664
665                 for (var i = 0; i < highlights.length; ++i) {
666                     var h = highlights[i];
667                     if (h.auto == eventname && !(item && h.series == item.series)) {
668                         unhighlight(h.series);
669                     }
670                 }
671             }
672
673             // highlight the slice
674
675             if (item) {
676                 highlight(item.series, eventname);
677             }
678
679             // trigger any hover bind events
680
681             var pos = { pageX: e.pageX, pageY: e.pageY };
682             target.trigger(eventname, [pos, item]);
683         }
684
685         function highlight(s, auto) {
686             //if (typeof s == "number") {
687             //    s = series[s];
688             //}
689
690             var i = indexOfHighlight(s);
691
692             if (i == -1) {
693                 highlights.push({ series: s, auto: auto });
694                 plot.triggerRedrawOverlay();
695             } else if (!auto) {
696                 highlights[i].auto = false;
697             }
698         }
699
700         function unhighlight(s) {
701             if (s == null) {
702                 highlights = [];
703                 plot.triggerRedrawOverlay();
704             }
705
706             //if (typeof s == "number") {
707             //    s = series[s];
708             //}
709
710             var i = indexOfHighlight(s);
711
712             if (i != -1) {
713                 highlights.splice(i, 1);
714                 plot.triggerRedrawOverlay();
715             }
716         }
717
718         function indexOfHighlight(s) {
719             for (var i = 0; i < highlights.length; ++i) {
720                 var h = highlights[i];
721                 if (h.series == s)
722                     return i;
723             }
724             return -1;
725         }
726
727         function drawOverlay(plot, octx) {
728
729             var options = plot.getOptions();
730
731             var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
732
733             octx.save();
734             octx.translate(centerLeft, centerTop);
735             octx.scale(1, options.series.pie.tilt);
736
737             for (var i = 0; i < highlights.length; ++i) {
738                 drawHighlight(highlights[i].series);
739             }
740
741             drawDonutHole(octx);
742
743             octx.restore();
744
745             function drawHighlight(series) {
746
747                 if (series.angle <= 0 || isNaN(series.angle)) {
748                     return;
749                 }
750
751                 //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
752                 octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
753                 octx.beginPath();
754                 if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
755                     octx.moveTo(0, 0); // Center of the pie
756                 }
757                 octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
758                 octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
759                 octx.closePath();
760                 octx.fill();
761             }
762         }
763     } // end init (plugin body)
764
765     // define pie specific options and their default values
766
767     var options = {
768         series: {
769             pie: {
770                 show: false,
771                 radius: "auto",    // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
772                 innerRadius: 0, /* for donut */
773                 startAngle: 3/2,
774                 tilt: 1,
775                 shadow: {
776                     left: 5,    // shadow left offset
777                     top: 15,    // shadow top offset
778                     alpha: 0.02    // shadow alpha
779                 },
780                 offset: {
781                     top: 0,
782                     left: "auto"
783                 },
784                 stroke: {
785                     color: "#fff",
786                     width: 1
787                 },
788                 label: {
789                     show: "auto",
790                     formatter: function(label, slice) {
791                         return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
792                     },    // formatter function
793                     radius: 1,    // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
794                     background: {
795                         color: null,
796                         opacity: 0
797                     },
798                     threshold: 0    // percentage at which to hide the label (i.e. the slice is too narrow)
799                 },
800                 combine: {
801                     threshold: -1,    // percentage at which to combine little slices into one larger slice
802                     color: null,    // color to give the new slice (auto-generated if null)
803                     label: "Other"    // label to give the new slice
804                 },
805                 highlight: {
806                     //color: "#fff",        // will add this functionality once parseColor is available
807                     opacity: 0.5
808                 }
809             }
810         }
811     };
812
813     $.plot.plugins.push({
814         init: init,
815         options: options,
816         name: "pie",
817         version: "1.1"
818     });
819
820 })(jQuery);