2 * @class Ext.chart.series.Pie
3 * @extends Ext.chart.series.Series
5 * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
6 * categories that also have a meaning as a whole.
7 * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
8 * documentation for more information. A typical configuration object for the pie series could be:
10 {@img Ext.chart.series.Pie/Ext.chart.series.Pie.png Ext.chart.series.Pie chart series}
12 var store = Ext.create('Ext.data.JsonStore', {
13 fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
15 {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
16 {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
17 {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
18 {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
19 {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
23 Ext.create('Ext.chart.Chart', {
24 renderTo: Ext.getBody(),
29 theme: 'Base:gradients',
38 renderer: function(storeItem, item) {
39 //calculate and display percentage on hover
41 store.each(function(rec) {
42 total += rec.get('data1');
44 this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
63 * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
64 * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
65 * 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
66 * 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.
67 * 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
68 * and size through the `font` parameter.
73 Ext.define('Ext.chart.series.Pie', {
75 /* Begin Definitions */
77 alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'],
79 extend: 'Ext.chart.series.Series',
90 * @cfg {Number} highlightDuration
91 * The duration for the pie slice highlight effect.
93 highlightDuration: 150,
96 * @cfg {String} angleField
97 * The store record field name to be used for the pie angles.
98 * The values bound to this field name must be positive real numbers.
99 * This parameter is required.
104 * @cfg {String} lengthField
105 * The store record field name to be used for the pie slice lengths.
106 * The values bound to this field name must be positive real numbers.
107 * This parameter is optional.
112 * @cfg {Boolean|Number} donut
113 * Whether to set the pie chart as donut chart.
114 * Default's false. Can be set to a particular percentage to set the radius
115 * of the donut chart.
120 * @cfg {Boolean} showInLegend
121 * Whether to add the pie chart elements as legend items. Default's false.
126 * @cfg {Array} colorSet
127 * An array of color values which will be used, in order, as the pie slice fill colors.
131 * @cfg {Object} style
132 * An object containing styles for overriding series styles from Theming.
136 constructor: function(config) {
137 this.callParent(arguments);
140 surface = chart.surface,
142 shadow = chart.shadow, i, l, cfg;
150 Ext.apply(me, config, {
154 stroke: 'rgb(200, 200, 200)',
163 stroke: 'rgb(150, 150, 150)',
172 stroke: 'rgb(100, 100, 100)',
179 me.group = surface.getGroup(me.seriesId);
181 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
182 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
185 surface.customAttributes.segment = function(opt) {
186 return me.getSegment(opt);
190 //@private updates some onbefore render parameters.
191 initialize: function() {
193 store = me.chart.substore || me.chart.store;
194 //Add yFields to be used in Legend.js
196 if (me.label.field) {
197 store.each(function(rec) {
198 me.yField.push(rec.get(me.label.field));
203 // @private returns an object with properties for a PieSlice.
204 getSegment: function(opt) {
212 x1 = 0, x2 = 0, x3 = 0, x4 = 0,
213 y1 = 0, y2 = 0, y3 = 0, y4 = 0,
215 r = opt.endRho - opt.startRho,
216 startAngle = opt.startAngle,
217 endAngle = opt.endAngle,
218 midAngle = (startAngle + endAngle) / 2 * rad,
219 margin = opt.margin || 0,
220 flag = abs(endAngle - startAngle) > 180,
221 a1 = Math.min(startAngle, endAngle) * rad,
222 a2 = Math.max(startAngle, endAngle) * rad,
225 x += margin * cos(midAngle);
226 y += margin * sin(midAngle);
228 x1 = x + opt.startRho * cos(a1);
229 y1 = y + opt.startRho * sin(a1);
231 x2 = x + opt.endRho * cos(a1);
232 y2 = y + opt.endRho * sin(a1);
234 x3 = x + opt.startRho * cos(a2);
235 y3 = y + opt.startRho * sin(a2);
237 x4 = x + opt.endRho * cos(a2);
238 y4 = y + opt.endRho * sin(a2);
240 if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
243 //Solves mysterious clipping bug with IE
249 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
257 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
259 ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
265 // @private utility function to calculate the middle point of a pie slice.
266 calcMiddle: function(item) {
272 startAngle = slice.startAngle,
273 endAngle = slice.endAngle,
275 a1 = Math.min(startAngle, endAngle) * rad,
276 a2 = Math.max(startAngle, endAngle) * rad,
277 midAngle = -(a1 + (a2 - a1) / 2),
278 xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle),
279 ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle);
288 * Draws the series for the current chart.
290 drawSeries: function() {
292 store = me.chart.substore || me.chart.store,
294 animate = me.chart.animate,
295 field = me.angleField || me.field || me.xField,
296 lenField = [].concat(me.lengthField),
298 colors = me.colorSet,
300 surface = chart.surface,
301 chartBBox = chart.chartBBox,
302 enableShadows = chart.shadow,
303 shadowGroups = me.shadowGroups,
304 shadowAttributes = me.shadowAttributes,
305 lnsh = shadowGroups.length,
307 layers = lenField.length,
320 seriesStyle = me.seriesStyle,
321 seriesLabelStyle = me.seriesLabelStyle,
322 colorArrayStyle = me.colorArrayStyle,
323 colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
324 gutterX = chart.maxGutter[0],
325 gutterY = chart.maxGutter[1],
354 Ext.apply(seriesStyle, me.style || {});
359 //override theme colors
361 colorArrayStyle = me.colorSet;
362 colorArrayLength = colorArrayStyle.length;
365 //if not store or store is empty then there's nothing to draw
366 if (!store || !store.getCount()) {
370 me.unHighlightItem();
371 me.cleanHighlights();
373 centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
374 centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
375 me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
376 me.slices = slices = [];
377 me.items = items = [];
379 store.each(function(record, i) {
380 if (this.__excludes && this.__excludes[i]) {
384 totalField += +record.get(field);
386 for (j = 0, totalLenField = 0; j < layers; j++) {
387 totalLenField += +record.get(lenField[j]);
389 layerTotals[i] = totalLenField;
390 maxLenField = Math.max(maxLenField, totalLenField);
394 store.each(function(record, i) {
395 if (this.__excludes && this.__excludes[i]) {
399 value = record.get(field);
400 middleAngle = angle - 360 * value / totalField / 2;
401 // TODO - Put up an empty circle
402 if (isNaN(middleAngle)) {
408 if (!i || first == 0) {
409 angle = 360 - middleAngle;
410 me.firstAngle = angle;
411 middleAngle = angle - 360 * value / totalField / 2;
413 endAngle = angle - 360 * value / totalField;
422 lenValue = layerTotals[i];
423 slice.rho = me.radius * (lenValue / maxLenField);
425 slice.rho = me.radius;
428 if((slice.startAngle % 360) == (slice.endAngle % 360)) {
429 slice.startAngle -= 0.0001;
435 //do all shadows first.
437 for (i = 0, ln = slices.length; i < ln; i++) {
438 if (this.__excludes && this.__excludes[i]) {
443 slice.shadowAttrs = [];
444 for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
445 sprite = group.getAt(i * layers + j);
446 deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
447 //set pie slice properties
448 rendererAttributes = {
450 startAngle: slice.startAngle,
451 endAngle: slice.endAngle,
454 startRho: rhoAcum + (deltaRho * donut / 100),
455 endRho: rhoAcum + deltaRho
459 for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
460 shadowAttr = shadowAttributes[shindex];
461 shadow = shadowGroups[shindex].getAt(i);
463 shadow = chart.surface.add(Ext.apply({},
466 group: shadowGroups[shindex],
467 strokeLinejoin: "round"
469 rendererAttributes, shadowAttr));
472 rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply({},
473 rendererAttributes, shadowAttr), i, store);
474 me.onAnimate(shadow, {
475 to: rendererAttributes
478 rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, {
481 shadow.setAttributes(rendererAttributes, true);
483 shadows.push(shadow);
485 slice.shadowAttrs[j] = shadows;
489 //do pie slices after.
490 for (i = 0, ln = slices.length; i < ln; i++) {
491 if (this.__excludes && this.__excludes[i]) {
496 for (j = 0, rhoAcum = 0; j < layers; j++) {
497 sprite = group.getAt(i * layers + j);
498 deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
499 //set pie slice properties
500 rendererAttributes = Ext.apply({
502 startAngle: slice.startAngle,
503 endAngle: slice.endAngle,
506 startRho: rhoAcum + (deltaRho * donut / 100),
507 endRho: rhoAcum + deltaRho
509 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
511 rendererAttributes.segment, {
514 storeItem: slice.storeItem,
519 item.shadows = slice.shadowAttrs[j];
522 // Create a new sprite if needed (no height)
524 spriteOptions = Ext.apply({
528 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
529 sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
531 slice.sprite = slice.sprite || [];
532 item.sprite = sprite;
533 slice.sprite.push(sprite);
534 slice.point = [item.middle.x, item.middle.y];
536 rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store);
537 sprite._to = rendererAttributes;
538 sprite._animating = true;
539 me.onAnimate(sprite, {
540 to: rendererAttributes,
544 this._animating = false;
551 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, {
554 sprite.setAttributes(rendererAttributes, true);
561 ln = group.getCount();
562 for (i = 0; i < ln; i++) {
563 if (!slices[(i / layers) >> 0] && group.getAt(i)) {
564 group.getAt(i).hide(true);
568 lnsh = shadowGroups.length;
569 for (shindex = 0; shindex < ln; shindex++) {
570 if (!slices[(shindex / layers) >> 0]) {
571 for (j = 0; j < lnsh; j++) {
572 if (shadowGroups[j].getAt(shindex)) {
573 shadowGroups[j].getAt(shindex).hide(true);
583 // @private callback for when creating a label sprite.
584 onCreateLabel: function(storeItem, item, i, display) {
586 group = me.labelsGroup,
588 centerX = me.centerX,
589 centerY = me.centerY,
590 middle = item.middle,
591 endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
593 return me.chart.surface.add(Ext.apply({
595 'text-anchor': 'middle',
602 // @private callback for when placing a label sprite.
603 onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
606 resizing = chart.resizing,
608 format = config.renderer,
609 field = [].concat(config.field),
610 centerX = me.centerX,
611 centerY = me.centerY,
612 middle = item.middle,
617 x = middle.x - centerX,
618 y = middle.y - centerY,
621 theta = Math.atan2(y, x || 1),
622 dg = theta * 180 / Math.PI,
625 function fixAngle(a) {
630 label.setAttributes({
631 text: format(storeItem.get(field[index]))
636 rho = Math.sqrt(x * x + y * y) * 2;
638 opt.x = rho * Math.cos(theta) + centerX;
639 opt.y = rho * Math.sin(theta) + centerY;
644 dg = (dg > 90 && dg < 270) ? dg + 180: dg;
646 prevDg = label.attr.rotation.degrees;
647 if (prevDg != null && Math.abs(prevDg - dg) > 180) {
657 //update rotation angle
668 //ensure the object has zero translation
672 if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
673 me.onAnimate(label, {
677 label.setAttributes(opt, true);
682 // @private callback for when placing a callout sprite.
683 onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
686 resizing = chart.resizing,
687 config = me.callouts,
688 centerX = me.centerX,
689 centerY = me.centerY,
690 middle = item.middle,
695 x = middle.x - centerX,
696 y = middle.y - centerY,
699 theta = Math.atan2(y, x || 1),
700 bbox = callout.label.getBBox(),
706 //should be able to config this.
707 rho = item.endRho + offsetFromViz;
708 rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3;
710 opt.x = rho * Math.cos(theta) + centerX;
711 opt.y = rho * Math.sin(theta) + centerY;
713 x = rhoCenter * Math.cos(theta);
714 y = rhoCenter * Math.sin(theta);
717 //set the line from the middle of the pie to the box.
718 me.onAnimate(callout.lines, {
720 path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
724 me.onAnimate(callout.box, {
726 x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
727 y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
728 width: bbox.width + 2 * offsetBox,
729 height: bbox.height + 2 * offsetBox
733 me.onAnimate(callout.label, {
735 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
736 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
740 //set the line from the middle of the pie to the box.
741 callout.lines.setAttributes({
742 path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
746 callout.box.setAttributes({
747 x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
748 y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
749 width: bbox.width + 2 * offsetBox,
750 height: bbox.height + 2 * offsetBox
754 callout.label.setAttributes({
755 x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
756 y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
761 callout[p].show(true);
765 // @private handles sprite animation for the series.
766 onAnimate: function(sprite, attr) {
768 return this.callParent(arguments);
771 isItemInPoint: function(x, y, item, i) {
778 startAngle = item.startAngle,
779 endAngle = item.endAngle,
780 rho = Math.sqrt(dx * dx + dy * dy),
781 angle = Math.atan2(y - cy, x - cx) / me.rad + 360;
783 // normalize to the same range of angles created by drawSeries
784 if (angle > me.firstAngle) {
787 return (angle <= startAngle && angle > endAngle
788 && rho >= item.startRho && rho <= item.endRho);
791 // @private hides all elements in the series.
792 hideAll: function() {
793 var i, l, shadow, shadows, sh, lsh, sprite;
794 if (!isNaN(this._index)) {
795 this.__excludes = this.__excludes || [];
796 this.__excludes[this._index] = true;
797 sprite = this.slices[this._index].sprite;
798 for (sh = 0, lsh = sprite.length; sh < lsh; sh++) {
799 sprite[sh].setAttributes({
803 if (this.slices[this._index].shadowAttrs) {
804 for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) {
806 for (sh = 0, lsh = shadow.length; sh < lsh; sh++) {
807 shadow[sh].setAttributes({
817 // @private shows all elements in the series.
818 showAll: function() {
819 if (!isNaN(this._index)) {
820 this.__excludes[this._index] = false;
826 * Highlight the specified item. If no item is provided the whole series will be highlighted.
827 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
829 highlightItem: function(item) {
832 item = item || this.items[this._index];
834 //TODO(nico): sometimes in IE itemmouseover is triggered
835 //twice without triggering itemmouseout in between. This
836 //fixes the highlighting bug. Eventually, events should be
837 //changed to trigger one itemmouseout between two itemmouseovers.
838 this.unHighlightItem();
840 if (!item || item.sprite && item.sprite._animating) {
843 me.callParent([item]);
847 if ('segment' in me.highlightCfg) {
848 var highlightSegment = me.highlightCfg.segment,
849 animate = me.chart.animate,
850 attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop;
852 if (me.labelsGroup) {
853 var group = me.labelsGroup,
854 display = me.label.display,
855 label = group.getAt(item.index),
856 middle = (item.startAngle + item.endAngle) / 2 * rad,
857 r = highlightSegment.margin || 0,
858 x = r * Math.cos(middle),
859 y = r * Math.sin(middle);
861 //TODO(nico): rounding to 1e-10
862 //gives the right translation. Translation
863 //was buggy for very small numbers. In this
864 //case we're not looking to translate to very small
865 //numbers but not to translate at all.
866 if (Math.abs(x) < 1e-10) {
869 if (Math.abs(y) < 1e-10) {
874 label.stopAnimation();
882 duration: me.highlightDuration
886 label.setAttributes({
895 if (me.chart.shadow && item.shadows) {
897 shadows = item.shadows;
899 for (; i < ln; i++) {
902 itemHighlightSegment = item.sprite._from.segment;
903 for (prop in itemHighlightSegment) {
904 if (! (prop in highlightSegment)) {
905 to[prop] = itemHighlightSegment[prop];
909 segment: Ext.applyIf(to, me.highlightCfg.segment)
912 shadow.stopAnimation();
915 duration: me.highlightDuration
919 shadow.setAttributes(attrs, true);
927 * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
928 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
930 unHighlightItem: function() {
936 if (('segment' in me.highlightCfg) && me.items) {
937 var items = me.items,
938 animate = me.chart.animate,
939 shadowsEnabled = !!me.chart.shadow,
940 group = me.labelsGroup,
944 display = me.label.display,
945 shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs;
947 for (; i < len; i++) {
952 sprite = item.sprite;
953 if (sprite && sprite._highlighted) {
956 label = group.getAt(item.index);
963 display == 'rotate' ? {
967 degrees: label.attr.rotation.degrees
971 label.stopAnimation();
974 duration: me.highlightDuration
978 label.setAttributes(attrs, true);
981 if (shadowsEnabled) {
982 shadows = item.shadows;
983 shadowLen = shadows.length;
984 for (; j < shadowLen; j++) {
986 ihs = item.sprite._to.segment;
987 hs = item.sprite._from.segment;
996 shadow.stopAnimation();
1001 duration: me.highlightDuration
1005 shadow.setAttributes({ segment: to }, true);
1012 me.callParent(arguments);
1016 * Returns the color of the series (to be displayed as color for the series legend item).
1017 * @param item {Object} Info about the item; same format as returned by #getItemForPoint
1019 getLegendColor: function(index) {
1021 return me.colorArrayStyle[index % me.colorArrayStyle.length];