3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.chart.series.Pie
17 * @extends Ext.chart.series.Series
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:
24 * {@img Ext.chart.series.Pie/Ext.chart.series.Pie.png Ext.chart.series.Pie chart series}
26 * var store = Ext.create('Ext.data.JsonStore', {
27 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
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}
37 * Ext.create('Ext.chart.Chart', {
38 * renderTo: Ext.getBody(),
43 * theme: 'Base:gradients',
52 * renderer: function(storeItem, item) {
53 * //calculate and display percentage on hover
55 * store.each(function(rec) {
56 * total += rec.get('data1');
58 * this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
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.
84 Ext.define('Ext.chart.series.Pie', {
86 /* Begin Definitions */
88 alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'],
90 extend: 'Ext.chart.series.Series',
101 * @cfg {Number} highlightDuration
102 * The duration for the pie slice highlight effect.
104 highlightDuration: 150,
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.
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.
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.
131 * @cfg {Boolean} showInLegend
132 * Whether to add the pie chart elements as legend items. Default's false.
137 * @cfg {Array} colorSet
138 * An array of color values which will be used, in order, as the pie slice fill colors.
142 * @cfg {Object} style
143 * An object containing styles for overriding series styles from Theming.
147 constructor: function(config) {
148 this.callParent(arguments);
151 surface = chart.surface,
153 shadow = chart.shadow, i, l, cfg;
161 Ext.apply(me, config, {
165 stroke: 'rgb(200, 200, 200)',
174 stroke: 'rgb(150, 150, 150)',
183 stroke: 'rgb(100, 100, 100)',
190 me.group = surface.getGroup(me.seriesId);
192 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
193 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
196 surface.customAttributes.segment = function(opt) {
197 return me.getSegment(opt);
201 //@private updates some onbefore render parameters.
202 initialize: function() {
204 store = me.chart.substore || me.chart.store;
205 //Add yFields to be used in Legend.js
207 if (me.label.field) {
208 store.each(function(rec) {
209 me.yField.push(rec.get(me.label.field));
214 // @private returns an object with properties for a PieSlice.
215 getSegment: function(opt) {
223 x1 = 0, x2 = 0, x3 = 0, x4 = 0,
224 y1 = 0, y2 = 0, y3 = 0, y4 = 0,
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,
236 x += margin * cos(midAngle);
237 y += margin * sin(midAngle);
239 x1 = x + opt.startRho * cos(a1);
240 y1 = y + opt.startRho * sin(a1);
242 x2 = x + opt.endRho * cos(a1);
243 y2 = y + opt.endRho * sin(a1);
245 x3 = x + opt.startRho * cos(a2);
246 y3 = y + opt.startRho * sin(a2);
248 x4 = x + opt.endRho * cos(a2);
249 y4 = y + opt.endRho * sin(a2);
251 if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
254 //Solves mysterious clipping bug with IE
260 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
268 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
270 ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
276 // @private utility function to calculate the middle point of a pie slice.
277 calcMiddle: function(item) {
283 startAngle = slice.startAngle,
284 endAngle = slice.endAngle,
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);
299 * Draws the series for the current chart.
301 drawSeries: function() {
303 store = me.chart.substore || me.chart.store,
305 animate = me.chart.animate,
306 field = me.angleField || me.field || me.xField,
307 lenField = [].concat(me.lengthField),
309 colors = me.colorSet,
311 surface = chart.surface,
312 chartBBox = chart.chartBBox,
313 enableShadows = chart.shadow,
314 shadowGroups = me.shadowGroups,
315 shadowAttributes = me.shadowAttributes,
316 lnsh = shadowGroups.length,
318 layers = lenField.length,
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],
365 Ext.apply(seriesStyle, me.style || {});
370 //override theme colors
372 colorArrayStyle = me.colorSet;
373 colorArrayLength = colorArrayStyle.length;
376 //if not store or store is empty then there's nothing to draw
377 if (!store || !store.getCount()) {
381 me.unHighlightItem();
382 me.cleanHighlights();
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 = [];
390 store.each(function(record, i) {
391 if (this.__excludes && this.__excludes[i]) {
395 totalField += +record.get(field);
397 for (j = 0, totalLenField = 0; j < layers; j++) {
398 totalLenField += +record.get(lenField[j]);
400 layerTotals[i] = totalLenField;
401 maxLenField = Math.max(maxLenField, totalLenField);
405 store.each(function(record, i) {
406 if (this.__excludes && this.__excludes[i]) {
410 value = record.get(field);
411 middleAngle = angle - 360 * value / totalField / 2;
412 // TODO - Put up an empty circle
413 if (isNaN(middleAngle)) {
419 if (!i || first == 0) {
420 angle = 360 - middleAngle;
421 me.firstAngle = angle;
422 middleAngle = angle - 360 * value / totalField / 2;
424 endAngle = angle - 360 * value / totalField;
433 lenValue = layerTotals[i];
434 slice.rho = me.radius * (lenValue / maxLenField);
436 slice.rho = me.radius;
439 if((slice.startAngle % 360) == (slice.endAngle % 360)) {
440 slice.startAngle -= 0.0001;
446 //do all shadows first.
448 for (i = 0, ln = slices.length; i < ln; i++) {
449 if (this.__excludes && this.__excludes[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 = {
461 startAngle: slice.startAngle,
462 endAngle: slice.endAngle,
465 startRho: rhoAcum + (deltaRho * donut / 100),
466 endRho: rhoAcum + deltaRho
470 for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
471 shadowAttr = shadowAttributes[shindex];
472 shadow = shadowGroups[shindex].getAt(i);
474 shadow = chart.surface.add(Ext.apply({}, {
476 group: shadowGroups[shindex],
477 strokeLinejoin: "round"
478 }, rendererAttributes, shadowAttr));
481 shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply({}, rendererAttributes, shadowAttr), i, store);
482 me.onAnimate(shadow, {
486 shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, {
489 shadow.setAttributes(shadowAttr, true);
491 shadows.push(shadow);
493 slice.shadowAttrs[j] = shadows;
497 //do pie slices after.
498 for (i = 0, ln = slices.length; i < ln; i++) {
499 if (this.__excludes && this.__excludes[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({
510 startAngle: slice.startAngle,
511 endAngle: slice.endAngle,
514 startRho: rhoAcum + (deltaRho * donut / 100),
515 endRho: rhoAcum + deltaRho
517 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
519 rendererAttributes.segment, {
522 storeItem: slice.storeItem,
527 item.shadows = slice.shadowAttrs[j];
530 // Create a new sprite if needed (no height)
532 spriteOptions = Ext.apply({
536 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
537 sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
539 slice.sprite = slice.sprite || [];
540 item.sprite = sprite;
541 slice.sprite.push(sprite);
542 slice.point = [item.middle.x, item.middle.y];
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,
552 this._animating = false;
559 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, {
562 sprite.setAttributes(rendererAttributes, true);
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);
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);
591 // @private callback for when creating a label sprite.
592 onCreateLabel: function(storeItem, item, i, display) {
594 group = me.labelsGroup,
596 centerX = me.centerX,
597 centerY = me.centerY,
598 middle = item.middle,
599 endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
601 return me.chart.surface.add(Ext.apply({
603 'text-anchor': 'middle',
610 // @private callback for when placing a label sprite.
611 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
614 resizing = chart.resizing,
616 format = config.renderer,
617 field = [].concat(config.field),
618 centerX = me.centerX,
619 centerY = me.centerY,
620 middle = item.middle,
625 x = middle.x - centerX,
626 y = middle.y - centerY,
629 theta = Math.atan2(y, x || 1),
630 dg = theta * 180 / Math.PI,
633 function fixAngle(a) {
638 label.setAttributes({
639 text: format(storeItem.get(field[index]))
644 rho = Math.sqrt(x * x + y * y) * 2;
646 opt.x = rho * Math.cos(theta) + centerX;
647 opt.y = rho * Math.sin(theta) + centerY;
652 dg = (dg > 90 && dg < 270) ? dg + 180: dg;
654 prevDg = label.attr.rotation.degrees;
655 if (prevDg != null && Math.abs(prevDg - dg) > 180) {
665 //update rotation angle
676 //ensure the object has zero translation
680 if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
681 me.onAnimate(label, {
685 label.setAttributes(opt, true);
690 // @private callback for when placing a callout sprite.
691 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
694 resizing = chart.resizing,
695 config = me.callouts,
696 centerX = me.centerX,
697 centerY = me.centerY,
698 middle = item.middle,
703 x = middle.x - centerX,
704 y = middle.y - centerY,
707 theta = Math.atan2(y, x || 1),
708 bbox = callout.label.getBBox(),
714 //should be able to config this.
715 rho = item.endRho + offsetFromViz;
716 rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3;
718 opt.x = rho * Math.cos(theta) + centerX;
719 opt.y = rho * Math.sin(theta) + centerY;
721 x = rhoCenter * Math.cos(theta);
722 y = rhoCenter * Math.sin(theta);
725 //set the line from the middle of the pie to the box.
726 me.onAnimate(callout.lines, {
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"]
732 me.onAnimate(callout.box, {
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
741 me.onAnimate(callout.label, {
743 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
744 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
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"]
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
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)
769 callout[p].show(true);
773 // @private handles sprite animation for the series.
774 onAnimate: function(sprite, attr) {
776 return this.callParent(arguments);
779 isItemInPoint: function(x, y, item, i) {
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;
791 // normalize to the same range of angles created by drawSeries
792 if (angle > me.firstAngle) {
795 return (angle <= startAngle && angle > endAngle
796 && rho >= item.startRho && rho <= item.endRho);
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({
811 if (this.slices[this._index].shadowAttrs) {
812 for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) {
814 for (sh = 0, lsh = shadow.length; sh < lsh; sh++) {
815 shadow[sh].setAttributes({
825 // @private shows all elements in the series.
826 showAll: function() {
827 if (!isNaN(this._index)) {
828 this.__excludes[this._index] = false;
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
837 highlightItem: function(item) {
840 item = item || this.items[this._index];
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();
848 if (!item || item.sprite && item.sprite._animating) {
851 me.callParent([item]);
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;
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);
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) {
877 if (Math.abs(y) < 1e-10) {
882 label.stopAnimation();
890 duration: me.highlightDuration
894 label.setAttributes({
903 if (me.chart.shadow && item.shadows) {
905 shadows = item.shadows;
907 for (; i < ln; i++) {
910 itemHighlightSegment = item.sprite._from.segment;
911 for (prop in itemHighlightSegment) {
912 if (! (prop in highlightSegment)) {
913 to[prop] = itemHighlightSegment[prop];
917 segment: Ext.applyIf(to, me.highlightCfg.segment)
920 shadow.stopAnimation();
923 duration: me.highlightDuration
927 shadow.setAttributes(attrs, true);
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
938 unHighlightItem: function() {
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,
952 display = me.label.display,
953 shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs;
955 for (; i < len; i++) {
960 sprite = item.sprite;
961 if (sprite && sprite._highlighted) {
964 label = group.getAt(item.index);
971 display == 'rotate' ? {
975 degrees: label.attr.rotation.degrees
979 label.stopAnimation();
982 duration: me.highlightDuration
986 label.setAttributes(attrs, true);
989 if (shadowsEnabled) {
990 shadows = item.shadows;
991 shadowLen = shadows.length;
992 for (; j < shadowLen; j++) {
994 ihs = item.sprite._to.segment;
995 hs = item.sprite._from.segment;
1002 shadow = shadows[j];
1004 shadow.stopAnimation();
1009 duration: me.highlightDuration
1013 shadow.setAttributes({ segment: to }, true);
1020 me.callParent(arguments);
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
1027 getLegendColor: function(index) {
1029 return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];