Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / chart / series / Pie.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.chart.series.Pie
17  * @extends Ext.chart.series.Series
18  *
19  * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
20  * categories that also have a meaning as a whole.
21  * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
22  * documentation for more information. A typical configuration object for the pie series could be:
23  *
24  *     @example
25  *     var store = Ext.create('Ext.data.JsonStore', {
26  *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
27  *         data: [
28  *             { 'name': 'metric one',   'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8,  'data5': 13 },
29  *             { 'name': 'metric two',   'data1': 7,  'data2': 8,  'data3': 16, 'data4': 10, 'data5': 3  },
30  *             { 'name': 'metric three', 'data1': 5,  'data2': 2,  'data3': 14, 'data4': 12, 'data5': 7  },
31  *             { 'name': 'metric four',  'data1': 2,  'data2': 14, 'data3': 6,  'data4': 1,  'data5': 23 },
32  *             { 'name': 'metric five',  'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
33  *         ]
34  *     });
35  *
36  *     Ext.create('Ext.chart.Chart', {
37  *         renderTo: Ext.getBody(),
38  *         width: 500,
39  *         height: 350,
40  *         animate: true,
41  *         store: store,
42  *         theme: 'Base:gradients',
43  *         series: [{
44  *             type: 'pie',
45  *             field: 'data1',
46  *             showInLegend: true,
47  *             tips: {
48  *                 trackMouse: true,
49  *                 width: 140,
50  *                 height: 28,
51  *                 renderer: function(storeItem, item) {
52  *                     // calculate and display percentage on hover
53  *                     var total = 0;
54  *                     store.each(function(rec) {
55  *                         total += rec.get('data1');
56  *                     });
57  *                     this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
58  *                 }
59  *             },
60  *             highlight: {
61  *                 segment: {
62  *                     margin: 20
63  *                 }
64  *             },
65  *             label: {
66  *                 field: 'name',
67  *                 display: 'rotate',
68  *                 contrast: true,
69  *                 font: '18px Arial'
70  *             }
71  *         }]
72  *     });
73  *
74  * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
75  * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
76  *
77  * We set `data1` as the value of the field to determine the angle span for each pie slice. We also set a label configuration object
78  * where we set the field name of the store field to be renderer as text for the label. The labels will also be displayed rotated.
79  *
80  * We set `contrast` to `true` to flip the color of the label if it is to similar to the background color. Finally, we set the font family
81  * and size through the `font` parameter.
82  *
83  * @xtype pie
84  */
85 Ext.define('Ext.chart.series.Pie', {
86
87     /* Begin Definitions */
88
89     alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'],
90
91     extend: 'Ext.chart.series.Series',
92
93     /* End Definitions */
94
95     type: "pie",
96
97     alias: 'series.pie',
98
99     rad: Math.PI / 180,
100
101     /**
102      * @cfg {Number} highlightDuration
103      * The duration for the pie slice highlight effect.
104      */
105     highlightDuration: 150,
106
107     /**
108      * @cfg {String} angleField (required)
109      * The store record field name to be used for the pie angles.
110      * The values bound to this field name must be positive real numbers.
111      */
112     angleField: false,
113
114     /**
115      * @cfg {String} lengthField
116      * The store record field name to be used for the pie slice lengths.
117      * The values bound to this field name must be positive real numbers.
118      */
119     lengthField: false,
120
121     /**
122      * @cfg {Boolean/Number} donut
123      * Whether to set the pie chart as donut chart.
124      * Default's false. Can be set to a particular percentage to set the radius
125      * of the donut chart.
126      */
127     donut: false,
128
129     /**
130      * @cfg {Boolean} showInLegend
131      * Whether to add the pie chart elements as legend items. Default's false.
132      */
133     showInLegend: false,
134
135     /**
136      * @cfg {Array} colorSet
137      * An array of color values which will be used, in order, as the pie slice fill colors.
138      */
139
140     /**
141      * @cfg {Object} style
142      * An object containing styles for overriding series styles from Theming.
143      */
144     style: {},
145
146     constructor: function(config) {
147         this.callParent(arguments);
148         var me = this,
149             chart = me.chart,
150             surface = chart.surface,
151             store = chart.store,
152             shadow = chart.shadow, i, l, cfg;
153         Ext.applyIf(me, {
154             highlightCfg: {
155                 segment: {
156                     margin: 20
157                 }
158             }
159         });
160         Ext.apply(me, config, {
161             shadowAttributes: [{
162                 "stroke-width": 6,
163                 "stroke-opacity": 1,
164                 stroke: 'rgb(200, 200, 200)',
165                 translate: {
166                     x: 1.2,
167                     y: 2
168                 }
169             },
170             {
171                 "stroke-width": 4,
172                 "stroke-opacity": 1,
173                 stroke: 'rgb(150, 150, 150)',
174                 translate: {
175                     x: 0.9,
176                     y: 1.5
177                 }
178             },
179             {
180                 "stroke-width": 2,
181                 "stroke-opacity": 1,
182                 stroke: 'rgb(100, 100, 100)',
183                 translate: {
184                     x: 0.6,
185                     y: 1
186                 }
187             }]
188         });
189         me.group = surface.getGroup(me.seriesId);
190         if (shadow) {
191             for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
192                 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
193             }
194         }
195         surface.customAttributes.segment = function(opt) {
196             return me.getSegment(opt);
197         };
198         me.__excludes = me.__excludes || [];
199     },
200
201     //@private updates some onbefore render parameters.
202     initialize: function() {
203         var me = this,
204             store = me.chart.getChartStore();
205         //Add yFields to be used in Legend.js
206         me.yField = [];
207         if (me.label.field) {
208             store.each(function(rec) {
209                 me.yField.push(rec.get(me.label.field));
210             });
211         }
212     },
213
214     // @private returns an object with properties for a PieSlice.
215     getSegment: function(opt) {
216         var me = this,
217             rad = me.rad,
218             cos = Math.cos,
219             sin = Math.sin,
220             x = me.centerX,
221             y = me.centerY,
222             x1 = 0, x2 = 0, x3 = 0, x4 = 0,
223             y1 = 0, y2 = 0, y3 = 0, y4 = 0,
224             x5 = 0, y5 = 0, x6 = 0, y6 = 0,
225             delta = 1e-2,
226             startAngle = opt.startAngle,
227             endAngle = opt.endAngle,
228             midAngle = (startAngle + endAngle) / 2 * rad,
229             margin = opt.margin || 0,
230             a1 = Math.min(startAngle, endAngle) * rad,
231             a2 = Math.max(startAngle, endAngle) * rad,
232             c1 = cos(a1), s1 = sin(a1),
233             c2 = cos(a2), s2 = sin(a2),
234             cm = cos(midAngle), sm = sin(midAngle),
235             flag = 0, hsqr2 = 0.7071067811865476; // sqrt(0.5)
236
237         if (a2 - a1 < delta) {
238             return {path: ""};
239         }
240
241         if (margin !== 0) {
242             x += margin * cm;
243             y += margin * sm;
244         }
245
246         x2 = x + opt.endRho * c1;
247         y2 = y + opt.endRho * s1;
248
249         x4 = x + opt.endRho * c2;
250         y4 = y + opt.endRho * s2;
251
252         if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
253             cm = hsqr2;
254             sm = -hsqr2;
255             flag = 1;
256         }
257
258         x6 = x + opt.endRho * cm;
259         y6 = y + opt.endRho * sm;
260
261         // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
262         // Better fix the VML engine for half circles.
263
264         if (opt.startRho !== 0) {
265             x1 = x + opt.startRho * c1;
266             y1 = y + opt.startRho * s1;
267     
268             x3 = x + opt.startRho * c2;
269             y3 = y + opt.startRho * s2;
270     
271             x5 = x + opt.startRho * cm;
272             y5 = y + opt.startRho * sm;
273
274             return {
275                 path: [
276                     ["M", x2, y2],
277                     ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
278                     ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
279                     ["L", x3, y3],
280                     ["A", opt.startRho, opt.startRho, 0, flag, 0, x5, y5], ["L", x5, y5],
281                     ["A", opt.startRho, opt.startRho, 0, 0, 0, x1, y1], ["L", x1, y1],
282                     ["Z"]
283                 ]
284             };
285         } else {
286             return {
287                 path: [
288                     ["M", x, y],
289                     ["L", x2, y2],
290                     ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
291                     ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
292                     ["L", x, y],
293                     ["Z"]
294                 ]
295             };
296         }
297     },
298
299     // @private utility function to calculate the middle point of a pie slice.
300     calcMiddle: function(item) {
301         var me = this,
302             rad = me.rad,
303             slice = item.slice,
304             x = me.centerX,
305             y = me.centerY,
306             startAngle = slice.startAngle,
307             endAngle = slice.endAngle,
308             donut = +me.donut,
309             midAngle = -(startAngle + endAngle) * rad / 2,
310             r = (item.endRho + item.startRho) / 2,
311             xm = x + r * Math.cos(midAngle),
312             ym = y - r * Math.sin(midAngle);
313
314         item.middle = {
315             x: xm,
316             y: ym
317         };
318     },
319
320     /**
321      * Draws the series for the current chart.
322      */
323     drawSeries: function() {
324         var me = this,
325             store = me.chart.getChartStore(),
326             group = me.group,
327             animate = me.chart.animate,
328             field = me.angleField || me.field || me.xField,
329             lenField = [].concat(me.lengthField),
330             totalLenField = 0,
331             colors = me.colorSet,
332             chart = me.chart,
333             surface = chart.surface,
334             chartBBox = chart.chartBBox,
335             enableShadows = chart.shadow,
336             shadowGroups = me.shadowGroups,
337             shadowAttributes = me.shadowAttributes,
338             lnsh = shadowGroups.length,
339             rad = me.rad,
340             layers = lenField.length,
341             rhoAcum = 0,
342             donut = +me.donut,
343             layerTotals = [],
344             values = {},
345             fieldLength,
346             items = [],
347             passed = false,
348             totalField = 0,
349             maxLenField = 0,
350             cut = 9,
351             defcut = true,
352             angle = 0,
353             seriesStyle = me.seriesStyle,
354             seriesLabelStyle = me.seriesLabelStyle,
355             colorArrayStyle = me.colorArrayStyle,
356             colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
357             gutterX = chart.maxGutter[0],
358             gutterY = chart.maxGutter[1],
359             abs = Math.abs,
360             rendererAttributes,
361             shadowGroup,
362             shadowAttr,
363             shadows,
364             shadow,
365             shindex,
366             centerX,
367             centerY,
368             deltaRho,
369             first = 0,
370             slice,
371             slices,
372             sprite,
373             value,
374             item,
375             lenValue,
376             ln,
377             record,
378             i,
379             j,
380             startAngle,
381             endAngle,
382             middleAngle,
383             sliceLength,
384             path,
385             p,
386             spriteOptions, bbox;
387
388         Ext.apply(seriesStyle, me.style || {});
389
390         me.setBBox();
391         bbox = me.bbox;
392
393         //override theme colors
394         if (me.colorSet) {
395             colorArrayStyle = me.colorSet;
396             colorArrayLength = colorArrayStyle.length;
397         }
398
399         //if not store or store is empty then there's nothing to draw
400         if (!store || !store.getCount()) {
401             return;
402         }
403
404         me.unHighlightItem();
405         me.cleanHighlights();
406
407         centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
408         centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
409         me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
410         me.slices = slices = [];
411         me.items = items = [];
412
413         store.each(function(record, i) {
414             if (this.__excludes && this.__excludes[i]) {
415                 //hidden series
416                 return;
417             }
418             totalField += +record.get(field);
419             if (lenField[0]) {
420                 for (j = 0, totalLenField = 0; j < layers; j++) {
421                     totalLenField += +record.get(lenField[j]);
422                 }
423                 layerTotals[i] = totalLenField;
424                 maxLenField = Math.max(maxLenField, totalLenField);
425             }
426         }, this);
427
428         totalField = totalField || 1;
429         store.each(function(record, i) {
430             if (this.__excludes && this.__excludes[i]) {
431                 value = 0;
432             } else {
433                 value = record.get(field);
434                 if (first == 0) {
435                     first = 1;
436                 }
437             }
438
439             // First slice
440             if (first == 1) {
441                 first = 2;
442                 me.firstAngle = angle = 360 * value / totalField / 2;
443                 for (j = 0; j < i; j++) {
444                     slices[j].startAngle = slices[j].endAngle = me.firstAngle;
445                 }
446             }
447             
448             endAngle = angle - 360 * value / totalField;
449             slice = {
450                 series: me,
451                 value: value,
452                 startAngle: angle,
453                 endAngle: endAngle,
454                 storeItem: record
455             };
456             if (lenField[0]) {
457                 lenValue = layerTotals[i];
458                 slice.rho = me.radius * (lenValue / maxLenField);
459             } else {
460                 slice.rho = me.radius;
461             }
462             slices[i] = slice;
463             angle = endAngle;
464         }, me);
465         //do all shadows first.
466         if (enableShadows) {
467             for (i = 0, ln = slices.length; i < ln; i++) {
468                 slice = slices[i];
469                 slice.shadowAttrs = [];
470                 for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
471                     sprite = group.getAt(i * layers + j);
472                     deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
473                     //set pie slice properties
474                     rendererAttributes = {
475                         segment: {
476                             startAngle: slice.startAngle,
477                             endAngle: slice.endAngle,
478                             margin: 0,
479                             rho: slice.rho,
480                             startRho: rhoAcum + (deltaRho * donut / 100),
481                             endRho: rhoAcum + deltaRho
482                         },
483                         hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
484                     };
485                     //create shadows
486                     for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
487                         shadowAttr = shadowAttributes[shindex];
488                         shadow = shadowGroups[shindex].getAt(i);
489                         if (!shadow) {
490                             shadow = chart.surface.add(Ext.apply({}, {
491                                 type: 'path',
492                                 group: shadowGroups[shindex],
493                                 strokeLinejoin: "round"
494                             }, rendererAttributes, shadowAttr));
495                         }
496                         if (animate) {
497                             shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply({}, rendererAttributes, shadowAttr), i, store);
498                             me.onAnimate(shadow, {
499                                 to: shadowAttr
500                             });
501                         } else {
502                             shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
503                             shadow.setAttributes(shadowAttr, true);
504                         }
505                         shadows.push(shadow);
506                     }
507                     slice.shadowAttrs[j] = shadows;
508                 }
509             }
510         }
511         //do pie slices after.
512         for (i = 0, ln = slices.length; i < ln; i++) {
513             slice = slices[i];
514             for (j = 0, rhoAcum = 0; j < layers; j++) {
515                 sprite = group.getAt(i * layers + j);
516                 deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
517                 //set pie slice properties
518                 rendererAttributes = Ext.apply({
519                     segment: {
520                         startAngle: slice.startAngle,
521                         endAngle: slice.endAngle,
522                         margin: 0,
523                         rho: slice.rho,
524                         startRho: rhoAcum + (deltaRho * donut / 100),
525                         endRho: rhoAcum + deltaRho
526                     },
527                     hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
528                 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
529                 item = Ext.apply({},
530                 rendererAttributes.segment, {
531                     slice: slice,
532                     series: me,
533                     storeItem: slice.storeItem,
534                     index: i
535                 });
536                 me.calcMiddle(item);
537                 if (enableShadows) {
538                     item.shadows = slice.shadowAttrs[j];
539                 }
540                 items[i] = item;
541                 // Create a new sprite if needed (no height)
542                 if (!sprite) {
543                     spriteOptions = Ext.apply({
544                         type: "path",
545                         group: group,
546                         middle: item.middle
547                     }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
548                     sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
549                 }
550                 slice.sprite = slice.sprite || [];
551                 item.sprite = sprite;
552                 slice.sprite.push(sprite);
553                 slice.point = [item.middle.x, item.middle.y];
554                 if (animate) {
555                     rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store);
556                     sprite._to = rendererAttributes;
557                     sprite._animating = true;
558                     me.onAnimate(sprite, {
559                         to: rendererAttributes,
560                         listeners: {
561                             afteranimate: {
562                                 fn: function() {
563                                     this._animating = false;
564                                 },
565                                 scope: sprite
566                             }
567                         }
568                     });
569                 } else {
570                     rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, {
571                         hidden: false
572                     }), i, store);
573                     sprite.setAttributes(rendererAttributes, true);
574                 }
575                 rhoAcum += deltaRho;
576             }
577         }
578
579         // Hide unused bars
580         ln = group.getCount();
581         for (i = 0; i < ln; i++) {
582             if (!slices[(i / layers) >> 0] && group.getAt(i)) {
583                 group.getAt(i).hide(true);
584             }
585         }
586         if (enableShadows) {
587             lnsh = shadowGroups.length;
588             for (shindex = 0; shindex < ln; shindex++) {
589                 if (!slices[(shindex / layers) >> 0]) {
590                     for (j = 0; j < lnsh; j++) {
591                         if (shadowGroups[j].getAt(shindex)) {
592                             shadowGroups[j].getAt(shindex).hide(true);
593                         }
594                     }
595                 }
596             }
597         }
598         me.renderLabels();
599         me.renderCallouts();
600     },
601
602     // @private callback for when creating a label sprite.
603     onCreateLabel: function(storeItem, item, i, display) {
604         var me = this,
605             group = me.labelsGroup,
606             config = me.label,
607             centerX = me.centerX,
608             centerY = me.centerY,
609             middle = item.middle,
610             endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
611
612         return me.chart.surface.add(Ext.apply({
613             'type': 'text',
614             'text-anchor': 'middle',
615             'group': group,
616             'x': middle.x,
617             'y': middle.y
618         }, endLabelStyle));
619     },
620
621     // @private callback for when placing a label sprite.
622     onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
623         var me = this,
624             chart = me.chart,
625             resizing = chart.resizing,
626             config = me.label,
627             format = config.renderer,
628             field = [].concat(config.field),
629             centerX = me.centerX,
630             centerY = me.centerY,
631             middle = item.middle,
632             opt = {
633                 x: middle.x,
634                 y: middle.y
635             },
636             x = middle.x - centerX,
637             y = middle.y - centerY,
638             from = {},
639             rho = 1,
640             theta = Math.atan2(y, x || 1),
641             dg = theta * 180 / Math.PI,
642             prevDg;
643         if (this.__excludes && this.__excludes[i]) {
644             opt.hidden = true;
645         }
646         function fixAngle(a) {
647             if (a < 0) {
648                 a += 360;
649             }
650             return a % 360;
651         }
652
653         label.setAttributes({
654             text: format(storeItem.get(field[index]))
655         }, true);
656
657         switch (display) {
658         case 'outside':
659             rho = Math.sqrt(x * x + y * y) * 2;
660             //update positions
661             opt.x = rho * Math.cos(theta) + centerX;
662             opt.y = rho * Math.sin(theta) + centerY;
663             break;
664
665         case 'rotate':
666             dg = fixAngle(dg);
667             dg = (dg > 90 && dg < 270) ? dg + 180: dg;
668
669             prevDg = label.attr.rotation.degrees;
670             if (prevDg != null && Math.abs(prevDg - dg) > 180) {
671                 if (dg > prevDg) {
672                     dg -= 360;
673                 } else {
674                     dg += 360;
675                 }
676                 dg = dg % 360;
677             } else {
678                 dg = fixAngle(dg);
679             }
680             //update rotation angle
681             opt.rotate = {
682                 degrees: dg,
683                 x: opt.x,
684                 y: opt.y
685             };
686             break;
687
688         default:
689             break;
690         }
691         //ensure the object has zero translation
692         opt.translate = {
693             x: 0, y: 0
694         };
695         if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
696             me.onAnimate(label, {
697                 to: opt
698             });
699         } else {
700             label.setAttributes(opt, true);
701         }
702         label._from = from;
703     },
704
705     // @private callback for when placing a callout sprite.
706     onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
707         var me = this,
708             chart = me.chart,
709             resizing = chart.resizing,
710             config = me.callouts,
711             centerX = me.centerX,
712             centerY = me.centerY,
713             middle = item.middle,
714             opt = {
715                 x: middle.x,
716                 y: middle.y
717             },
718             x = middle.x - centerX,
719             y = middle.y - centerY,
720             rho = 1,
721             rhoCenter,
722             theta = Math.atan2(y, x || 1),
723             bbox = callout.label.getBBox(),
724             offsetFromViz = 20,
725             offsetToSide = 10,
726             offsetBox = 10,
727             p;
728
729         //should be able to config this.
730         rho = item.endRho + offsetFromViz;
731         rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3;
732         //update positions
733         opt.x = rho * Math.cos(theta) + centerX;
734         opt.y = rho * Math.sin(theta) + centerY;
735
736         x = rhoCenter * Math.cos(theta);
737         y = rhoCenter * Math.sin(theta);
738
739         if (chart.animate) {
740             //set the line from the middle of the pie to the box.
741             me.onAnimate(callout.lines, {
742                 to: {
743                     path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
744                 }
745             });
746             //set box position
747             me.onAnimate(callout.box, {
748                 to: {
749                     x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
750                     y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
751                     width: bbox.width + 2 * offsetBox,
752                     height: bbox.height + 2 * offsetBox
753                 }
754             });
755             //set text position
756             me.onAnimate(callout.label, {
757                 to: {
758                     x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
759                     y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
760                 }
761             });
762         } else {
763             //set the line from the middle of the pie to the box.
764             callout.lines.setAttributes({
765                 path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
766             },
767             true);
768             //set box position
769             callout.box.setAttributes({
770                 x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
771                 y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
772                 width: bbox.width + 2 * offsetBox,
773                 height: bbox.height + 2 * offsetBox
774             },
775             true);
776             //set text position
777             callout.label.setAttributes({
778                 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
779                 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
780             },
781             true);
782         }
783         for (p in callout) {
784             callout[p].show(true);
785         }
786     },
787
788     // @private handles sprite animation for the series.
789     onAnimate: function(sprite, attr) {
790         sprite.show();
791         return this.callParent(arguments);
792     },
793
794     isItemInPoint: function(x, y, item, i) {
795         var me = this,
796             cx = me.centerX,
797             cy = me.centerY,
798             abs = Math.abs,
799             dx = abs(x - cx),
800             dy = abs(y - cy),
801             startAngle = item.startAngle,
802             endAngle = item.endAngle,
803             rho = Math.sqrt(dx * dx + dy * dy),
804             angle = Math.atan2(y - cy, x - cx) / me.rad;
805
806         // normalize to the same range of angles created by drawSeries
807         if (angle > me.firstAngle) {
808             angle -= 360;
809         }
810         return (angle <= startAngle && angle > endAngle
811                 && rho >= item.startRho && rho <= item.endRho);
812     },
813
814     // @private hides all elements in the series.
815     hideAll: function() {
816         var i, l, shadow, shadows, sh, lsh, sprite;
817         if (!isNaN(this._index)) {
818             this.__excludes = this.__excludes || [];
819             this.__excludes[this._index] = true;
820             sprite = this.slices[this._index].sprite;
821             for (sh = 0, lsh = sprite.length; sh < lsh; sh++) {
822                 sprite[sh].setAttributes({
823                     hidden: true
824                 }, true);
825             }
826             if (this.slices[this._index].shadowAttrs) {
827                 for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) {
828                     shadow = shadows[i];
829                     for (sh = 0, lsh = shadow.length; sh < lsh; sh++) {
830                         shadow[sh].setAttributes({
831                             hidden: true
832                         }, true);
833                     }
834                 }
835             }
836             this.drawSeries();
837         }
838     },
839
840     // @private shows all elements in the series.
841     showAll: function() {
842         if (!isNaN(this._index)) {
843             this.__excludes[this._index] = false;
844             this.drawSeries();
845         }
846     },
847
848     /**
849      * Highlight the specified item. If no item is provided the whole series will be highlighted.
850      * @param item {Object} Info about the item; same format as returned by #getItemForPoint
851      */
852     highlightItem: function(item) {
853         var me = this,
854             rad = me.rad;
855         item = item || this.items[this._index];
856
857         //TODO(nico): sometimes in IE itemmouseover is triggered
858         //twice without triggering itemmouseout in between. This
859         //fixes the highlighting bug. Eventually, events should be
860         //changed to trigger one itemmouseout between two itemmouseovers.
861         this.unHighlightItem();
862
863         if (!item || item.sprite && item.sprite._animating) {
864             return;
865         }
866         me.callParent([item]);
867         if (!me.highlight) {
868             return;
869         }
870         if ('segment' in me.highlightCfg) {
871             var highlightSegment = me.highlightCfg.segment,
872                 animate = me.chart.animate,
873                 attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop;
874             //animate labels
875             if (me.labelsGroup) {
876                 var group = me.labelsGroup,
877                     display = me.label.display,
878                     label = group.getAt(item.index),
879                     middle = (item.startAngle + item.endAngle) / 2 * rad,
880                     r = highlightSegment.margin || 0,
881                     x = r * Math.cos(middle),
882                     y = r * Math.sin(middle);
883
884                 //TODO(nico): rounding to 1e-10
885                 //gives the right translation. Translation
886                 //was buggy for very small numbers. In this
887                 //case we're not looking to translate to very small
888                 //numbers but not to translate at all.
889                 if (Math.abs(x) < 1e-10) {
890                     x = 0;
891                 }
892                 if (Math.abs(y) < 1e-10) {
893                     y = 0;
894                 }
895
896                 if (animate) {
897                     label.stopAnimation();
898                     label.animate({
899                         to: {
900                             translate: {
901                                 x: x,
902                                 y: y
903                             }
904                         },
905                         duration: me.highlightDuration
906                     });
907                 }
908                 else {
909                     label.setAttributes({
910                         translate: {
911                             x: x,
912                             y: y
913                         }
914                     }, true);
915                 }
916             }
917             //animate shadows
918             if (me.chart.shadow && item.shadows) {
919                 i = 0;
920                 shadows = item.shadows;
921                 ln = shadows.length;
922                 for (; i < ln; i++) {
923                     shadow = shadows[i];
924                     to = {};
925                     itemHighlightSegment = item.sprite._from.segment;
926                     for (prop in itemHighlightSegment) {
927                         if (! (prop in highlightSegment)) {
928                             to[prop] = itemHighlightSegment[prop];
929                         }
930                     }
931                     attrs = {
932                         segment: Ext.applyIf(to, me.highlightCfg.segment)
933                     };
934                     if (animate) {
935                         shadow.stopAnimation();
936                         shadow.animate({
937                             to: attrs,
938                             duration: me.highlightDuration
939                         });
940                     }
941                     else {
942                         shadow.setAttributes(attrs, true);
943                     }
944                 }
945             }
946         }
947     },
948
949     /**
950      * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
951      * @param item {Object} Info about the item; same format as returned by #getItemForPoint
952      */
953     unHighlightItem: function() {
954         var me = this;
955         if (!me.highlight) {
956             return;
957         }
958
959         if (('segment' in me.highlightCfg) && me.items) {
960             var items = me.items,
961                 animate = me.chart.animate,
962                 shadowsEnabled = !!me.chart.shadow,
963                 group = me.labelsGroup,
964                 len = items.length,
965                 i = 0,
966                 j = 0,
967                 display = me.label.display,
968                 shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs;
969
970             for (; i < len; i++) {
971                 item = items[i];
972                 if (!item) {
973                     continue;
974                 }
975                 sprite = item.sprite;
976                 if (sprite && sprite._highlighted) {
977                     //animate labels
978                     if (group) {
979                         label = group.getAt(item.index);
980                         attrs = Ext.apply({
981                             translate: {
982                                 x: 0,
983                                 y: 0
984                             }
985                         },
986                         display == 'rotate' ? {
987                             rotate: {
988                                 x: label.attr.x,
989                                 y: label.attr.y,
990                                 degrees: label.attr.rotation.degrees
991                             }
992                         }: {});
993                         if (animate) {
994                             label.stopAnimation();
995                             label.animate({
996                                 to: attrs,
997                                 duration: me.highlightDuration
998                             });
999                         }
1000                         else {
1001                             label.setAttributes(attrs, true);
1002                         }
1003                     }
1004                     if (shadowsEnabled) {
1005                         shadows = item.shadows;
1006                         shadowLen = shadows.length;
1007                         for (; j < shadowLen; j++) {
1008                             to = {};
1009                             ihs = item.sprite._to.segment;
1010                             hs = item.sprite._from.segment;
1011                             Ext.apply(to, hs);
1012                             for (p in ihs) {
1013                                 if (! (p in hs)) {
1014                                     to[p] = ihs[p];
1015                                 }
1016                             }
1017                             shadow = shadows[j];
1018                             if (animate) {
1019                                 shadow.stopAnimation();
1020                                 shadow.animate({
1021                                     to: {
1022                                         segment: to
1023                                     },
1024                                     duration: me.highlightDuration
1025                                 });
1026                             }
1027                             else {
1028                                 shadow.setAttributes({ segment: to }, true);
1029                             }
1030                         }
1031                     }
1032                 }
1033             }
1034         }
1035         me.callParent(arguments);
1036     },
1037
1038     /**
1039      * Returns the color of the series (to be displayed as color for the series legend item).
1040      * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1041      */
1042     getLegendColor: function(index) {
1043         var me = this;
1044         return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];
1045     }
1046 });
1047
1048