/**
* @class Ext.chart.series.Pie
* @extends Ext.chart.series.Polar
*
* 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:
*
* @example
* Ext.create({
* xtype: 'polar',
* renderTo: document.body,
* width: 400,
* height: 400,
* theme: 'green',
* interactions: ['rotate', 'itemhighlight'],
* store: {
* fields: ['name', 'data1'],
* data: [{
* name: 'metric one',
* data1: 14
* }, {
* name: 'metric two',
* data1: 16
* }, {
* name: 'metric three',
* data1: 14
* }, {
* name: 'metric four',
* data1: 6
* }, {
* name: 'metric five',
* data1: 36
* }]
* },
* series: {
* type: 'pie',
* highlight: true,
* angleField: 'data1',
* label: {
* field: 'name',
* display: 'rotate'
* },
* donut: 30
* }
* });
*
* In this configuration we set `pie` as the type for the series, then set the `highlight` config
* to `true` (we can also specify an object with specific style properties for highlighting options)
* which is triggered when hovering or tapping elements.
* We set `data1` as the value of the `angleField` to determine the angular span for each pie slice.
* We also set a label configuration object where we set the name of the store field
* to be rendered as text for the label. The labels will also be displayed rotated.
* And finally, we specify the donut hole radius for the pie series in percentages of the series
* radius.
*
*/
Ext.define('Ext.chart.series.Pie', {
extend: 'Ext.chart.series.Polar',
requires: [
'Ext.chart.series.sprite.PieSlice'
],
type: 'pie',
alias: 'series.pie',
seriesType: 'pieslice',
isPie: true,
/**
* @cfg {Object} style Custom style configuration for the sprite used in the series.
* It overrides the style that is provided by the current theme. See
* {@link Ext.chart.theme.series.Pie}
*/
config: {
/**
* @cfg {String} radiusField
* 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.
*/
/**
* @cfg {Number} donut Specifies the radius of the donut hole, as a percentage
* of the chart's radius.
* Defaults to 0 (no donut hole).
*/
donut: 0,
/**
* @cfg {Number} rotation The starting angle of the pie slices.
*/
rotation: 0,
/**
* @cfg {Boolean} clockwise
* Whether the pie slices are displayed clockwise. Default's true.
*/
clockwise: true,
/**
* @cfg {Number} [totalAngle=2*PI] The total angle of the pie series.
*/
totalAngle: 2 * Math.PI,
/**
* @cfg {Array} hidden Determines which pie slices are hidden.
*/
hidden: [],
/**
* @cfg {Number} [radiusFactor=100] Allows adjustment of the radius
* by a specific percentage.
*/
radiusFactor: 100,
/**
* @cfg {Ext.chart.series.sprite.PieSlice/Object} highlightCfg
* Default highlight config for the pie series.
* Slides highlighted pie sector outward by default.
*
* highlightCfg accepts as its value a config object (or array of configs) for a
* {@link Ext.chart.series.sprite.PieSlice pie sprite}.
*
*
* Example config:
*
* Ext.create('Ext.chart.PolarChart', {
* renderTo: document.body,
* width: 600,
* height: 400,
* innerPadding: 5,
* store: {
* fields: ['name', 'data1'],
* data: [{
* name: 'metric one',
* data1: 10
* }, {
* name: 'metric two',
* data1: 7
* }, {
* name: 'metric three',
* data1: 5
* }]
* },
* series: {
* type: 'pie',
* label: {
* field: 'name',
* display: 'rotate'
* },
* xField: 'data1',
* donut: 30,
* highlightCfg: {
* margin: 10,
* fillOpacity: .7
* }
* }
* });
*/
highlightCfg: {
margin: 20
},
style: {}
},
directions: ['X'],
applyLabel: function(newLabel, oldLabel) {
if (Ext.isObject(newLabel) && !Ext.isString(newLabel.orientation)) {
// Override default label orientation from '' to 'vertical'.
Ext.apply(newLabel = Ext.Object.chain(newLabel), { orientation: 'vertical' });
}
return newLabel;
},
updateLabelData: function() {
var me = this,
store = me.getStore(),
items = store.getData().items,
sprites = me.getSprites(),
label = me.getLabel(),
labelField = label && label.getTemplate().getField(),
hidden = me.getHidden(),
i, ln, labels, sprite;
if (sprites.length && labelField) {
labels = [];
for (i = 0, ln = items.length; i < ln; i++) {
labels.push(items[i].get(labelField));
}
for (i = 0, ln = sprites.length; i < ln; i++) {
sprite = sprites[i];
sprite.setAttributes({ label: labels[i] });
sprite.putMarker('labels', { hidden: hidden[i] }, sprite.attr.attributeId);
}
}
},
coordinateX: function() {
var me = this,
store = me.getStore(),
records = store.getData().items,
recordCount = records.length,
xField = me.getXField(),
yField = me.getYField(),
x,
sumX = 0,
unit,
y,
maxY = 0,
hidden = me.getHidden(),
summation = [],
i,
lastAngle = 0,
totalAngle = me.getTotalAngle(),
clockwise = me.getClockwise() ? 1 : -1,
sprites = me.getSprites(),
sprite, labels;
if (!sprites) {
return;
}
for (i = 0; i < recordCount; i++) {
x = Math.abs(Number(records[i].get(xField))) || 0;
y = yField && Math.abs(Number(records[i].get(yField))) || 0;
if (!hidden[i]) {
sumX += x;
if (y > maxY) {
maxY = y;
}
}
summation[i] = sumX;
if (i >= hidden.length) {
hidden[i] = false;
}
}
hidden.length = recordCount;
me.maxY = maxY;
if (sumX !== 0) {
unit = totalAngle / sumX;
}
for (i = 0; i < recordCount; i++) {
sprite = sprites[i];
if (sprite) {
sprite.setAttributes({
startAngle: lastAngle,
endAngle: lastAngle = (unit ? clockwise * summation[i] * unit : 0),
globalAlpha: 1
});
}
}
if (recordCount < sprites.length) {
for (i = recordCount; i < sprites.length; i++) {
sprite = sprites[i];
labels = sprite.getMarker('labels');
if (labels) {
// Don't want the 'labels' Markers and its 'template' sprite to be destroyed
// with the PieSlice MarkerHolder, as it is also used by other pie slices.
// So we release 'labels' before destroying the PieSlice.
// But first, we have to clear the instances of the 'labels'
// Markers created by the PieSlice MarkerHolder.
labels.clear(sprite.getId());
sprite.releaseMarker('labels');
}
sprite.destroy();
}
sprites.length = recordCount;
}
for (i = recordCount; i < sprites.length; i++) {
sprites[i].setAttributes({
startAngle: totalAngle,
endAngle: totalAngle,
globalAlpha: 0
});
}
},
updateCenter: function(center) {
this.setStyle({
translationX: center[0] + this.getOffsetX(),
translationY: center[1] + this.getOffsetY()
});
this.doUpdateStyles();
},
updateRadius: function(radius) {
this.setStyle({
startRho: radius * this.getDonut() * 0.01,
endRho: radius * this.getRadiusFactor() * 0.01
});
this.doUpdateStyles();
},
getStyleByIndex: function(i) {
var me = this,
store = me.getStore(),
item = store.getAt(i),
yField = me.getYField(),
radius = me.getRadius(),
style = {},
startRho, endRho, y;
if (item) {
y = yField && Math.abs(Number(item.get(yField))) || 0;
startRho = radius * me.getDonut() * 0.01;
endRho = radius * me.getRadiusFactor() * 0.01;
style = me.callParent([i]);
style.startRho = startRho;
style.endRho = me.maxY ? (startRho + (endRho - startRho) * y / me.maxY) : endRho;
}
return style;
},
updateDonut: function(donut) {
var radius = this.getRadius();
this.setStyle({
startRho: radius * donut * 0.01,
endRho: radius * this.getRadiusFactor() * 0.01
});
this.doUpdateStyles();
},
// Subtract 90 degrees from rotation, so that `rotation` config's default
// value of 0 makes first pie sector start at noon, rather than 3 o'clock.
rotationOffset: -Math.PI / 2,
updateRotation: function(rotation) {
this.setStyle({
rotationRads: rotation + this.rotationOffset
});
this.doUpdateStyles();
},
updateTotalAngle: function(totalAngle) {
this.processData();
},
getSprites: function() {
var me = this,
chart = me.getChart(),
xField = me.getXField(),
yField = me.getYField(),
store = me.getStore();
if (!chart || !store) {
return Ext.emptyArray;
}
me.getColors();
me.getSubStyle();
// eslint-disable-next-line vars-on-top, one-var
var items = store.getData().items,
length = items.length,
animation = me.getAnimation() || chart && chart.getAnimation(),
sprites = me.sprites,
sprite,
spriteCreated = false,
spriteIndex = 0,
label = me.getLabel(),
labelTpl = label && label.getTemplate(),
i, rendererData;
rendererData = {
store: store,
field: xField, // for backward compatibility only (deprecated in 5.5)
angleField: xField,
radiusField: yField,
series: me
};
for (i = 0; i < length; i++) {
sprite = sprites[i];
if (!sprite && items[i].get(xField)) {
sprite = me.createSprite();
if (me.getHighlight()) {
sprite.config.highlight = me.getHighlight();
sprite.addModifier('highlight', true);
}
if (labelTpl && labelTpl.getField()) {
labelTpl.setAttributes({
labelOverflowPadding: me.getLabelOverflowPadding()
});
labelTpl.getAnimation().setCustomDurations({ 'callout': 200 });
}
sprite.setAttributes(me.getStyleByIndex(i));
sprite.setRendererData(rendererData);
spriteCreated = true;
}
if (sprite) {
sprite.setRendererIndex(spriteIndex++);
sprite.setAnimation(animation);
}
}
if (spriteCreated) {
me.doUpdateStyles();
}
return me.sprites;
},
betweenAngle: function(x, a, b) {
var pp = Math.PI * 2,
offset = this.rotationOffset;
if (a === b) {
return false;
}
if (!this.getClockwise()) {
x *= -1;
a *= -1;
b *= -1;
a -= offset;
b -= offset;
}
else {
a += offset;
b += offset;
}
x -= a;
b -= a;
// Normalize, so that both x and b are in the [0,360) interval.
x %= pp;
b %= pp;
x += pp;
b += pp;
x %= pp;
b %= pp;
// Because 360 * n angles will be normalized to 0,
// we need to treat b ~= 0 as a special case.
return x < b || Ext.Number.isEqual(b, 0, 1e-8);
},
getItemByIndex: function(index, category) {
category = category || 'sprites';
return this.callParent([index, category]);
},
/**
* Returns the pie slice for a given angle
* @param {Number} angle The angle to search for the slice
* @return {Object} An object containing the reocord, sprite, scope etc.
*/
getItemForAngle: function(angle) {
var me = this,
sprites = me.getSprites(),
attr, store, items, hidden, i, ln;
angle %= Math.PI * 2;
while (angle < 0) {
angle += Math.PI * 2;
}
if (sprites) {
store = me.getStore();
items = store.getData().items;
hidden = me.getHidden();
for (i = 0, ln = store.getCount(); i < ln; i++) {
if (!hidden[i]) {
// Fortunately, item's id equals its index in the instances list.
attr = sprites[i].attr;
if (attr.startAngle <= angle && attr.endAngle >= angle) {
return {
series: me,
sprite: sprites[i],
index: i,
record: items[i],
field: me.getXField()
};
}
}
}
}
return null;
},
getItemForPoint: function(x, y) {
var me = this,
sprites = me.getSprites(),
center = me.getCenter(),
offsetX = me.getOffsetX(),
offsetY = me.getOffsetY(),
// Distance from the center of the series to the cursor.
dx = x - center[0] + offsetX,
dy = y - center[1] + offsetY,
store = me.getStore(),
donut = me.getDonut(),
records = store.getData().items,
direction = Math.atan2(dy, dx) - me.getRotation(),
radius = Math.sqrt(dx * dx + dy * dy),
startRadius = me.getRadius() * donut * 0.01,
hidden = me.getHidden(),
result = null,
i, ln, attr, sprite;
for (i = 0, ln = records.length; i < ln; i++) {
if (hidden[i]) {
continue;
}
sprite = sprites[i];
if (!sprite) {
break;
}
attr = sprite.attr;
if (radius >= startRadius + attr.margin &&
radius <= attr.endRho + attr.margin &&
me.betweenAngle(direction, attr.startAngle, attr.endAngle)) {
result = {
series: me,
sprite: sprites[i],
index: i,
record: records[i],
field: me.getXField()
};
break;
}
}
return result;
},
provideLegendInfo: function(target) {
var me = this,
store = me.getStore(),
items, labelField, xField, hidden, i, style, fill;
if (store) {
items = store.getData().items;
labelField = me.getLabel().getTemplate().getField();
xField = me.getXField();
hidden = me.getHidden();
for (i = 0; i < items.length; i++) {
style = me.getStyleByIndex(i);
fill = style.fillStyle;
if (Ext.isObject(fill)) {
fill = fill.stops && fill.stops[0].color;
}
target.push({
name: labelField ? String(items[i].get(labelField)) : xField + ' ' + i,
mark: fill || style.strokeStyle || 'black',
disabled: hidden[i],
series: me.getId(),
index: i
});
}
}
}
});