/**
* @class Ext.chart.Chart
* @extends Ext.draw.Component
*
* The Ext.chart package provides the capability to visualize data.
* Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart.
* A chart configuration object has some overall styling options as well as an array of axes
* and series. A chart instance example could look like:
*
var chart = new Ext.chart.Chart({
renderTo: Ext.getBody(),
width: 800,
height: 600,
animate: true,
store: store1,
renderTo: Ext.getBody(),
shadow: true,
theme: 'Category1',
legend: {
position: 'right'
},
axes: [ ...some axes options... ],
series: [ ...some series options... ]
});
*
* In this example we set the `width` and `height` of the chart, we decide whether our series are
* animated or not and we select a store to be bound to the chart. We also turn on shadows for all series,
* select a color theme `Category1` for coloring the series, set the legend to the right part of the chart and
* then tell the chart to render itself in the body element of the document. For more information about the axes and
* series configurations please check the documentation of each series (Line, Bar, Pie, etc).
*
* @xtype chart
*/
Ext.define('Ext.chart.Chart', {
/* Begin Definitions */
alias: 'widget.chart',
extend: 'Ext.draw.Component',
mixins: {
themeMgr: 'Ext.chart.theme.Theme'
},
requires: [
'Ext.util.MixedCollection',
'Ext.data.StoreMgr',
'Ext.chart.Legend',
'Ext.util.DelayedTask'
],
/* End Definitions */
// @private
viewBox: false,
/**
* @cfg {Boolean/Object} animate (optional) true for the default animation (easing: 'ease' and duration: 500)
* or a standard animation config object to be used for default chart animations. Defaults to false.
*/
animate: false,
/**
* @cfg {Boolean/Object} legend (optional) true for the default legend display or a legend config object. Defaults to false.
*/
legend: false,
/**
* @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10.
*/
insetPadding: 10,
/**
* @cfg {Array} implOrder
* Defines the priority order for which Surface implementation to use. The first
* one supported by the current environment will be used.
*/
implOrder: ['SVG', 'VML', 'Canvas'],
/**
* @cfg {string} background (optional) Set the chart background. This can be a gradient name, image, or color.
* Defaults to false for no background.
*/
background: false,
constructor: function(config) {
var me = this,
defaultAnim;
me.initTheme(config.theme || me.theme);
if (me.gradients) {
Ext.apply(config, { gradients: me.gradients });
}
if (me.background) {
Ext.apply(config, { background: me.background });
}
if (config.animate) {
defaultAnim = {
easing: 'ease',
duration: 500
};
if (Ext.isObject(config.animate)) {
config.animate = Ext.applyIf(config.animate, defaultAnim);
}
else {
config.animate = defaultAnim;
}
}
this.callParent([config]);
},
initComponent: function() {
var me = this,
axes,
series;
me.callParent();
me.addEvents(
'itemmousedown',
'itemmouseup',
'itemmouseover',
'itemmouseout',
'itemclick',
'itemdoubleclick',
'itemdragstart',
'itemdrag',
'itemdragend',
/**
* @event beforerefresh
* Fires before a refresh to the chart data is called. If the beforerefresh handler returns
* false the {@link #refresh} action will be cancelled.
* @param {Chart} this
*/
'beforerefresh',
/**
* @event refresh
* Fires after the chart data has been refreshed.
* @param {Chart} this
*/
'refresh'
);
Ext.applyIf(me, {
zoom: {
x: 1,
y: 1
}
});
me.maxGutter = [0, 0];
me.store = Ext.data.StoreMgr.lookup(me.store);
axes = me.axes;
me.axes = new Ext.util.MixedCollection(false, function(a) { return a.position; });
if (axes) {
me.axes.addAll(axes);
}
series = me.series;
me.series = new Ext.util.MixedCollection(false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
if (series) {
me.series.addAll(series);
}
if (me.legend !== false) {
me.legend = new Ext.chart.Legend(Ext.applyIf({chart:me}, me.legend));
}
me.on({
mousemove: me.onMouseMove,
mouseleave: me.onMouseLeave,
mousedown: me.onMouseDown,
mouseup: me.onMouseUp,
scope: me
});
},
// @private overrides the component method to set the correct dimensions to the chart.
afterComponentLayout: function(width, height) {
var me = this;
if (Ext.isNumber(width) && Ext.isNumber(height)) {
me.curWidth = width;
me.curHeight = height;
me.redraw(true);
}
this.callParent(arguments);
// Relayout once for zoom
if (me.ownerCt && me.ownerCt.layout && !me.zoom.adj && (me.zoom.x !== 1 || me.zoom.y !== 1)) {
me.zoom.adj = true;
me.ownerCt.doLayout();
}
},
/**
* Redraw the chart. If animations are set this will animate the chart too.
* @cfg {boolean} resize Optional flag which changes the default origin points of the chart for animations.
*/
redraw: function(resize) {
var me = this,
chartBBox = me.chartBBox = {
x: 0,
y: 0,
height: (me.curHeight * me.zoom.y),
width: (me.curWidth * me.zoom.x)
},
legend = me.legend;
me.surface.setSize(chartBBox.width, chartBBox.height);
// Instantiate Series and Axes
me.series.each(me.initializeSeries, me);
me.axes.each(me.initializeAxis, me);
// Create legend if not already created
if (legend !== false) {
legend.create();
}
// Place axes properly, including influence from each other
me.alignAxes();
// Reposition legend based on new axis alignment
if (me.legend !== false) {
legend.updatePosition();
}
// Find the max gutter
me.getMaxGutter();
// Draw axes and series
me.resizing = !!resize;
me.axes.each(me.drawAxis, me);
me.series.each(me.drawCharts, me);
me.resizing = false;
},
// @private set the store after rendering the chart.
afterRender: function() {
var ref,
me = this;
Ext.chart.Chart.superclass.afterRender.call(me);
if (me.categoryNames) {
me.setCategoryNames(me.categoryNames);
}
if (me.tipRenderer) {
ref = me.getFunctionRef(me.tipRenderer);
me.setTipRenderer(ref.fn, ref.scope);
}
me.bindStore(me.store, true);
me.refresh();
},
// @private get x and y position of the mouse cursor.
getEventXY: function(e) {
var me = this,
box = this.surface.getRegion(),
pageXY = e.getXY(),
x = pageXY[0] - box.left,
y = pageXY[1] - box.top;
return [x, y];
},
// @private wrap the mouse down position to delegate the event to the series.
onMouseDown: function(e) {
var me = this,
item,
position;
// Ask each series if it has an item corresponding to (not necessarily exactly
// on top of) the current mouse coords. Fire mousedown event.
me.series.each(function(series) {
if (series.getItemForPoint) {
position = position || me.getEventXY(e);
item = series.getItemForPoint(position[0], position[1]);
if (item) {
me.fireEvent('itemmousedown', item);
}
}
}, me);
},
// @private wrap the mouse up event to delegate it to the series.
onMouseUp: function(e) {
var me = this,
item,
position;
// Ask each series if it has an item corresponding to (not necessarily exactly
// on top of) the current mouse coords. Fire mousedown event.
me.series.each(function(series) {
if (series.getItemForPoint) {
position = position || me.getEventXY(e);
item = series.getItemForPoint(position[0], position[1]);
if (item) {
me.fireEvent('itemmouseup', item);
}
}
}, me);
},
// @private wrap the mouse move event so it can be delegated to the series.
onMouseMove: function(e) {
var me = this,
item,
position,
last,
storeItem,
storeField;
// Ask each series if it has an item corresponding to (not necessarily exactly
// on top of) the current mouse coords. Fire itemmouseover/out events.
me.series.each(function(series) {
if (series.getItemForPoint) {
position = position || me.getEventXY(e);
item = series.getItemForPoint(position[0], position[1]);
last = series._lastItemForPoint;
storeItem = series._lastStoreItem;
storeField = series._lastStoreField;
if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) {
if (last) {
me.fireEvent('itemmouseout', last);
delete series._lastItemForPoint;
delete series._lastStoreField;
delete series._lastStoreItem;
}
if (item) {
me.fireEvent('itemmouseover', item);
series._lastItemForPoint = item;
series._lastStoreItem = item.storeItem;
series._lastStoreField = item.storeField;
}
}
}
}, me);
},
// @private handle mouse leave event.
onMouseLeave: function(e) {
this.series.each(function(series) {
delete series._lastItemForPoint;
});
},
// @private buffered refresh for when we update the store
delayRefresh: function() {
var me = this;
if (!me.refreshTask) {
me.refreshTask = new Ext.util.DelayedTask(me.refresh, me);
}
me.refreshTask.delay(me.refreshBuffer);
},
// @private
refresh: function() {
var me = this;
if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) {
if (me.fireEvent('beforerefresh', me) !== false) {
me.redraw();
me.fireEvent('refresh', me);
}
}
},
/**
* Changes the data store bound to this chart and refreshes it.
* @param {Store} store The store to bind to this chart
*/
bindStore: function(store, initial) {
var me = this;
if (!initial && me.store) {
if (store !== me.store && me.store.autoDestroy) {
me.store.destroy();
}
else {
me.store.un('datachanged', me.refresh, me);
me.store.un('add', me.delayRefresh, me);
me.store.un('remove', me.delayRefresh, me);
me.store.un('update', me.delayRefresh, me);
me.store.un('clear', me.refresh, me);
}
}
if (store) {
store = Ext.data.StoreMgr.lookup(store);
store.on({
scope: me,
datachanged: me.refresh,
add: me.delayRefresh,
remove: me.delayRefresh,
update: me.delayRefresh,
clear: me.refresh
});
}
me.store = store;
if (store && !initial) {
me.refresh();
}
},
// @private Create Axis
initializeAxis: function(axis) {
var me = this,
chartBBox = me.chartBBox,
w = chartBBox.width,
h = chartBBox.height,
x = chartBBox.x,
y = chartBBox.y,
themeAttrs = me.themeAttrs,
config = {
chart: me
};
if (themeAttrs) {
config.axisStyle = Ext.apply({}, themeAttrs.axis);
config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft);
config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight);
config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop);
config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom);
config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft);
config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight);
config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop);
config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom);
}
switch (axis.position) {
case 'top':
Ext.apply(config, {
length: w,
width: h,
x: x,
y: y
});
break;
case 'bottom':
Ext.apply(config, {
length: w,
width: h,
x: x,
y: h
});
break;
case 'left':
Ext.apply(config, {
length: h,
width: w,
x: x,
y: h
});
break;
case 'right':
Ext.apply(config, {
length: h,
width: w,
x: w,
y: h
});
break;
}
if (!axis.chart) {
Ext.apply(config, axis);
axis = me.axes.replace(Ext.create('Ext.chart.axis.' + axis.type, config));
}
else {
Ext.apply(axis, config);
}
axis.drawAxis(true);
},
/**
* @private Adjust the dimensions and positions of each axis and the chart body area after accounting
* for the space taken up on each side by the axes and legend.
*/
alignAxes: function() {
var me = this,
axes = me.axes,
legend = me.legend,
edges = ['top', 'right', 'bottom', 'left'],
chartBBox,
insetPadding = me.insetPadding,
insets = {
top: insetPadding,
right: insetPadding,
bottom: insetPadding,
left: insetPadding
};
function getAxis(edge) {
var i = axes.findIndex('position', edge);
return (i < 0) ? null : axes.getAt(i);
}
// Find the space needed by axes and legend as a positive inset from each edge
Ext.each(edges, function(edge) {
var isVertical = (edge === 'left' || edge === 'right'),
axis = getAxis(edge),
bbox;
// Add legend size if it's on this edge
if (legend !== false) {
if (legend.position === edge) {
bbox = legend.getBBox();
insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge];
}
}
// Add axis size if there's one on this edge
if (axis) {
bbox = axis.bbox;
insets[edge] += (isVertical ? bbox.width : bbox.height);
}
});
// Build the chart bbox based on the collected inset values
chartBBox = {
x: insets.left,
y: insets.top,
width: (me.curWidth * me.zoom.x) - insets.left - insets.right,
height: (me.curHeight * me.zoom.y) - insets.top - insets.bottom
};
me.chartBBox = chartBBox;
// Go back through each axis and set its length and position based on the
// corresponding edge of the chartBBox
axes.each(function(axis) {
var pos = axis.position,
isVertical = (pos === 'left' || pos === 'right');
axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x);
axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height);
axis.width = (isVertical ? chartBBox.width : chartBBox.height);
axis.length = (isVertical ? chartBBox.height : chartBBox.width);
});
},
// @private initialize the series.
initializeSeries: function(series, idx) {
var me = this,
themeAttrs = me.themeAttrs,
seriesObj, markerObj, seriesThemes, st,
markerThemes, colorArrayStyle = [],
i = 0, l,
config = {
chart: me,
seriesId: series.seriesId
};
if (themeAttrs) {
seriesThemes = themeAttrs.seriesThemes;
markerThemes = themeAttrs.markerThemes;
seriesObj = Ext.apply({}, themeAttrs.series);
markerObj = Ext.apply({}, themeAttrs.marker);
config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]);
config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
if (themeAttrs.colors) {
config.colorArrayStyle = themeAttrs.colors;
} else {
colorArrayStyle = [];
for (l = seriesThemes.length; i < l; i++) {
st = seriesThemes[i];
if (st.fill || st.stroke) {
colorArrayStyle.push(st.fill || st.stroke);
}
}
if (colorArrayStyle.length) {
config.colorArrayStyle = colorArrayStyle;
}
}
config.seriesIdx = idx;
}
if (!series.chart) {
Ext.applyIf(config, series);
series = me.series.replace(Ext.create('Ext.chart.series.' + Ext.String.capitalize(series.type), config));
} else {
Ext.apply(series, config);
}
},
// @private
getMaxGutter: function() {
var me = this,
maxGutter = [0, 0];
me.series.each(function(s) {
var gutter = s.getGutters && s.getGutters() || [0, 0];
maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
});
me.maxGutter = maxGutter;
},
// @private draw axis.
drawAxis: function(axis) {
axis.drawAxis();
},
// @private draw series.
drawCharts: function(series) {
series.drawSeries();
},
// @private remove gently.
destroy: function() {
this.surface.destroy();
this.callParent(arguments);
}
});