Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / chart / series / Pie.js
index ff1c874..ec7e7cf 100644 (file)
@@ -15,29 +15,28 @@ If you are unsure which license is appropriate for your use, please contact the
 /**
  * @class Ext.chart.series.Pie
  * @extends Ext.chart.series.Series
- * 
- * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different 
+ *
+ * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
  * categories that also have a meaning as a whole.
- * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart 
+ * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
  * documentation for more information. A typical configuration object for the pie series could be:
- * 
- * {@img Ext.chart.series.Pie/Ext.chart.series.Pie.png Ext.chart.series.Pie chart series}
  *
+ *     @example
  *     var store = Ext.create('Ext.data.JsonStore', {
  *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
  *         data: [
- *             {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
- *             {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
- *             {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
- *             {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
- *             {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
+ *             { 'name': 'metric one',   'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8,  'data5': 13 },
+ *             { 'name': 'metric two',   'data1': 7,  'data2': 8,  'data3': 16, 'data4': 10, 'data5': 3  },
+ *             { 'name': 'metric three', 'data1': 5,  'data2': 2,  'data3': 14, 'data4': 12, 'data5': 7  },
+ *             { 'name': 'metric four',  'data1': 2,  'data2': 14, 'data3': 6,  'data4': 1,  'data5': 23 },
+ *             { 'name': 'metric five',  'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
  *         ]
  *     });
- *     
+ *
  *     Ext.create('Ext.chart.Chart', {
  *         renderTo: Ext.getBody(),
  *         width: 500,
- *         height: 300,
+ *         height: 350,
  *         animate: true,
  *         store: store,
  *         theme: 'Base:gradients',
@@ -46,22 +45,22 @@ If you are unsure which license is appropriate for your use, please contact the
  *             field: 'data1',
  *             showInLegend: true,
  *             tips: {
- *               trackMouse: true,
- *               width: 140,
- *               height: 28,
- *               renderer: function(storeItem, item) {
- *                 //calculate and display percentage on hover
- *                 var total = 0;
- *                 store.each(function(rec) {
- *                     total += rec.get('data1');
- *                 });
- *                 this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
- *               }
+ *                 trackMouse: true,
+ *                 width: 140,
+ *                 height: 28,
+ *                 renderer: function(storeItem, item) {
+ *                     // calculate and display percentage on hover
+ *                     var total = 0;
+ *                     store.each(function(rec) {
+ *                         total += rec.get('data1');
+ *                     });
+ *                     this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
+ *                 }
  *             },
  *             highlight: {
- *               segment: {
- *                 margin: 20
- *               }
+ *                 segment: {
+ *                     margin: 20
+ *                 }
  *             },
  *             label: {
  *                 field: 'name',
@@ -69,16 +68,18 @@ If you are unsure which license is appropriate for your use, please contact the
  *                 contrast: true,
  *                 font: '18px Arial'
  *             }
- *         }]    
+ *         }]
  *     });
- * 
- * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options 
- * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item. 
- * 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 
- * 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. 
- * 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 
- * and size through the `font` parameter. 
- * 
+ *
+ * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
+ * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
+ *
+ * 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
+ * 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.
+ *
+ * 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
+ * and size through the `font` parameter.
+ *
  * @xtype pie
  */
 Ext.define('Ext.chart.series.Pie', {
@@ -92,7 +93,7 @@ Ext.define('Ext.chart.series.Pie', {
     /* End Definitions */
 
     type: "pie",
-    
+
     alias: 'series.pie',
 
     rad: Math.PI / 180,
@@ -104,10 +105,9 @@ Ext.define('Ext.chart.series.Pie', {
     highlightDuration: 150,
 
     /**
-     * @cfg {String} angleField
+     * @cfg {String} angleField (required)
      * The store record field name to be used for the pie angles.
      * The values bound to this field name must be positive real numbers.
-     * This parameter is required.
      */
     angleField: false,
 
@@ -115,12 +115,11 @@ Ext.define('Ext.chart.series.Pie', {
      * @cfg {String} lengthField
      * The store record field name to be used for the pie slice lengths.
      * The values bound to this field name must be positive real numbers.
-     * This parameter is optional.
      */
     lengthField: false,
 
     /**
-     * @cfg {Boolean|Number} donut
+     * @cfg {Boolean/Number} donut
      * Whether to set the pie chart as donut chart.
      * Default's false. Can be set to a particular percentage to set the radius
      * of the donut chart.
@@ -137,13 +136,13 @@ Ext.define('Ext.chart.series.Pie', {
      * @cfg {Array} colorSet
      * An array of color values which will be used, in order, as the pie slice fill colors.
      */
-    
+
     /**
      * @cfg {Object} style
      * An object containing styles for overriding series styles from Theming.
      */
     style: {},
-    
+
     constructor: function(config) {
         this.callParent(arguments);
         var me = this,
@@ -158,7 +157,7 @@ Ext.define('Ext.chart.series.Pie', {
                 }
             }
         });
-        Ext.apply(me, config, {            
+        Ext.apply(me, config, {
             shadowAttributes: [{
                 "stroke-width": 6,
                 "stroke-opacity": 1,
@@ -196,12 +195,13 @@ Ext.define('Ext.chart.series.Pie', {
         surface.customAttributes.segment = function(opt) {
             return me.getSegment(opt);
         };
+        me.__excludes = me.__excludes || [];
     },
-    
+
     //@private updates some onbefore render parameters.
     initialize: function() {
         var me = this,
-            store = me.chart.substore || me.chart.store;
+            store = me.chart.getChartStore();
         //Add yFields to be used in Legend.js
         me.yField = [];
         if (me.label.field) {
@@ -217,58 +217,81 @@ Ext.define('Ext.chart.series.Pie', {
             rad = me.rad,
             cos = Math.cos,
             sin = Math.sin,
-            abs = Math.abs,
             x = me.centerX,
             y = me.centerY,
             x1 = 0, x2 = 0, x3 = 0, x4 = 0,
             y1 = 0, y2 = 0, y3 = 0, y4 = 0,
+            x5 = 0, y5 = 0, x6 = 0, y6 = 0,
             delta = 1e-2,
-            r = opt.endRho - opt.startRho,
             startAngle = opt.startAngle,
             endAngle = opt.endAngle,
             midAngle = (startAngle + endAngle) / 2 * rad,
             margin = opt.margin || 0,
-            flag = abs(endAngle - startAngle) > 180,
             a1 = Math.min(startAngle, endAngle) * rad,
             a2 = Math.max(startAngle, endAngle) * rad,
-            singleSlice = false;
+            c1 = cos(a1), s1 = sin(a1),
+            c2 = cos(a2), s2 = sin(a2),
+            cm = cos(midAngle), sm = sin(midAngle),
+            flag = 0, hsqr2 = 0.7071067811865476; // sqrt(0.5)
 
-        x += margin * cos(midAngle);
-        y += margin * sin(midAngle);
-
-        x1 = x + opt.startRho * cos(a1);
-        y1 = y + opt.startRho * sin(a1);
+        if (a2 - a1 < delta) {
+            return {path: ""};
+        }
 
-        x2 = x + opt.endRho * cos(a1);
-        y2 = y + opt.endRho * sin(a1);
+        if (margin !== 0) {
+            x += margin * cm;
+            y += margin * sm;
+        }
 
-        x3 = x + opt.startRho * cos(a2);
-        y3 = y + opt.startRho * sin(a2);
+        x2 = x + opt.endRho * c1;
+        y2 = y + opt.endRho * s1;
 
-        x4 = x + opt.endRho * cos(a2);
-        y4 = y + opt.endRho * sin(a2);
+        x4 = x + opt.endRho * c2;
+        y4 = y + opt.endRho * s2;
 
-        if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
-            singleSlice = true;
+        if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
+            cm = hsqr2;
+            sm = -hsqr2;
+            flag = 1;
         }
-        //Solves mysterious clipping bug with IE
-        if (singleSlice) {
+
+        x6 = x + opt.endRho * cm;
+        y6 = y + opt.endRho * sm;
+
+        // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
+        // Better fix the VML engine for half circles.
+
+        if (opt.startRho !== 0) {
+            x1 = x + opt.startRho * c1;
+            y1 = y + opt.startRho * s1;
+    
+            x3 = x + opt.startRho * c2;
+            y3 = y + opt.startRho * s2;
+    
+            x5 = x + opt.startRho * cm;
+            y5 = y + opt.startRho * sm;
+
             return {
                 path: [
-                ["M", x1, y1],
-                ["L", x2, y2],
-                ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
-                ["Z"]]
+                    ["M", x2, y2],
+                    ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+                    ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+                    ["L", x3, y3],
+                    ["A", opt.startRho, opt.startRho, 0, flag, 0, x5, y5], ["L", x5, y5],
+                    ["A", opt.startRho, opt.startRho, 0, 0, 0, x1, y1], ["L", x1, y1],
+                    ["Z"]
+                ]
             };
         } else {
             return {
                 path: [
-                ["M", x1, y1],
-                ["L", x2, y2],
-                ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
-                ["L", x3, y3],
-                ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
-                ["Z"]]
+                    ["M", x, y],
+                    ["L", x2, y2],
+                    ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+                    ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+                    ["L", x, y],
+                    ["Z"]
+                ]
             };
         }
     },
@@ -283,11 +306,10 @@ Ext.define('Ext.chart.series.Pie', {
             startAngle = slice.startAngle,
             endAngle = slice.endAngle,
             donut = +me.donut,
-            a1 = Math.min(startAngle, endAngle) * rad,
-            a2 = Math.max(startAngle, endAngle) * rad,
-            midAngle = -(a1 + (a2 - a1) / 2),
-            xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle),
-            ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle);
+            midAngle = -(startAngle + endAngle) * rad / 2,
+            r = (item.endRho + item.startRho) / 2,
+            xm = x + r * Math.cos(midAngle),
+            ym = y - r * Math.sin(midAngle);
 
         item.middle = {
             x: xm,
@@ -300,7 +322,7 @@ Ext.define('Ext.chart.series.Pie', {
      */
     drawSeries: function() {
         var me = this,
-            store = me.chart.substore || me.chart.store,
+            store = me.chart.getChartStore(),
             group = me.group,
             animate = me.chart.animate,
             field = me.angleField || me.field || me.xField,
@@ -334,6 +356,7 @@ Ext.define('Ext.chart.series.Pie', {
             colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
             gutterX = chart.maxGutter[0],
             gutterY = chart.maxGutter[1],
+            abs = Math.abs,
             rendererAttributes,
             shadowGroup,
             shadowAttr,
@@ -361,7 +384,7 @@ Ext.define('Ext.chart.series.Pie', {
             path,
             p,
             spriteOptions, bbox;
-        
+
         Ext.apply(seriesStyle, me.style || {});
 
         me.setBBox();
@@ -372,12 +395,12 @@ Ext.define('Ext.chart.series.Pie', {
             colorArrayStyle = me.colorSet;
             colorArrayLength = colorArrayStyle.length;
         }
-        
+
         //if not store or store is empty then there's nothing to draw
         if (!store || !store.getCount()) {
             return;
         }
-        
+
         me.unHighlightItem();
         me.cleanHighlights();
 
@@ -402,25 +425,26 @@ Ext.define('Ext.chart.series.Pie', {
             }
         }, this);
 
+        totalField = totalField || 1;
         store.each(function(record, i) {
             if (this.__excludes && this.__excludes[i]) {
-                //hidden series
-                return;
-            } 
-            value = record.get(field);
-            middleAngle = angle - 360 * value / totalField / 2;
-            // TODO - Put up an empty circle
-            if (isNaN(middleAngle)) {
-                middleAngle = 360;
-                value = 1;
-                totalField = 1;
+                value = 0;
+            } else {
+                value = record.get(field);
+                if (first == 0) {
+                    first = 1;
+                }
             }
+
             // First slice
-            if (!i || first == 0) {
-                angle = 360 - middleAngle;
-                me.firstAngle = angle;
-                middleAngle = angle - 360 * value / totalField / 2;
+            if (first == 1) {
+                first = 2;
+                me.firstAngle = angle = 360 * value / totalField / 2;
+                for (j = 0; j < i; j++) {
+                    slices[j].startAngle = slices[j].endAngle = me.firstAngle;
+                }
             }
+            
             endAngle = angle - 360 * value / totalField;
             slice = {
                 series: me,
@@ -436,20 +460,11 @@ Ext.define('Ext.chart.series.Pie', {
                 slice.rho = me.radius;
             }
             slices[i] = slice;
-            if((slice.startAngle % 360) == (slice.endAngle % 360)) {
-                slice.startAngle -= 0.0001;
-            }
             angle = endAngle;
-            first++;
         }, me);
-        
         //do all shadows first.
         if (enableShadows) {
             for (i = 0, ln = slices.length; i < ln; i++) {
-                if (this.__excludes && this.__excludes[i]) {
-                    //hidden series
-                    continue;
-                }
                 slice = slices[i];
                 slice.shadowAttrs = [];
                 for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
@@ -464,7 +479,8 @@ Ext.define('Ext.chart.series.Pie', {
                             rho: slice.rho,
                             startRho: rhoAcum + (deltaRho * donut / 100),
                             endRho: rhoAcum + deltaRho
-                        }
+                        },
+                        hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
                     };
                     //create shadows
                     for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
@@ -483,9 +499,7 @@ Ext.define('Ext.chart.series.Pie', {
                                 to: shadowAttr
                             });
                         } else {
-                            shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, {
-                                hidden: false
-                            }), i, store);
+                            shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
                             shadow.setAttributes(shadowAttr, true);
                         }
                         shadows.push(shadow);
@@ -496,10 +510,6 @@ Ext.define('Ext.chart.series.Pie', {
         }
         //do pie slices after.
         for (i = 0, ln = slices.length; i < ln; i++) {
-            if (this.__excludes && this.__excludes[i]) {
-                //hidden series
-                continue;
-            }
             slice = slices[i];
             for (j = 0, rhoAcum = 0; j < layers; j++) {
                 sprite = group.getAt(i * layers + j);
@@ -513,7 +523,8 @@ Ext.define('Ext.chart.series.Pie', {
                         rho: slice.rho,
                         startRho: rhoAcum + (deltaRho * donut / 100),
                         endRho: rhoAcum + deltaRho
-                    } 
+                    },
+                    hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
                 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
                 item = Ext.apply({},
                 rendererAttributes.segment, {
@@ -564,7 +575,7 @@ Ext.define('Ext.chart.series.Pie', {
                 rhoAcum += deltaRho;
             }
         }
-        
+
         // Hide unused bars
         ln = group.getCount();
         for (i = 0; i < ln; i++) {
@@ -597,7 +608,7 @@ Ext.define('Ext.chart.series.Pie', {
             centerY = me.centerY,
             middle = item.middle,
             endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
-        
+
         return me.chart.surface.add(Ext.apply({
             'type': 'text',
             'text-anchor': 'middle',
@@ -629,9 +640,13 @@ Ext.define('Ext.chart.series.Pie', {
             theta = Math.atan2(y, x || 1),
             dg = theta * 180 / Math.PI,
             prevDg;
-        
+        if (this.__excludes && this.__excludes[i]) {
+            opt.hidden = true;
+        }
         function fixAngle(a) {
-            if (a < 0) a += 360;
+            if (a < 0) {
+                a += 360;
+            }
             return a % 360;
         }
 
@@ -675,7 +690,7 @@ Ext.define('Ext.chart.series.Pie', {
         }
         //ensure the object has zero translation
         opt.translate = {
-            x: 0, y: 0    
+            x: 0, y: 0
         };
         if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
             me.onAnimate(label, {
@@ -786,8 +801,8 @@ Ext.define('Ext.chart.series.Pie', {
             startAngle = item.startAngle,
             endAngle = item.endAngle,
             rho = Math.sqrt(dx * dx + dy * dy),
-            angle = Math.atan2(y - cy, x - cx) / me.rad + 360;
-        
+            angle = Math.atan2(y - cy, x - cx) / me.rad;
+
         // normalize to the same range of angles created by drawSeries
         if (angle > me.firstAngle) {
             angle -= 360;
@@ -795,7 +810,7 @@ Ext.define('Ext.chart.series.Pie', {
         return (angle <= startAngle && angle > endAngle
                 && rho >= item.startRho && rho <= item.endRho);
     },
-    
+
     // @private hides all elements in the series.
     hideAll: function() {
         var i, l, shadow, shadows, sh, lsh, sprite;
@@ -821,7 +836,7 @@ Ext.define('Ext.chart.series.Pie', {
             this.drawSeries();
         }
     },
-    
+
     // @private shows all elements in the series.
     showAll: function() {
         if (!isNaN(this._index)) {
@@ -838,13 +853,13 @@ Ext.define('Ext.chart.series.Pie', {
         var me = this,
             rad = me.rad;
         item = item || this.items[this._index];
-        
+
         //TODO(nico): sometimes in IE itemmouseover is triggered
         //twice without triggering itemmouseout in between. This
         //fixes the highlighting bug. Eventually, events should be
         //changed to trigger one itemmouseout between two itemmouseovers.
         this.unHighlightItem();
-        
+
         if (!item || item.sprite && item.sprite._animating) {
             return;
         }
@@ -877,7 +892,7 @@ Ext.define('Ext.chart.series.Pie', {
                 if (Math.abs(y) < 1e-10) {
                     y = 0;
                 }
-                
+
                 if (animate) {
                     label.stopAnimation();
                     label.animate({
@@ -932,7 +947,7 @@ Ext.define('Ext.chart.series.Pie', {
     },
 
     /**
-     * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+     * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
      * @param item {Object} Info about the item; same format as returned by #getItemForPoint
      */
     unHighlightItem: function() {
@@ -1019,7 +1034,7 @@ Ext.define('Ext.chart.series.Pie', {
         }
         me.callParent(arguments);
     },
-    
+
     /**
      * Returns the color of the series (to be displayed as color for the series legend item).
      * @param item {Object} Info about the item; same format as returned by #getItemForPoint