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:
25 * var store = Ext.create('Ext.data.JsonStore', {
26 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
28 * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
29 * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
30 * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
31 * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
32 * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
36 * Ext.create('Ext.chart.Chart', {
37 * renderTo: Ext.getBody(),
42 * theme: 'Base:gradients',
51 * renderer: function(storeItem, item) {
52 * // calculate and display percentage on hover
54 * store.each(function(rec) {
55 * total += rec.get('data1');
57 * this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
74 * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
75 * (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.
80 * 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
81 * and size through the `font` parameter.
85 Ext.define('Ext.chart.series.Pie', {
87 /* Begin Definitions */
89 alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'],
91 extend: 'Ext.chart.series.Series',
102 * @cfg {Number} highlightDuration
103 * The duration for the pie slice highlight effect.
105 highlightDuration: 150,
108 * @cfg {String} angleField (required)
109 * The store record field name to be used for the pie angles.
110 * The values bound to this field name must be positive real numbers.
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.
122 * @cfg {Boolean/Number} donut
123 * Whether to set the pie chart as donut chart.
124 * Default's false. Can be set to a particular percentage to set the radius
125 * of the donut chart.
130 * @cfg {Boolean} showInLegend
131 * Whether to add the pie chart elements as legend items. Default's false.
136 * @cfg {Array} colorSet
137 * An array of color values which will be used, in order, as the pie slice fill colors.
141 * @cfg {Object} style
142 * An object containing styles for overriding series styles from Theming.
146 constructor: function(config) {
147 this.callParent(arguments);
150 surface = chart.surface,
152 shadow = chart.shadow, i, l, cfg;
160 Ext.apply(me, config, {
164 stroke: 'rgb(200, 200, 200)',
173 stroke: 'rgb(150, 150, 150)',
182 stroke: 'rgb(100, 100, 100)',
189 me.group = surface.getGroup(me.seriesId);
191 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
192 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
195 surface.customAttributes.segment = function(opt) {
196 return me.getSegment(opt);
198 me.__excludes = me.__excludes || [];
201 //@private updates some onbefore render parameters.
202 initialize: function() {
204 store = me.chart.getChartStore();
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) {
222 x1 = 0, x2 = 0, x3 = 0, x4 = 0,
223 y1 = 0, y2 = 0, y3 = 0, y4 = 0,
224 x5 = 0, y5 = 0, x6 = 0, y6 = 0,
226 startAngle = opt.startAngle,
227 endAngle = opt.endAngle,
228 midAngle = (startAngle + endAngle) / 2 * rad,
229 margin = opt.margin || 0,
230 a1 = Math.min(startAngle, endAngle) * rad,
231 a2 = Math.max(startAngle, endAngle) * rad,
232 c1 = cos(a1), s1 = sin(a1),
233 c2 = cos(a2), s2 = sin(a2),
234 cm = cos(midAngle), sm = sin(midAngle),
235 flag = 0, hsqr2 = 0.7071067811865476; // sqrt(0.5)
237 if (a2 - a1 < delta) {
246 x2 = x + opt.endRho * c1;
247 y2 = y + opt.endRho * s1;
249 x4 = x + opt.endRho * c2;
250 y4 = y + opt.endRho * s2;
252 if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
258 x6 = x + opt.endRho * cm;
259 y6 = y + opt.endRho * sm;
261 // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
262 // Better fix the VML engine for half circles.
264 if (opt.startRho !== 0) {
265 x1 = x + opt.startRho * c1;
266 y1 = y + opt.startRho * s1;
268 x3 = x + opt.startRho * c2;
269 y3 = y + opt.startRho * s2;
271 x5 = x + opt.startRho * cm;
272 y5 = y + opt.startRho * sm;
277 ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
278 ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
280 ["A", opt.startRho, opt.startRho, 0, flag, 0, x5, y5], ["L", x5, y5],
281 ["A", opt.startRho, opt.startRho, 0, 0, 0, x1, y1], ["L", x1, y1],
290 ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
291 ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
299 // @private utility function to calculate the middle point of a pie slice.
300 calcMiddle: function(item) {
306 startAngle = slice.startAngle,
307 endAngle = slice.endAngle,
309 midAngle = -(startAngle + endAngle) * rad / 2,
310 r = (item.endRho + item.startRho) / 2,
311 xm = x + r * Math.cos(midAngle),
312 ym = y - r * Math.sin(midAngle);
321 * Draws the series for the current chart.
323 drawSeries: function() {
325 store = me.chart.getChartStore(),
327 animate = me.chart.animate,
328 field = me.angleField || me.field || me.xField,
329 lenField = [].concat(me.lengthField),
331 colors = me.colorSet,
333 surface = chart.surface,
334 chartBBox = chart.chartBBox,
335 enableShadows = chart.shadow,
336 shadowGroups = me.shadowGroups,
337 shadowAttributes = me.shadowAttributes,
338 lnsh = shadowGroups.length,
340 layers = lenField.length,
353 seriesStyle = me.seriesStyle,
354 seriesLabelStyle = me.seriesLabelStyle,
355 colorArrayStyle = me.colorArrayStyle,
356 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
357 gutterX = chart.maxGutter[0],
358 gutterY = chart.maxGutter[1],
388 Ext.apply(seriesStyle, me.style || {});
393 //override theme colors
395 colorArrayStyle = me.colorSet;
396 colorArrayLength = colorArrayStyle.length;
399 //if not store or store is empty then there's nothing to draw
400 if (!store || !store.getCount()) {
404 me.unHighlightItem();
405 me.cleanHighlights();
407 centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
408 centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
409 me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
410 me.slices = slices = [];
411 me.items = items = [];
413 store.each(function(record, i) {
414 if (this.__excludes && this.__excludes[i]) {
418 totalField += +record.get(field);
420 for (j = 0, totalLenField = 0; j < layers; j++) {
421 totalLenField += +record.get(lenField[j]);
423 layerTotals[i] = totalLenField;
424 maxLenField = Math.max(maxLenField, totalLenField);
428 totalField = totalField || 1;
429 store.each(function(record, i) {
430 if (this.__excludes && this.__excludes[i]) {
433 value = record.get(field);
442 me.firstAngle = angle = 360 * value / totalField / 2;
443 for (j = 0; j < i; j++) {
444 slices[j].startAngle = slices[j].endAngle = me.firstAngle;
448 endAngle = angle - 360 * value / totalField;
457 lenValue = layerTotals[i];
458 slice.rho = me.radius * (lenValue / maxLenField);
460 slice.rho = me.radius;
465 //do all shadows first.
467 for (i = 0, ln = slices.length; i < ln; i++) {
469 slice.shadowAttrs = [];
470 for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
471 sprite = group.getAt(i * layers + j);
472 deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
473 //set pie slice properties
474 rendererAttributes = {
476 startAngle: slice.startAngle,
477 endAngle: slice.endAngle,
480 startRho: rhoAcum + (deltaRho * donut / 100),
481 endRho: rhoAcum + deltaRho
483 hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
486 for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
487 shadowAttr = shadowAttributes[shindex];
488 shadow = shadowGroups[shindex].getAt(i);
490 shadow = chart.surface.add(Ext.apply({}, {
492 group: shadowGroups[shindex],
493 strokeLinejoin: "round"
494 }, rendererAttributes, shadowAttr));
497 shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply({}, rendererAttributes, shadowAttr), i, store);
498 me.onAnimate(shadow, {
502 shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
503 shadow.setAttributes(shadowAttr, true);
505 shadows.push(shadow);
507 slice.shadowAttrs[j] = shadows;
511 //do pie slices after.
512 for (i = 0, ln = slices.length; i < ln; i++) {
514 for (j = 0, rhoAcum = 0; j < layers; j++) {
515 sprite = group.getAt(i * layers + j);
516 deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
517 //set pie slice properties
518 rendererAttributes = Ext.apply({
520 startAngle: slice.startAngle,
521 endAngle: slice.endAngle,
524 startRho: rhoAcum + (deltaRho * donut / 100),
525 endRho: rhoAcum + deltaRho
527 hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
528 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
530 rendererAttributes.segment, {
533 storeItem: slice.storeItem,
538 item.shadows = slice.shadowAttrs[j];
541 // Create a new sprite if needed (no height)
543 spriteOptions = Ext.apply({
547 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
548 sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
550 slice.sprite = slice.sprite || [];
551 item.sprite = sprite;
552 slice.sprite.push(sprite);
553 slice.point = [item.middle.x, item.middle.y];
555 rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store);
556 sprite._to = rendererAttributes;
557 sprite._animating = true;
558 me.onAnimate(sprite, {
559 to: rendererAttributes,
563 this._animating = false;
570 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, {
573 sprite.setAttributes(rendererAttributes, true);
580 ln = group.getCount();
581 for (i = 0; i < ln; i++) {
582 if (!slices[(i / layers) >> 0] && group.getAt(i)) {
583 group.getAt(i).hide(true);
587 lnsh = shadowGroups.length;
588 for (shindex = 0; shindex < ln; shindex++) {
589 if (!slices[(shindex / layers) >> 0]) {
590 for (j = 0; j < lnsh; j++) {
591 if (shadowGroups[j].getAt(shindex)) {
592 shadowGroups[j].getAt(shindex).hide(true);
602 // @private callback for when creating a label sprite.
603 onCreateLabel: function(storeItem, item, i, display) {
605 group = me.labelsGroup,
607 centerX = me.centerX,
608 centerY = me.centerY,
609 middle = item.middle,
610 endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
612 return me.chart.surface.add(Ext.apply({
614 'text-anchor': 'middle',
621 // @private callback for when placing a label sprite.
622 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
625 resizing = chart.resizing,
627 format = config.renderer,
628 field = [].concat(config.field),
629 centerX = me.centerX,
630 centerY = me.centerY,
631 middle = item.middle,
636 x = middle.x - centerX,
637 y = middle.y - centerY,
640 theta = Math.atan2(y, x || 1),
641 dg = theta * 180 / Math.PI,
643 if (this.__excludes && this.__excludes[i]) {
646 function fixAngle(a) {
653 label.setAttributes({
654 text: format(storeItem.get(field[index]))
659 rho = Math.sqrt(x * x + y * y) * 2;
661 opt.x = rho * Math.cos(theta) + centerX;
662 opt.y = rho * Math.sin(theta) + centerY;
667 dg = (dg > 90 && dg < 270) ? dg + 180: dg;
669 prevDg = label.attr.rotation.degrees;
670 if (prevDg != null && Math.abs(prevDg - dg) > 180) {
680 //update rotation angle
691 //ensure the object has zero translation
695 if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
696 me.onAnimate(label, {
700 label.setAttributes(opt, true);
705 // @private callback for when placing a callout sprite.
706 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
709 resizing = chart.resizing,
710 config = me.callouts,
711 centerX = me.centerX,
712 centerY = me.centerY,
713 middle = item.middle,
718 x = middle.x - centerX,
719 y = middle.y - centerY,
722 theta = Math.atan2(y, x || 1),
723 bbox = callout.label.getBBox(),
729 //should be able to config this.
730 rho = item.endRho + offsetFromViz;
731 rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3;
733 opt.x = rho * Math.cos(theta) + centerX;
734 opt.y = rho * Math.sin(theta) + centerY;
736 x = rhoCenter * Math.cos(theta);
737 y = rhoCenter * Math.sin(theta);
740 //set the line from the middle of the pie to the box.
741 me.onAnimate(callout.lines, {
743 path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
747 me.onAnimate(callout.box, {
749 x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
750 y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
751 width: bbox.width + 2 * offsetBox,
752 height: bbox.height + 2 * offsetBox
756 me.onAnimate(callout.label, {
758 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
759 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
763 //set the line from the middle of the pie to the box.
764 callout.lines.setAttributes({
765 path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
769 callout.box.setAttributes({
770 x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
771 y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
772 width: bbox.width + 2 * offsetBox,
773 height: bbox.height + 2 * offsetBox
777 callout.label.setAttributes({
778 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
779 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
784 callout[p].show(true);
788 // @private handles sprite animation for the series.
789 onAnimate: function(sprite, attr) {
791 return this.callParent(arguments);
794 isItemInPoint: function(x, y, item, i) {
801 startAngle = item.startAngle,
802 endAngle = item.endAngle,
803 rho = Math.sqrt(dx * dx + dy * dy),
804 angle = Math.atan2(y - cy, x - cx) / me.rad;
806 // normalize to the same range of angles created by drawSeries
807 if (angle > me.firstAngle) {
810 return (angle <= startAngle && angle > endAngle
811 && rho >= item.startRho && rho <= item.endRho);
814 // @private hides all elements in the series.
815 hideAll: function() {
816 var i, l, shadow, shadows, sh, lsh, sprite;
817 if (!isNaN(this._index)) {
818 this.__excludes = this.__excludes || [];
819 this.__excludes[this._index] = true;
820 sprite = this.slices[this._index].sprite;
821 for (sh = 0, lsh = sprite.length; sh < lsh; sh++) {
822 sprite[sh].setAttributes({
826 if (this.slices[this._index].shadowAttrs) {
827 for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) {
829 for (sh = 0, lsh = shadow.length; sh < lsh; sh++) {
830 shadow[sh].setAttributes({
840 // @private shows all elements in the series.
841 showAll: function() {
842 if (!isNaN(this._index)) {
843 this.__excludes[this._index] = false;
849 * Highlight the specified item. If no item is provided the whole series will be highlighted.
850 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
852 highlightItem: function(item) {
855 item = item || this.items[this._index];
857 //TODO(nico): sometimes in IE itemmouseover is triggered
858 //twice without triggering itemmouseout in between. This
859 //fixes the highlighting bug. Eventually, events should be
860 //changed to trigger one itemmouseout between two itemmouseovers.
861 this.unHighlightItem();
863 if (!item || item.sprite && item.sprite._animating) {
866 me.callParent([item]);
870 if ('segment' in me.highlightCfg) {
871 var highlightSegment = me.highlightCfg.segment,
872 animate = me.chart.animate,
873 attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop;
875 if (me.labelsGroup) {
876 var group = me.labelsGroup,
877 display = me.label.display,
878 label = group.getAt(item.index),
879 middle = (item.startAngle + item.endAngle) / 2 * rad,
880 r = highlightSegment.margin || 0,
881 x = r * Math.cos(middle),
882 y = r * Math.sin(middle);
884 //TODO(nico): rounding to 1e-10
885 //gives the right translation. Translation
886 //was buggy for very small numbers. In this
887 //case we're not looking to translate to very small
888 //numbers but not to translate at all.
889 if (Math.abs(x) < 1e-10) {
892 if (Math.abs(y) < 1e-10) {
897 label.stopAnimation();
905 duration: me.highlightDuration
909 label.setAttributes({
918 if (me.chart.shadow && item.shadows) {
920 shadows = item.shadows;
922 for (; i < ln; i++) {
925 itemHighlightSegment = item.sprite._from.segment;
926 for (prop in itemHighlightSegment) {
927 if (! (prop in highlightSegment)) {
928 to[prop] = itemHighlightSegment[prop];
932 segment: Ext.applyIf(to, me.highlightCfg.segment)
935 shadow.stopAnimation();
938 duration: me.highlightDuration
942 shadow.setAttributes(attrs, true);
950 * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
951 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
953 unHighlightItem: function() {
959 if (('segment' in me.highlightCfg) && me.items) {
960 var items = me.items,
961 animate = me.chart.animate,
962 shadowsEnabled = !!me.chart.shadow,
963 group = me.labelsGroup,
967 display = me.label.display,
968 shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs;
970 for (; i < len; i++) {
975 sprite = item.sprite;
976 if (sprite && sprite._highlighted) {
979 label = group.getAt(item.index);
986 display == 'rotate' ? {
990 degrees: label.attr.rotation.degrees
994 label.stopAnimation();
997 duration: me.highlightDuration
1001 label.setAttributes(attrs, true);
1004 if (shadowsEnabled) {
1005 shadows = item.shadows;
1006 shadowLen = shadows.length;
1007 for (; j < shadowLen; j++) {
1009 ihs = item.sprite._to.segment;
1010 hs = item.sprite._from.segment;
1017 shadow = shadows[j];
1019 shadow.stopAnimation();
1024 duration: me.highlightDuration
1028 shadow.setAttributes({ segment: to }, true);
1035 me.callParent(arguments);
1039 * Returns the color of the series (to be displayed as color for the series legend item).
1040 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1042 getLegendColor: function(index) {
1044 return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];