]>
Commit | Line | Data |
---|---|---|
31f18b77 FG |
1 | /* Flot plugin for rendering pie charts. |
2 | ||
3 | Copyright (c) 2007-2013 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 | data: [[1, value]], | |
229 | color: data[i].color, | |
230 | label: data[i].label, | |
231 | angle: value * Math.PI * 2 / total, | |
232 | percent: value / (total / 100) | |
233 | }); | |
234 | } | |
235 | } | |
236 | ||
237 | if (numCombined > 1) { | |
238 | newdata.push({ | |
239 | data: [[1, combined]], | |
240 | color: color, | |
241 | label: options.series.pie.combine.label, | |
242 | angle: combined * Math.PI * 2 / total, | |
243 | percent: combined / (total / 100) | |
244 | }); | |
245 | } | |
246 | ||
247 | return newdata; | |
248 | } | |
249 | ||
250 | function draw(plot, newCtx) { | |
251 | ||
252 | if (!target) { | |
253 | return; // if no series were passed | |
254 | } | |
255 | ||
256 | var canvasWidth = plot.getPlaceholder().width(), | |
257 | canvasHeight = plot.getPlaceholder().height(), | |
258 | legendWidth = target.children().filter(".legend").children().width() || 0; | |
259 | ||
260 | ctx = newCtx; | |
261 | ||
262 | // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! | |
263 | ||
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. | |
269 | ||
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. | |
273 | ||
274 | // Unfortunately this turns future setData calls into no-ops; they | |
275 | // call processDatapoints, the flag is true, and nothing happens. | |
276 | ||
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. | |
282 | ||
283 | processed = false; | |
284 | ||
285 | // calculate maximum radius and center point | |
286 | ||
287 | maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; | |
288 | centerTop = canvasHeight / 2 + options.series.pie.offset.top; | |
289 | centerLeft = canvasWidth / 2; | |
290 | ||
291 | if (options.series.pie.offset.left == "auto") { | |
292 | if (options.legend.position.match("w")) { | |
293 | centerLeft += legendWidth / 2; | |
294 | } else { | |
295 | centerLeft -= legendWidth / 2; | |
296 | } | |
297 | if (centerLeft < maxRadius) { | |
298 | centerLeft = maxRadius; | |
299 | } else if (centerLeft > canvasWidth - maxRadius) { | |
300 | centerLeft = canvasWidth - maxRadius; | |
301 | } | |
302 | } else { | |
303 | centerLeft += options.series.pie.offset.left; | |
304 | } | |
305 | ||
306 | var slices = plot.getData(), | |
307 | attempts = 0; | |
308 | ||
309 | // Keep shrinking the pie's radius until drawPie returns true, | |
310 | // indicating that all the labels fit, or we try too many times. | |
311 | ||
312 | do { | |
313 | if (attempts > 0) { | |
314 | maxRadius *= REDRAW_SHRINK; | |
315 | } | |
316 | attempts += 1; | |
317 | clear(); | |
318 | if (options.series.pie.tilt <= 0.8) { | |
319 | drawShadow(); | |
320 | } | |
321 | } while (!drawPie() && attempts < REDRAW_ATTEMPTS); | |
322 | ||
323 | if (attempts >= REDRAW_ATTEMPTS) { | |
324 | clear(); | |
325 | target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>"); | |
326 | } | |
327 | ||
328 | if (plot.setSeries && plot.insertLegend) { | |
329 | plot.setSeries(slices); | |
330 | plot.insertLegend(); | |
331 | } | |
332 | ||
333 | // we're actually done at this point, just defining internal functions at this point | |
334 | ||
335 | function clear() { | |
336 | ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
337 | target.children().filter(".pieLabel, .pieLabelBackground").remove(); | |
338 | } | |
339 | ||
340 | function drawShadow() { | |
341 | ||
342 | var shadowLeft = options.series.pie.shadow.left; | |
343 | var shadowTop = options.series.pie.shadow.top; | |
344 | var edge = 10; | |
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; | |
347 | ||
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 | |
350 | } | |
351 | ||
352 | ctx.save(); | |
353 | ctx.translate(shadowLeft,shadowTop); | |
354 | ctx.globalAlpha = alpha; | |
355 | ctx.fillStyle = "#000"; | |
356 | ||
357 | // center and rotate to starting position | |
358 | ||
359 | ctx.translate(centerLeft,centerTop); | |
360 | ctx.scale(1, options.series.pie.tilt); | |
361 | ||
362 | //radius -= edge; | |
363 | ||
364 | for (var i = 1; i <= edge; i++) { | |
365 | ctx.beginPath(); | |
366 | ctx.arc(0, 0, radius, 0, Math.PI * 2, false); | |
367 | ctx.fill(); | |
368 | radius -= i; | |
369 | } | |
370 | ||
371 | ctx.restore(); | |
372 | } | |
373 | ||
374 | function drawPie() { | |
375 | ||
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; | |
378 | ||
379 | // center and rotate to starting position | |
380 | ||
381 | ctx.save(); | |
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 | |
385 | ||
386 | // draw slices | |
387 | ||
388 | ctx.save(); | |
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); | |
393 | } | |
394 | ctx.restore(); | |
395 | ||
396 | // draw slice outlines | |
397 | ||
398 | if (options.series.pie.stroke.width > 0) { | |
399 | ctx.save(); | |
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); | |
404 | } | |
405 | ctx.restore(); | |
406 | } | |
407 | ||
408 | // draw donut hole | |
409 | ||
410 | drawDonutHole(ctx); | |
411 | ||
412 | ctx.restore(); | |
413 | ||
414 | // Draw the labels, returning true if they fit within the plot | |
415 | ||
416 | if (options.series.pie.label.show) { | |
417 | return drawLabels(); | |
418 | } else return true; | |
419 | ||
420 | function drawSlice(angle, color, fill) { | |
421 | ||
422 | if (angle <= 0 || isNaN(angle)) { | |
423 | return; | |
424 | } | |
425 | ||
426 | if (fill) { | |
427 | ctx.fillStyle = color; | |
428 | } else { | |
429 | ctx.strokeStyle = color; | |
430 | ctx.lineJoin = "round"; | |
431 | } | |
432 | ||
433 | ctx.beginPath(); | |
434 | if (Math.abs(angle - Math.PI * 2) > 0.000000001) { | |
435 | ctx.moveTo(0, 0); // Center of the pie | |
436 | } | |
437 | ||
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); | |
441 | ctx.closePath(); | |
442 | //ctx.rotate(angle); // This doesn't work properly in Opera | |
443 | currentAngle += angle; | |
444 | ||
445 | if (fill) { | |
446 | ctx.fill(); | |
447 | } else { | |
448 | ctx.stroke(); | |
449 | } | |
450 | } | |
451 | ||
452 | function drawLabels() { | |
453 | ||
454 | var currentAngle = startAngle; | |
455 | var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; | |
456 | ||
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)) { | |
460 | return false; | |
461 | } | |
462 | } | |
463 | currentAngle += slices[i].angle; | |
464 | } | |
465 | ||
466 | return true; | |
467 | ||
468 | function drawLabel(slice, startAngle, index) { | |
469 | ||
470 | if (slice.data[0][1] == 0) { | |
471 | return true; | |
472 | } | |
473 | ||
474 | // format label text | |
475 | ||
476 | var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; | |
477 | ||
478 | if (lf) { | |
479 | text = lf(slice.label, slice); | |
480 | } else { | |
481 | text = slice.label; | |
482 | } | |
483 | ||
484 | if (plf) { | |
485 | text = plf(text, slice); | |
486 | } | |
487 | ||
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; | |
491 | ||
492 | var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>"; | |
493 | target.append(html); | |
494 | ||
495 | var label = target.children("#pieLabel" + index); | |
496 | var labelTop = (y - label.height() / 2); | |
497 | var labelLeft = (x - label.width() / 2); | |
498 | ||
499 | label.css("top", labelTop); | |
500 | label.css("left", labelLeft); | |
501 | ||
502 | // check to make sure that the label is not outside the canvas | |
503 | ||
504 | if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { | |
505 | return false; | |
506 | } | |
507 | ||
508 | if (options.series.pie.label.background.opacity != 0) { | |
509 | ||
510 | // put in the transparent background separately to avoid blended labels and label boxes | |
511 | ||
512 | var c = options.series.pie.label.background.color; | |
513 | ||
514 | if (c == null) { | |
515 | c = slice.color; | |
516 | } | |
517 | ||
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); | |
522 | } | |
523 | ||
524 | return true; | |
525 | } // end individual label function | |
526 | } // end drawLabels function | |
527 | } // end drawPie function | |
528 | } // end draw function | |
529 | ||
530 | // Placed here because it needs to be accessed from multiple locations | |
531 | ||
532 | function drawDonutHole(layer) { | |
533 | if (options.series.pie.innerRadius > 0) { | |
534 | ||
535 | // subtract the center | |
536 | ||
537 | layer.save(); | |
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 | |
540 | layer.beginPath(); | |
541 | layer.fillStyle = options.series.pie.stroke.color; | |
542 | layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); | |
543 | layer.fill(); | |
544 | layer.closePath(); | |
545 | layer.restore(); | |
546 | ||
547 | // add inner stroke | |
548 | ||
549 | layer.save(); | |
550 | layer.beginPath(); | |
551 | layer.strokeStyle = options.series.pie.stroke.color; | |
552 | layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); | |
553 | layer.stroke(); | |
554 | layer.closePath(); | |
555 | layer.restore(); | |
556 | ||
557 | // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. | |
558 | } | |
559 | } | |
560 | ||
561 | //-- Additional Interactive related functions -- | |
562 | ||
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]) | |
567 | && (c = !c); | |
568 | return c; | |
569 | } | |
570 | ||
571 | function findNearbySlice(mouseX, mouseY) { | |
572 | ||
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, | |
576 | x, y; | |
577 | ||
578 | for (var i = 0; i < slices.length; ++i) { | |
579 | ||
580 | var s = slices[i]; | |
581 | ||
582 | if (s.pie.show) { | |
583 | ||
584 | ctx.save(); | |
585 | ctx.beginPath(); | |
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); | |
590 | ctx.closePath(); | |
591 | x = mouseX - centerLeft; | |
592 | y = mouseY - centerTop; | |
593 | ||
594 | if (ctx.isPointInPath) { | |
595 | if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { | |
596 | ctx.restore(); | |
597 | return { | |
598 | datapoint: [s.percent, s.data], | |
599 | dataIndex: 0, | |
600 | series: s, | |
601 | seriesIndex: i | |
602 | }; | |
603 | } | |
604 | } else { | |
605 | ||
606 | // excanvas for IE doesn;t support isPointInPath, this is a workaround. | |
607 | ||
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]], | |
619 | arrPoint = [x, y]; | |
620 | ||
621 | // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? | |
622 | ||
623 | if (isPointInPoly(arrPoly, arrPoint)) { | |
624 | ctx.restore(); | |
625 | return { | |
626 | datapoint: [s.percent, s.data], | |
627 | dataIndex: 0, | |
628 | series: s, | |
629 | seriesIndex: i | |
630 | }; | |
631 | } | |
632 | } | |
633 | ||
634 | ctx.restore(); | |
635 | } | |
636 | } | |
637 | ||
638 | return null; | |
639 | } | |
640 | ||
641 | function onMouseMove(e) { | |
642 | triggerClickHoverEvent("plothover", e); | |
643 | } | |
644 | ||
645 | function onClick(e) { | |
646 | triggerClickHoverEvent("plotclick", e); | |
647 | } | |
648 | ||
649 | // trigger click or hover event (they send the same parameters so we share their code) | |
650 | ||
651 | function triggerClickHoverEvent(eventname, e) { | |
652 | ||
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); | |
657 | ||
658 | if (options.grid.autoHighlight) { | |
659 | ||
660 | // clear auto-highlights | |
661 | ||
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); | |
666 | } | |
667 | } | |
668 | } | |
669 | ||
670 | // highlight the slice | |
671 | ||
672 | if (item) { | |
673 | highlight(item.series, eventname); | |
674 | } | |
675 | ||
676 | // trigger any hover bind events | |
677 | ||
678 | var pos = { pageX: e.pageX, pageY: e.pageY }; | |
679 | target.trigger(eventname, [pos, item]); | |
680 | } | |
681 | ||
682 | function highlight(s, auto) { | |
683 | //if (typeof s == "number") { | |
684 | // s = series[s]; | |
685 | //} | |
686 | ||
687 | var i = indexOfHighlight(s); | |
688 | ||
689 | if (i == -1) { | |
690 | highlights.push({ series: s, auto: auto }); | |
691 | plot.triggerRedrawOverlay(); | |
692 | } else if (!auto) { | |
693 | highlights[i].auto = false; | |
694 | } | |
695 | } | |
696 | ||
697 | function unhighlight(s) { | |
698 | if (s == null) { | |
699 | highlights = []; | |
700 | plot.triggerRedrawOverlay(); | |
701 | } | |
702 | ||
703 | //if (typeof s == "number") { | |
704 | // s = series[s]; | |
705 | //} | |
706 | ||
707 | var i = indexOfHighlight(s); | |
708 | ||
709 | if (i != -1) { | |
710 | highlights.splice(i, 1); | |
711 | plot.triggerRedrawOverlay(); | |
712 | } | |
713 | } | |
714 | ||
715 | function indexOfHighlight(s) { | |
716 | for (var i = 0; i < highlights.length; ++i) { | |
717 | var h = highlights[i]; | |
718 | if (h.series == s) | |
719 | return i; | |
720 | } | |
721 | return -1; | |
722 | } | |
723 | ||
724 | function drawOverlay(plot, octx) { | |
725 | ||
726 | var options = plot.getOptions(); | |
727 | ||
728 | var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; | |
729 | ||
730 | octx.save(); | |
731 | octx.translate(centerLeft, centerTop); | |
732 | octx.scale(1, options.series.pie.tilt); | |
733 | ||
734 | for (var i = 0; i < highlights.length; ++i) { | |
735 | drawHighlight(highlights[i].series); | |
736 | } | |
737 | ||
738 | drawDonutHole(octx); | |
739 | ||
740 | octx.restore(); | |
741 | ||
742 | function drawHighlight(series) { | |
743 | ||
744 | if (series.angle <= 0 || isNaN(series.angle)) { | |
745 | return; | |
746 | } | |
747 | ||
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 | |
750 | octx.beginPath(); | |
751 | if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { | |
752 | octx.moveTo(0, 0); // Center of the pie | |
753 | } | |
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); | |
756 | octx.closePath(); | |
757 | octx.fill(); | |
758 | } | |
759 | } | |
760 | } // end init (plugin body) | |
761 | ||
762 | // define pie specific options and their default values | |
763 | ||
764 | var options = { | |
765 | series: { | |
766 | pie: { | |
767 | show: false, | |
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 */ | |
770 | startAngle: 3/2, | |
771 | tilt: 1, | |
772 | shadow: { | |
773 | left: 5, // shadow left offset | |
774 | top: 15, // shadow top offset | |
775 | alpha: 0.02 // shadow alpha | |
776 | }, | |
777 | offset: { | |
778 | top: 0, | |
779 | left: "auto" | |
780 | }, | |
781 | stroke: { | |
782 | color: "#fff", | |
783 | width: 1 | |
784 | }, | |
785 | label: { | |
786 | show: "auto", | |
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) | |
791 | background: { | |
792 | color: null, | |
793 | opacity: 0 | |
794 | }, | |
795 | threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) | |
796 | }, | |
797 | combine: { | |
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 | |
801 | }, | |
802 | highlight: { | |
803 | //color: "#fff", // will add this functionality once parseColor is available | |
804 | opacity: 0.5 | |
805 | } | |
806 | } | |
807 | } | |
808 | }; | |
809 | ||
810 | $.plot.plugins.push({ | |
811 | init: init, | |
812 | options: options, | |
813 | name: "pie", | |
814 | version: "1.1" | |
815 | }); | |
816 | ||
817 | })(jQuery); |