Mercurial > hg > extraction-interface
comparison geotemco/lib/flot/jquery.flot.pie.js @ 0:b12c99b7c3f0
commit for previous development
author | Zoe Hong <zhong@mpiwg-berlin.mpg.de> |
---|---|
date | Mon, 19 Jan 2015 17:13:49 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:b12c99b7c3f0 |
---|---|
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 $.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); |