/**
* @abstract
* @extends Ext.chart.series.Cartesian
* Abstract class for all the stacked cartesian series including area series
* and bar series.
*/
Ext.define('Ext.chart.series.StackedCartesian', {
extend: 'Ext.chart.series.Cartesian',
config: {
/**
* @cfg {Boolean} [stacked=true]
* `true` to display the series in its stacked configuration.
*/
stacked: true,
/**
* @cfg {Boolean} [splitStacks=true]
* `true` to stack negative/positive values in respective y-axis directions.
*/
splitStacks: true,
/**
* @cfg {Boolean} [fullStack=false]
* If `true`, the height of a stacked bar is always the full height of the chart,
* with individual components viewed as shares of the whole determined by the
* {@link #fullStackTotal} config.
*/
fullStack: false,
/**
* @cfg {Boolean} [fullStackTotal=100]
* If the {@link #fullStack} config is set to `true`, this will determine
* the absolute total value of each stack.
*/
fullStackTotal: 100,
/**
* @cfg {Array} hidden
*/
hidden: []
},
/**
* @private
* @property
* If `true`, each subsequent sprite has a lower zIndex so that the stroke of previous
* sprite in the stack is not covered by the next sprite (which makes the very top
* segment look odd in flat bar and area series, especially when wide strokes are used).
*/
reversedSpriteZOrder: true,
spriteAnimationCount: 0,
themeColorCount: function() {
var me = this,
yField = me.getYField();
return Ext.isArray(yField) ? yField.length : 1;
},
updateStacked: function() {
this.processData();
},
updateSplitStacks: function() {
this.processData();
},
coordinateY: function() {
return this.coordinateStacked('Y', 1, 2);
},
coordinateStacked: function(direction, directionOffset, directionCount) {
var me = this,
store = me.getStore(),
items = store.getData().items,
itemCount = items.length,
axis = me['get' + direction + 'Axis'](),
hidden = me.getHidden(),
splitStacks = me.getSplitStacks(),
fullStack = me.getFullStack(),
fullStackTotal = me.getFullStackTotal(),
range = [0, 0],
directions = me['fieldCategory' + direction],
dataStart = [],
posDataStart = [],
negDataStart = [],
dataEnd,
stacked = me.getStacked(),
sprites = me.getSprites(),
coordinatedData = [],
i, j, k, fields, fieldCount,
posTotals, negTotals,
fieldCategoriesItem,
data, attr;
if (!sprites.length) {
return;
}
for (i = 0; i < directions.length; i++) {
fieldCategoriesItem = directions[i];
fields = me.getFields([fieldCategoriesItem]);
fieldCount = fields.length;
for (j = 0; j < itemCount; j++) {
dataStart[j] = 0;
posDataStart[j] = 0;
negDataStart[j] = 0;
}
for (j = 0; j < fieldCount; j++) {
if (!hidden[j]) {
coordinatedData[j] = me.coordinateData(items, fields[j], axis);
}
}
if (stacked && fullStack) {
posTotals = [];
if (splitStacks) {
negTotals = [];
}
for (j = 0; j < itemCount; j++) {
posTotals[j] = 0;
if (splitStacks) {
negTotals[j] = 0;
}
for (k = 0; k < fieldCount; k++) {
data = coordinatedData[k];
if (!data) {
// If the field is hidden there's no coordinated data for it.
continue;
}
data = data[j];
if (data >= 0 || !splitStacks) {
posTotals[j] += data;
}
else if (data < 0) {
negTotals[j] += data;
} // else not a valid number
}
}
}
for (j = 0; j < fieldCount; j++) {
attr = {};
if (hidden[j]) {
attr['dataStart' + fieldCategoriesItem] = dataStart;
attr['data' + fieldCategoriesItem] = dataStart;
sprites[j].setAttributes(attr);
continue;
}
data = coordinatedData[j];
if (stacked) {
dataEnd = [];
for (k = 0; k < itemCount; k++) {
if (!data[k]) {
data[k] = 0;
}
if (data[k] >= 0 || !splitStacks) {
if (fullStack && posTotals[k]) {
data[k] *= fullStackTotal / posTotals[k];
}
dataStart[k] = posDataStart[k];
posDataStart[k] += data[k];
dataEnd[k] = posDataStart[k];
}
else {
if (fullStack && negTotals[k]) {
data[k] *= fullStackTotal / negTotals[k];
}
dataStart[k] = negDataStart[k];
negDataStart[k] += data[k];
dataEnd[k] = negDataStart[k];
}
}
attr['dataStart' + fieldCategoriesItem] = dataStart;
attr['data' + fieldCategoriesItem] = dataEnd;
Ext.chart.Util.expandRange(range, dataStart);
Ext.chart.Util.expandRange(range, dataEnd);
}
else {
attr['dataStart' + fieldCategoriesItem] = dataStart;
attr['data' + fieldCategoriesItem] = data;
Ext.chart.Util.expandRange(range, data);
}
sprites[j].setAttributes(attr);
}
}
range = Ext.chart.Util.validateRange(range, me.defaultRange);
me.dataRange[directionOffset] = range[0];
me.dataRange[directionOffset + directionCount] = range[1];
attr = {};
attr['dataMin' + direction] = range[0];
attr['dataMax' + direction] = range[1];
for (i = 0; i < sprites.length; i++) {
sprites[i].setAttributes(attr);
}
},
getFields: function(fieldCategory) {
var me = this,
fields = [],
ln = fieldCategory.length,
i, fieldsItem;
for (i = 0; i < ln; i++) {
fieldsItem = me['get' + fieldCategory[i] + 'Field']();
if (Ext.isArray(fieldsItem)) {
fields.push.apply(fields, fieldsItem);
}
else {
fields.push(fieldsItem);
}
}
return fields;
},
updateLabelOverflowPadding: function(labelOverflowPadding) {
var me = this,
label;
if (!me.isConfiguring) {
label = me.getLabel();
if (label) {
label.setAttributes({ labelOverflowPadding: labelOverflowPadding });
}
}
},
updateLabelData: function() {
var me = this,
label = me.getLabel();
if (label) {
label.setAttributes({ labelOverflowPadding: me.getLabelOverflowPadding() });
}
me.callParent();
},
getSprites: function() {
var me = this,
chart = me.getChart(),
fields = me.getFields(me.fieldCategoryY),
itemInstancing = me.getItemInstancing(),
sprites = me.sprites,
hidden = me.getHidden(),
spritesCreated = false,
fieldCount = fields.length,
i, sprite;
if (!chart) {
return [];
}
// Create one Ext.chart.series.sprite.StackedCartesian sprite per field.
for (i = 0; i < fieldCount; i++) {
sprite = sprites[i];
if (!sprite) {
sprite = me.createSprite();
sprite.setAttributes({
zIndex: (me.reversedSpriteZOrder ? -1 : 1) * i
});
sprite.setField(fields[i]);
spritesCreated = true;
hidden.push(false);
if (itemInstancing) {
sprite.getMarker('items').getTemplate().setAttributes(me.getStyleByIndex(i));
}
else {
sprite.setAttributes(me.getStyleByIndex(i));
}
}
}
if (spritesCreated) {
me.updateHidden(hidden);
}
return sprites;
},
getItemForPoint: function(x, y) {
var me = this,
sprites = me.getSprites(),
store = me.getStore(),
hidden = me.getHidden(),
minDistance = Infinity,
item = null,
spriteIndex = -1,
pointIndex = -1,
point,
yField,
sprite,
i, ln;
for (i = 0, ln = sprites.length; i < ln; i++) {
if (hidden[i]) {
continue;
}
sprite = sprites[i];
point = sprite.getNearestDataPoint(x, y);
// Don't stop when the first matching point is found.
// Keep looking for the nearest point.
if (point) {
if (point.distance < minDistance) {
minDistance = point.distance;
pointIndex = point.index;
spriteIndex = i;
}
}
}
if (spriteIndex > -1) {
yField = me.getYField();
item = {
series: me,
sprite: sprites[spriteIndex],
category: me.getItemInstancing() ? 'items' : 'markers',
index: pointIndex,
record: store.getData().items[pointIndex],
// Handle the case where we're stacked but a single segment
field: typeof yField === 'string' ? yField : yField[spriteIndex],
distance: minDistance
};
}
return item;
},
provideLegendInfo: function(target) {
var me = this,
sprites = me.getSprites(),
title = me.getTitle(),
field = me.getYField(),
hidden = me.getHidden(),
single = sprites.length === 1,
style, fill,
i, name;
for (i = 0; i < sprites.length; i++) {
style = me.getStyleByIndex(i);
fill = style.fillStyle;
if (title) {
if (Ext.isArray(title)) {
name = title[i];
}
else if (single) {
name = title;
}
}
if (!title || !name) {
if (Ext.isArray(field)) {
name = field[i];
}
else {
name = me.getId();
}
}
target.push({
name: name,
mark: (Ext.isObject(fill) ? fill.stops && fill.stops[0].color : fill) ||
style.strokeStyle || 'black',
disabled: hidden[i],
series: me.getId(),
index: i
});
}
},
onSpriteAnimationStart: function(sprite) {
this.spriteAnimationCount++;
if (this.spriteAnimationCount === 1) {
this.fireEvent('animationstart');
}
},
onSpriteAnimationEnd: function(sprite) {
this.spriteAnimationCount--;
if (this.spriteAnimationCount === 0) {
this.fireEvent('animationend');
}
}
});