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