extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
481 lines (425 loc) • 15.4 kB
JavaScript
/**
* @class Ext.chart.series.Pie3D
* @extends Ext.chart.series.Polar
*
* Creates a 3D Pie Chart.
*
* **Note:** Labels, legends, and lines are not currently available when using the
* 3D Pie chart series.
*
* @example
* Ext.create({
* xtype: 'polar',
* renderTo: document.body,
* width: 600,
* height: 400,
* theme: 'green',
* interactions: 'rotate',
* store: {
* fields: ['data3'],
* data: [{
* 'data3': 14
* }, {
* 'data3': 16
* }, {
* 'data3': 14
* }, {
* 'data3': 6
* }, {
* 'data3': 36
* }]
* },
* series: {
* type: 'pie3d',
* angleField: 'data3',
* donut: 30
* }
* });
*/
Ext.define('Ext.chart.series.Pie3D', {
extend: 'Ext.chart.series.Polar',
requires: [
'Ext.chart.series.sprite.Pie3DPart',
'Ext.draw.PathUtil'
],
type: 'pie3d',
seriesType: 'pie3d',
alias: 'series.pie3d',
is3D: true,
config: {
rect: [0, 0, 0, 0],
thickness: 35,
distortion: 0.5,
/**
* @cfg {String} angleField (required)
* The store record field name to be used for the pie angles.
* The values bound to this field name must be positive real numbers.
*/
/**
* @private
* @cfg {String} radiusField
* Not supported.
*/
/**
* @cfg {Boolean/Number} donut
* Whether to set the pie chart as donut chart.
* Can be set to a particular percentage to set the radius
* of the donut chart.
*/
donut: false,
/**
* @cfg {Array} hidden Determines which pie slices are hidden.
*/
hidden: [], // Populated by the coordinateX method.
/**
* @cfg {Object} highlightCfg Default {@link #highlight} config for the 3D pie series.
* Slides highlighted pie sector outward.
*/
highlightCfg: {
margin: 20
},
/**
* @cfg {Number} [rotation=0] The starting angle of the pie slices.
*/
/**
* @private
* @cfg {Boolean/Object} [shadow=false]
*/
shadow: false
},
// Subtract 90 degrees from rotation, so that `rotation` config's default
// zero value makes first pie sector start at noon, rather than 3 o'clock.
rotationOffset: -Math.PI / 2,
setField: function (value) {
return this.setXField(value);
},
getField: function () {
return this.getXField();
},
updateRotation: function (rotation) {
this.setStyle({
baseRotation: rotation + this.rotationOffset
});
this.doUpdateStyles();
},
updateDistortion: function () {
this.setRadius();
},
updateThickness: function () {
this.setRadius();
},
updateColors: function (colors) {
this.setSubStyle({baseColor: colors});
},
applyShadow: function (shadow) {
if (shadow === true) {
shadow = {
shadowColor: 'rgba(0,0,0,0.8)',
shadowBlur: 30
};
} else if (!Ext.isObject(shadow)) {
shadow = {
shadowColor: Ext.util.Color.RGBA_NONE
};
}
return shadow;
},
updateShadow: function (shadow) {
var me = this,
sprites = me.getSprites(),
spritesPerSlice = me.spritesPerSlice,
ln = sprites && sprites.length,
i, sprite;
for (i = 1; i < ln; i += spritesPerSlice) {
sprite = sprites[i];
if (sprite.attr.part = 'bottom') {
sprite.setAttributes(shadow);
}
}
},
// This is a temporary solution until the Series.getStyleByIndex is fixed
// to give user styles the priority over theme ones. Also, for sprites of
// this particular series, the fillStyle shouldn't be set directly. Instead,
// the 'baseColor' attribute should be set, from which the stops of the
// gradient (used for fillStyle) will be calculated. Themes can't handle
// situations like that properly.
getStyleByIndex: function (i) {
var indexStyle = this.callParent([i]),
style = this.getStyle(),
// 'fill' and 'color' are 'fillStyle' aliases
// (see Ext.draw.sprite.Sprite.inheritableStatics.def.aliases)
fillStyle = indexStyle.fillStyle || indexStyle.fill || indexStyle.color,
strokeStyle = style.strokeStyle || style.stroke;
if (fillStyle) {
indexStyle.baseColor = fillStyle;
delete indexStyle.fillStyle;
delete indexStyle.fill;
delete indexStyle.color;
}
if (strokeStyle) {
indexStyle.strokeStyle = strokeStyle;
}
return indexStyle;
},
doUpdateStyles: function () {
var me = this,
sprites = me.getSprites(),
spritesPerSlice = me.spritesPerSlice,
ln = sprites && sprites.length,
i = 0, j = 0, k,
style;
for (; i < ln; i += spritesPerSlice, j++) {
style = me.getStyleByIndex(j);
for (k = 0; k < spritesPerSlice; k++) {
sprites[i + k].setAttributes(style);
}
}
},
coordinateX: function () {
var me = this,
chart = me.getChart(),
animation = chart && chart.getAnimation(),
store = me.getStore(),
records = store.getData().items,
recordCount = records.length,
xField = me.getXField(),
rotation = me.getRotation(),
hidden = me.getHidden(),
value, sum = 0, ratio,
summation = [],
sprites = me.getSprites(),
spriteCount = sprites.length,
spritesPerSlice = me.spritesPerSlice,
lastAngle = 0,
twoPi = Math.PI * 2,
// To avoid adjacent start/end part blinking (z-index jitter)
// when rotating a translucent pie chart.
delta = 1e-10,
i, j;
for (i = 0; i < recordCount; i++) {
value = Math.abs(Number(records[i].get(xField))) || 0;
if (!hidden[i]) {
sum += value;
}
summation[i] = sum;
if (i >= hidden.length) {
hidden[i] = false;
}
}
hidden.length = recordCount;
if (sum === 0) {
return;
}
ratio = 2 * Math.PI / sum;
for (i = 0; i < recordCount; i++) {
summation[i] *= ratio;
}
for (i = 0; i < spriteCount; i++) {
sprites[i].setAnimation(animation);
}
for (i = 0; i < recordCount; i++) {
for (j = 0; j < spritesPerSlice; j++) {
sprites[i * spritesPerSlice + j].setAttributes({
startAngle: lastAngle,
endAngle: summation[i] - delta,
globalAlpha: 1,
baseRotation: rotation
});
}
lastAngle = summation[i];
}
for (i *= spritesPerSlice; i < spriteCount; i++) {
sprites[i].setAnimation(animation);
sprites[i].setAttributes({
startAngle: twoPi,
endAngle: twoPi,
globalAlpha: 0,
baseRotation: rotation
});
}
},
updateLabelData: function () {
var me = this,
store = me.getStore(),
items = store.getData().items,
sprites = me.getSprites(),
labelField = me.getLabel().getTemplate().getField(),
hidden = me.getHidden(),
spritesPerSlice = me.spritesPerSlice,
i, j, ln, labels, sprite;
if (sprites.length && labelField) {
labels = [];
for (i = 0, ln = items.length; i < ln; i++) {
labels.push(items[i].get(labelField));
}
// Only set labels for the sprites that compose the top lid of the pie.
for (i = 0, j = 0, ln = sprites.length; i < ln; i += spritesPerSlice, j++) {
sprite = sprites[i];
sprite.setAttributes({label: labels[j]});
sprite.putMarker('labels', {hidden: hidden[j]}, sprite.attr.attributeId);
}
}
},
// The radius here will normally be set by the PolarChart.performLayout,
// where it's half the width or height (whichever is smaller) of the chart's rect.
// But for 3D pie series we have to take the thickness of the pie and the
// distortion into account to calculate the proper radius.
// The passed value is never used (or derived from) since the radius config
// is not really meant to be used directly, as it will be reset by the next layout.
applyRadius: function () {
var me = this,
chart = me.getChart(),
padding = chart.getInnerPadding(),
rect = chart.getMainRect() || [0, 0, 1, 1],
width = rect[2] - padding * 2,
height = rect[3] - padding * 2 - me.getThickness(),
horizontalRadius = width / 2,
verticalRadius = horizontalRadius * me.getDistortion();
if (verticalRadius > height / 2) {
return height / (me.getDistortion() * 2);
} else {
return horizontalRadius;
}
},
getSprites: function () {
var me = this,
store = me.getStore();
if (!store) {
return [];
}
var chart = me.getChart(),
surface = me.getSurface(),
records = store.getData().items,
spritesPerSlice = me.spritesPerSlice,
recordCount = records.length,
animation = me.getAnimation() || chart && chart.getAnimation(),
center = me.getCenter(),
offsetX = me.getOffsetX(),
offsetY = me.getOffsetY(),
radius = me.getRadius(),
rotation = me.getRotation(),
highlight = me.getHighlight(),
commonAttributes = {
centerX: center[0] + offsetX,
centerY: center[1] + offsetY - me.getThickness() / 2,
endRho: radius,
startRho: radius * me.getDonut() / 100,
thickness: me.getThickness(),
distortion: me.getDistortion()
},
sprites = me.sprites,
label = me.getLabel(),
labelTpl = label.getTemplate(),
sliceSprites, sliceAttributes, sprite,
i, j;
for (i = 0; i < recordCount; i++) {
sliceAttributes = Ext.apply({}, this.getStyleByIndex(i), commonAttributes);
if (!sprites[i * spritesPerSlice]) {
for (j = 0; j < me.partNames.length; j++) {
sprite = surface.add({
type: 'pie3dPart',
part: me.partNames[j]
});
if (j === 0 && labelTpl.getField()) {
// Make the 'top' part hold the label.
sprite.bindMarker('labels', label);
}
sprite.fx.setDurationOn('baseRotation', rotation);
if (highlight) {
sprite.config.highlight = highlight;
sprite.addModifier('highlight', true);
}
sprite.setAttributes(sliceAttributes);
sprites.push(sprite);
}
} else {
sliceSprites = sprites.slice(i * spritesPerSlice, (i + 1) * spritesPerSlice);
for (j = 0; j < sliceSprites.length; j++) {
sprite = sliceSprites[j];
if (animation) {
sprite.setAnimation(animation);
}
sprite.setAttributes(sliceAttributes);
}
}
}
return sprites;
},
betweenAngle: function (x, a, b) {
var pp = Math.PI * 2,
offset = this.rotationOffset;
a += offset;
b += offset;
x -= a;
b -= a;
// Normalize, so that both x and b are in the [0,360) interval.
// Since 360 * n angles will be normalized to 0,
// we need to treat b === 0 as a special case.
x %= pp;
b %= pp;
x += pp;
b += pp;
x %= pp;
b %= pp;
return x < b || b === 0;
},
getItemForPoint: function (x, y) {
var me = this,
sprites = me.getSprites();
if (sprites) {
var store = me.getStore(),
records = store.getData().items,
spritesPerSlice = me.spritesPerSlice,
hidden = me.getHidden(),
i, ln, sprite, topPartIndex;
for (i = 0, ln = records.length; i < ln; i++) {
if (!hidden[i]) {
topPartIndex = i * spritesPerSlice;
sprite = sprites[topPartIndex];
// This is CPU intensive on mousemove (no visial slowdown
// on a fast machine, but some throttling might be desirable
// on slower machines).
// On touch devices performance/battery hit is negligible.
if (sprite.hitTest([x, y])) {
return {
series: me,
sprite: sprites.slice(topPartIndex, topPartIndex + spritesPerSlice),
index: i,
record: records[i],
category: 'sprites',
field: me.getXField()
};
}
}
}
return null;
}
},
provideLegendInfo: function (target) {
var me = this,
store = me.getStore();
if (store) {
var items = store.getData().items,
labelField = me.getLabel().getTemplate().getField(),
field = me.getField(),
hidden = me.getHidden(),
i, style, color;
for (i = 0; i < items.length; i++) {
style = me.getStyleByIndex(i);
color = style.baseColor;
target.push({
name: labelField ? String(items[i].get(labelField)) : field + ' ' + i,
mark: color || 'black',
disabled: hidden[i],
series: me.getId(),
index: i
});
}
}
}
}, function () {
var proto = this.prototype,
definition = Ext.chart.series.sprite.Pie3DPart.def.getInitialConfig().processors.part;
proto.partNames = definition.replace(/^enums\(|\)/g, '').split(',');
proto.spritesPerSlice = proto.partNames.length;
});