X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/series/Pie.js diff --git a/src/chart/series/Pie.js b/src/chart/series/Pie.js new file mode 100644 index 00000000..0043662a --- /dev/null +++ b/src/chart/series/Pie.js @@ -0,0 +1,1024 @@ +/** + * @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 + * 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 + * 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} +

+    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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        store: store,
+        theme: 'Base:gradients',
+        series: [{
+            type: 'pie',
+            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) + '%');
+              }
+            },
+            highlight: {
+              segment: {
+                margin: 20
+              }
+            },
+            label: {
+                field: 'name',
+                display: 'rotate',
+                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. + * + * @xtype pie + * + */ +Ext.define('Ext.chart.series.Pie', { + + /* Begin Definitions */ + + alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'], + + extend: 'Ext.chart.series.Series', + + /* End Definitions */ + + type: "pie", + + alias: 'series.pie', + + rad: Math.PI / 180, + + /** + * @cfg {Number} highlightDuration + * The duration for the pie slice highlight effect. + */ + highlightDuration: 150, + + /** + * @cfg {String} angleField + * 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, + + /** + * @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 + * 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. + */ + donut: false, + + /** + * @cfg {Boolean} showInLegend + * Whether to add the pie chart elements as legend items. Default's false. + */ + showInLegend: false, + + /** + * @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, + chart = me.chart, + surface = chart.surface, + store = chart.store, + shadow = chart.shadow, i, l, cfg; + Ext.applyIf(me, { + highlightCfg: { + segment: { + margin: 20 + } + } + }); + Ext.apply(me, config, { + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 1, + stroke: 'rgb(200, 200, 200)', + translate: { + x: 1.2, + y: 2 + } + }, + { + "stroke-width": 4, + "stroke-opacity": 1, + stroke: 'rgb(150, 150, 150)', + translate: { + x: 0.9, + y: 1.5 + } + }, + { + "stroke-width": 2, + "stroke-opacity": 1, + stroke: 'rgb(100, 100, 100)', + translate: { + x: 0.6, + y: 1 + } + }] + }); + me.group = surface.getGroup(me.seriesId); + if (shadow) { + for (i = 0, l = me.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + surface.customAttributes.segment = function(opt) { + return me.getSegment(opt); + }; + }, + + //@private updates some onbefore render parameters. + initialize: function() { + var me = this, + store = me.chart.substore || me.chart.store; + //Add yFields to be used in Legend.js + me.yField = []; + if (me.label.field) { + store.each(function(rec) { + me.yField.push(rec.get(me.label.field)); + }); + } + }, + + // @private returns an object with properties for a PieSlice. + getSegment: function(opt) { + var me = this, + 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, + 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; + + x += margin * cos(midAngle); + y += margin * sin(midAngle); + + x1 = x + opt.startRho * cos(a1); + y1 = y + opt.startRho * sin(a1); + + x2 = x + opt.endRho * cos(a1); + y2 = y + opt.endRho * sin(a1); + + x3 = x + opt.startRho * cos(a2); + y3 = y + opt.startRho * sin(a2); + + x4 = x + opt.endRho * cos(a2); + y4 = y + opt.endRho * sin(a2); + + if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) { + singleSlice = true; + } + //Solves mysterious clipping bug with IE + if (singleSlice) { + return { + path: [ + ["M", x1, y1], + ["L", x2, y2], + ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4], + ["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"]] + }; + } + }, + + // @private utility function to calculate the middle point of a pie slice. + calcMiddle: function(item) { + var me = this, + rad = me.rad, + slice = item.slice, + x = me.centerX, + y = me.centerY, + 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); + + item.middle = { + x: xm, + y: ym + }; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + store = me.chart.substore || me.chart.store, + group = me.group, + animate = me.chart.animate, + field = me.angleField || me.field || me.xField, + lenField = [].concat(me.lengthField), + totalLenField = 0, + colors = me.colorSet, + chart = me.chart, + surface = chart.surface, + chartBBox = chart.chartBBox, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + lnsh = shadowGroups.length, + rad = me.rad, + layers = lenField.length, + rhoAcum = 0, + donut = +me.donut, + layerTotals = [], + values = {}, + fieldLength, + items = [], + passed = false, + totalField = 0, + maxLenField = 0, + cut = 9, + defcut = true, + angle = 0, + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + gutterX = chart.maxGutter[0], + gutterY = chart.maxGutter[1], + rendererAttributes, + shadowGroup, + shadowAttr, + shadows, + shadow, + shindex, + centerX, + centerY, + deltaRho, + first = 0, + slice, + slices, + sprite, + value, + item, + lenValue, + ln, + record, + i, + j, + startAngle, + endAngle, + middleAngle, + sliceLength, + path, + p, + spriteOptions, bbox; + + Ext.apply(seriesStyle, me.style || {}); + + me.setBBox(); + bbox = me.bbox; + + //override theme colors + if (me.colorSet) { + 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(); + + centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); + centerY = me.centerY = chartBBox.y + (chartBBox.height / 2); + me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y); + me.slices = slices = []; + me.items = items = []; + + store.each(function(record, i) { + if (this.__excludes && this.__excludes[i]) { + //hidden series + return; + } + totalField += +record.get(field); + if (lenField[0]) { + for (j = 0, totalLenField = 0; j < layers; j++) { + totalLenField += +record.get(lenField[j]); + } + layerTotals[i] = totalLenField; + maxLenField = Math.max(maxLenField, totalLenField); + } + }, this); + + 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; + } + // First slice + if (!i || first == 0) { + angle = 360 - middleAngle; + me.firstAngle = angle; + middleAngle = angle - 360 * value / totalField / 2; + } + endAngle = angle - 360 * value / totalField; + slice = { + series: me, + value: value, + startAngle: angle, + endAngle: endAngle, + storeItem: record + }; + if (lenField[0]) { + lenValue = layerTotals[i]; + slice.rho = me.radius * (lenValue / maxLenField); + } else { + 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++) { + sprite = group.getAt(i * layers + j); + deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho; + //set pie slice properties + rendererAttributes = { + segment: { + startAngle: slice.startAngle, + endAngle: slice.endAngle, + margin: 0, + rho: slice.rho, + startRho: rhoAcum + (deltaRho * donut / 100), + endRho: rhoAcum + deltaRho + } + }; + //create shadows + for (shindex = 0, shadows = []; shindex < lnsh; shindex++) { + shadowAttr = shadowAttributes[shindex]; + shadow = shadowGroups[shindex].getAt(i); + if (!shadow) { + shadow = chart.surface.add(Ext.apply({}, + { + type: 'path', + group: shadowGroups[shindex], + strokeLinejoin: "round" + }, + rendererAttributes, shadowAttr)); + } + if (animate) { + rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply({}, + rendererAttributes, shadowAttr), i, store); + me.onAnimate(shadow, { + to: rendererAttributes + }); + } else { + rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, { + hidden: false + }), i, store); + shadow.setAttributes(rendererAttributes, true); + } + shadows.push(shadow); + } + slice.shadowAttrs[j] = shadows; + } + } + } + //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); + deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho; + //set pie slice properties + rendererAttributes = Ext.apply({ + segment: { + startAngle: slice.startAngle, + endAngle: slice.endAngle, + margin: 0, + rho: slice.rho, + startRho: rhoAcum + (deltaRho * donut / 100), + endRho: rhoAcum + deltaRho + } + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {})); + item = Ext.apply({}, + rendererAttributes.segment, { + slice: slice, + series: me, + storeItem: slice.storeItem, + index: i + }); + me.calcMiddle(item); + if (enableShadows) { + item.shadows = slice.shadowAttrs[j]; + } + items[i] = item; + // Create a new sprite if needed (no height) + if (!sprite) { + spriteOptions = Ext.apply({ + type: "path", + group: group, + middle: item.middle + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {})); + sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes)); + } + slice.sprite = slice.sprite || []; + item.sprite = sprite; + slice.sprite.push(sprite); + slice.point = [item.middle.x, item.middle.y]; + if (animate) { + rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store); + sprite._to = rendererAttributes; + sprite._animating = true; + me.onAnimate(sprite, { + to: rendererAttributes, + listeners: { + afteranimate: { + fn: function() { + this._animating = false; + }, + scope: sprite + } + } + }); + } else { + rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, { + hidden: false + }), i, store); + sprite.setAttributes(rendererAttributes, true); + } + rhoAcum += deltaRho; + } + } + + // Hide unused bars + ln = group.getCount(); + for (i = 0; i < ln; i++) { + if (!slices[(i / layers) >> 0] && group.getAt(i)) { + group.getAt(i).hide(true); + } + } + if (enableShadows) { + lnsh = shadowGroups.length; + for (shindex = 0; shindex < ln; shindex++) { + if (!slices[(shindex / layers) >> 0]) { + for (j = 0; j < lnsh; j++) { + if (shadowGroups[j].getAt(shindex)) { + shadowGroups[j].getAt(shindex).hide(true); + } + } + } + } + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private callback for when creating a label sprite. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {}); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': middle.x, + 'y': middle.y + }, endLabelStyle)); + }, + + // @private callback for when placing a label sprite. + onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = [].concat(config.field), + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + opt = { + x: middle.x, + y: middle.y + }, + x = middle.x - centerX, + y = middle.y - centerY, + from = {}, + rho = 1, + theta = Math.atan2(y, x || 1), + dg = theta * 180 / Math.PI, + prevDg; + + function fixAngle(a) { + if (a < 0) a += 360; + return a % 360; + } + + label.setAttributes({ + text: format(storeItem.get(field[index])) + }, true); + + switch (display) { + case 'outside': + rho = Math.sqrt(x * x + y * y) * 2; + //update positions + opt.x = rho * Math.cos(theta) + centerX; + opt.y = rho * Math.sin(theta) + centerY; + break; + + case 'rotate': + dg = fixAngle(dg); + dg = (dg > 90 && dg < 270) ? dg + 180: dg; + + prevDg = label.attr.rotation.degrees; + if (prevDg != null && Math.abs(prevDg - dg) > 180) { + if (dg > prevDg) { + dg -= 360; + } else { + dg += 360; + } + dg = dg % 360; + } else { + dg = fixAngle(dg); + } + //update rotation angle + opt.rotate = { + degrees: dg, + x: opt.x, + y: opt.y + }; + break; + + default: + break; + } + //ensure the object has zero translation + opt.translate = { + x: 0, y: 0 + }; + if (animate && !resizing && (display != 'rotate' || prevDg != null)) { + me.onAnimate(label, { + to: opt + }); + } else { + label.setAttributes(opt, true); + } + label._from = from; + }, + + // @private callback for when placing a callout sprite. + onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.callouts, + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + opt = { + x: middle.x, + y: middle.y + }, + x = middle.x - centerX, + y = middle.y - centerY, + rho = 1, + rhoCenter, + theta = Math.atan2(y, x || 1), + bbox = callout.label.getBBox(), + offsetFromViz = 20, + offsetToSide = 10, + offsetBox = 10, + p; + + //should be able to config this. + rho = item.endRho + offsetFromViz; + rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3; + //update positions + opt.x = rho * Math.cos(theta) + centerX; + opt.y = rho * Math.sin(theta) + centerY; + + x = rhoCenter * Math.cos(theta); + y = rhoCenter * Math.sin(theta); + + if (chart.animate) { + //set the line from the middle of the pie to the box. + me.onAnimate(callout.lines, { + to: { + path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"] + } + }); + //set box position + me.onAnimate(callout.box, { + to: { + x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)), + y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)), + width: bbox.width + 2 * offsetBox, + height: bbox.height + 2 * offsetBox + } + }); + //set text position + me.onAnimate(callout.label, { + to: { + x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), + y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4) + } + }); + } else { + //set the line from the middle of the pie to the box. + callout.lines.setAttributes({ + path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"] + }, + true); + //set box position + callout.box.setAttributes({ + x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)), + y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)), + width: bbox.width + 2 * offsetBox, + height: bbox.height + 2 * offsetBox + }, + true); + //set text position + callout.label.setAttributes({ + x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), + y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4) + }, + true); + } + for (p in callout) { + callout[p].show(true); + } + }, + + // @private handles sprite animation for the series. + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + isItemInPoint: function(x, y, item, i) { + var me = this, + cx = me.centerX, + cy = me.centerY, + abs = Math.abs, + dx = abs(x - cx), + dy = abs(y - cy), + startAngle = item.startAngle, + endAngle = item.endAngle, + rho = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(y - cy, x - cx) / me.rad + 360; + + // normalize to the same range of angles created by drawSeries + if (angle > me.firstAngle) { + angle -= 360; + } + 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; + if (!isNaN(this._index)) { + this.__excludes = this.__excludes || []; + this.__excludes[this._index] = true; + sprite = this.slices[this._index].sprite; + for (sh = 0, lsh = sprite.length; sh < lsh; sh++) { + sprite[sh].setAttributes({ + hidden: true + }, true); + } + if (this.slices[this._index].shadowAttrs) { + for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) { + shadow = shadows[i]; + for (sh = 0, lsh = shadow.length; sh < lsh; sh++) { + shadow[sh].setAttributes({ + hidden: true + }, true); + } + } + } + this.drawSeries(); + } + }, + + // @private shows all elements in the series. + showAll: function() { + if (!isNaN(this._index)) { + this.__excludes[this._index] = false; + this.drawSeries(); + } + }, + + /** + * Highlight the specified item. If no item is provided the whole series will be highlighted. + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + highlightItem: function(item) { + 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; + } + me.callParent([item]); + if (!me.highlight) { + return; + } + if ('segment' in me.highlightCfg) { + var highlightSegment = me.highlightCfg.segment, + animate = me.chart.animate, + attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop; + //animate labels + if (me.labelsGroup) { + var group = me.labelsGroup, + display = me.label.display, + label = group.getAt(item.index), + middle = (item.startAngle + item.endAngle) / 2 * rad, + r = highlightSegment.margin || 0, + x = r * Math.cos(middle), + y = r * Math.sin(middle); + + //TODO(nico): rounding to 1e-10 + //gives the right translation. Translation + //was buggy for very small numbers. In this + //case we're not looking to translate to very small + //numbers but not to translate at all. + if (Math.abs(x) < 1e-10) { + x = 0; + } + if (Math.abs(y) < 1e-10) { + y = 0; + } + + if (animate) { + label.stopAnimation(); + label.animate({ + to: { + translate: { + x: x, + y: y + } + }, + duration: me.highlightDuration + }); + } + else { + label.setAttributes({ + translate: { + x: x, + y: y + } + }, true); + } + } + //animate shadows + if (me.chart.shadow && item.shadows) { + i = 0; + shadows = item.shadows; + ln = shadows.length; + for (; i < ln; i++) { + shadow = shadows[i]; + to = {}; + itemHighlightSegment = item.sprite._from.segment; + for (prop in itemHighlightSegment) { + if (! (prop in highlightSegment)) { + to[prop] = itemHighlightSegment[prop]; + } + } + attrs = { + segment: Ext.applyIf(to, me.highlightCfg.segment) + }; + if (animate) { + shadow.stopAnimation(); + shadow.animate({ + to: attrs, + duration: me.highlightDuration + }); + } + else { + shadow.setAttributes(attrs, true); + } + } + } + } + }, + + /** + * 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() { + var me = this; + if (!me.highlight) { + return; + } + + if (('segment' in me.highlightCfg) && me.items) { + var items = me.items, + animate = me.chart.animate, + shadowsEnabled = !!me.chart.shadow, + group = me.labelsGroup, + len = items.length, + i = 0, + j = 0, + display = me.label.display, + shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs; + + for (; i < len; i++) { + item = items[i]; + if (!item) { + continue; + } + sprite = item.sprite; + if (sprite && sprite._highlighted) { + //animate labels + if (group) { + label = group.getAt(item.index); + attrs = Ext.apply({ + translate: { + x: 0, + y: 0 + } + }, + display == 'rotate' ? { + rotate: { + x: label.attr.x, + y: label.attr.y, + degrees: label.attr.rotation.degrees + } + }: {}); + if (animate) { + label.stopAnimation(); + label.animate({ + to: attrs, + duration: me.highlightDuration + }); + } + else { + label.setAttributes(attrs, true); + } + } + if (shadowsEnabled) { + shadows = item.shadows; + shadowLen = shadows.length; + for (; j < shadowLen; j++) { + to = {}; + ihs = item.sprite._to.segment; + hs = item.sprite._from.segment; + Ext.apply(to, hs); + for (p in ihs) { + if (! (p in hs)) { + to[p] = ihs[p]; + } + } + shadow = shadows[j]; + if (animate) { + shadow.stopAnimation(); + shadow.animate({ + to: { + segment: to + }, + duration: me.highlightDuration + }); + } + else { + shadow.setAttributes({ segment: to }, true); + } + } + } + } + } + } + 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 + */ + getLegendColor: function(index) { + var me = this; + return me.colorArrayStyle[index % me.colorArrayStyle.length]; + } +}); +