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