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