3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.chart.Chart
17 * @extends Ext.draw.Component
19 * The Ext.chart package provides the capability to visualize data.
20 * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart.
21 * A chart configuration object has some overall styling options as well as an array of axes
22 * and series. A chart instance example could look like:
25 Ext.create('Ext.chart.Chart', {
26 renderTo: Ext.getBody(),
36 axes: [ ...some axes options... ],
37 series: [ ...some series options... ]
41 * In this example we set the `width` and `height` of the chart, we decide whether our series are
42 * animated or not and we select a store to be bound to the chart. We also turn on shadows for all series,
43 * select a color theme `Category1` for coloring the series, set the legend to the right part of the chart and
44 * then tell the chart to render itself in the body element of the document. For more information about the axes and
45 * series configurations please check the documentation of each series (Line, Bar, Pie, etc).
47 Ext.define('Ext.chart.Chart', {
49 /* Begin Definitions */
51 alias: 'widget.chart',
53 extend: 'Ext.draw.Component',
56 themeManager: 'Ext.chart.theme.Theme',
57 mask: 'Ext.chart.Mask',
58 navigation: 'Ext.chart.Navigation'
62 'Ext.util.MixedCollection',
63 'Ext.data.StoreManager',
65 'Ext.util.DelayedTask'
74 * @cfg {String} theme (optional) The name of the theme to be used. A theme defines the colors and
75 * other visual displays of tick marks on axis, text, title text, line colors, marker colors and styles, etc.
76 * Possible theme values are 'Base', 'Green', 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes
77 * 'Category1' to 'Category6'. Default value is 'Base'.
81 * @cfg {Boolean/Object} animate (optional) true for the default animation (easing: 'ease' and duration: 500)
82 * or a standard animation config object to be used for default chart animations. Defaults to false.
87 * @cfg {Boolean/Object} legend (optional) true for the default legend display or a legend config object. Defaults to false.
92 * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10.
97 * @cfg {Array} enginePriority
98 * Defines the priority order for which Surface implementation to use. The first
99 * one supported by the current environment will be used.
101 enginePriority: ['Svg', 'Vml'],
104 * @cfg {Object|Boolean} background (optional) Set the chart background. This can be a gradient object, image, or color.
105 * Defaults to false for no background.
107 * For example, if `background` were to be a color we could set the object as
116 You can specify an image by using:
120 image: 'http://path.to.image/'
124 Also you can specify a gradient by using the gradient object syntax:
146 * @cfg {Array} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites.
147 * The gradients array is an array of objects with the following properties:
150 * <li><strong>id</strong> - string - The unique name of the gradient.</li>
151 * <li><strong>angle</strong> - number, optional - The angle of the gradient in degrees.</li>
152 * <li><strong>stops</strong> - object - An object with numbers as keys (from 0 to 100) and style objects
188 Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
191 sprite.setAttributes({
192 fill: 'url(#gradientId)'
199 constructor: function(config) {
202 me.initTheme(config.theme || me.theme);
204 Ext.apply(config, { gradients: me.gradients });
207 Ext.apply(config, { background: me.background });
209 if (config.animate) {
214 if (Ext.isObject(config.animate)) {
215 config.animate = Ext.applyIf(config.animate, defaultAnim);
218 config.animate = defaultAnim;
221 me.mixins.mask.constructor.call(me, config);
222 me.mixins.navigation.constructor.call(me, config);
223 me.callParent([config]);
226 initComponent: function() {
242 * @event beforerefresh
243 * Fires before a refresh to the chart data is called. If the beforerefresh handler returns
244 * <tt>false</tt> the {@link #refresh} action will be cancelled.
245 * @param {Chart} this
250 * Fires after the chart data has been refreshed.
251 * @param {Chart} this
263 me.maxGutter = [0, 0];
264 me.store = Ext.data.StoreManager.lookup(me.store);
266 me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
268 me.axes.addAll(axes);
271 me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
273 me.series.addAll(series);
275 if (me.legend !== false) {
276 me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
280 mousemove: me.onMouseMove,
281 mouseleave: me.onMouseLeave,
282 mousedown: me.onMouseDown,
283 mouseup: me.onMouseUp,
288 // @private overrides the component method to set the correct dimensions to the chart.
289 afterComponentLayout: function(width, height) {
291 if (Ext.isNumber(width) && Ext.isNumber(height)) {
293 me.curHeight = height;
296 this.callParent(arguments);
300 * Redraw the chart. If animations are set this will animate the chart too.
301 * @cfg {boolean} resize Optional flag which changes the default origin points of the chart for animations.
303 redraw: function(resize) {
305 chartBBox = me.chartBBox = {
308 height: me.curHeight,
312 me.surface.setSize(chartBBox.width, chartBBox.height);
313 // Instantiate Series and Axes
314 me.series.each(me.initializeSeries, me);
315 me.axes.each(me.initializeAxis, me);
316 //process all views (aggregated data etc) on stores
318 me.axes.each(function(axis) {
321 me.axes.each(function(axis) {
325 // Create legend if not already created
326 if (legend !== false) {
330 // Place axes properly, including influence from each other
333 // Reposition legend based on new axis alignment
334 if (me.legend !== false) {
335 legend.updatePosition();
338 // Find the max gutter
341 // Draw axes and series
342 me.resizing = !!resize;
344 me.axes.each(me.drawAxis, me);
345 me.series.each(me.drawCharts, me);
349 // @private set the store after rendering the chart.
350 afterRender: function() {
355 if (me.categoryNames) {
356 me.setCategoryNames(me.categoryNames);
359 if (me.tipRenderer) {
360 ref = me.getFunctionRef(me.tipRenderer);
361 me.setTipRenderer(ref.fn, ref.scope);
363 me.bindStore(me.store, true);
367 // @private get x and y position of the mouse cursor.
368 getEventXY: function(e) {
370 box = this.surface.getRegion(),
372 x = pageXY[0] - box.left,
373 y = pageXY[1] - box.top;
377 // @private wrap the mouse down position to delegate the event to the series.
378 onClick: function(e) {
380 position = me.getEventXY(e),
383 // Ask each series if it has an item corresponding to (not necessarily exactly
384 // on top of) the current mouse coords. Fire itemclick event.
385 me.series.each(function(series) {
386 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
387 if (series.getItemForPoint) {
388 item = series.getItemForPoint(position[0], position[1]);
390 series.fireEvent('itemclick', item);
397 // @private wrap the mouse down position to delegate the event to the series.
398 onMouseDown: function(e) {
400 position = me.getEventXY(e),
404 me.mixins.mask.onMouseDown.call(me, e);
406 // Ask each series if it has an item corresponding to (not necessarily exactly
407 // on top of) the current mouse coords. Fire mousedown event.
408 me.series.each(function(series) {
409 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
410 if (series.getItemForPoint) {
411 item = series.getItemForPoint(position[0], position[1]);
413 series.fireEvent('itemmousedown', item);
420 // @private wrap the mouse up event to delegate it to the series.
421 onMouseUp: function(e) {
423 position = me.getEventXY(e),
427 me.mixins.mask.onMouseUp.call(me, e);
429 // Ask each series if it has an item corresponding to (not necessarily exactly
430 // on top of) the current mouse coords. Fire mousedown event.
431 me.series.each(function(series) {
432 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
433 if (series.getItemForPoint) {
434 item = series.getItemForPoint(position[0], position[1]);
436 series.fireEvent('itemmouseup', item);
443 // @private wrap the mouse move event so it can be delegated to the series.
444 onMouseMove: function(e) {
446 position = me.getEventXY(e),
447 item, last, storeItem, storeField;
450 me.mixins.mask.onMouseMove.call(me, e);
452 // Ask each series if it has an item corresponding to (not necessarily exactly
453 // on top of) the current mouse coords. Fire itemmouseover/out events.
454 me.series.each(function(series) {
455 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
456 if (series.getItemForPoint) {
457 item = series.getItemForPoint(position[0], position[1]);
458 last = series._lastItemForPoint;
459 storeItem = series._lastStoreItem;
460 storeField = series._lastStoreField;
463 if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) {
465 series.fireEvent('itemmouseout', last);
466 delete series._lastItemForPoint;
467 delete series._lastStoreField;
468 delete series._lastStoreItem;
471 series.fireEvent('itemmouseover', item);
472 series._lastItemForPoint = item;
473 series._lastStoreItem = item.storeItem;
474 series._lastStoreField = item.storeField;
479 last = series._lastItemForPoint;
481 series.fireEvent('itemmouseout', last);
482 delete series._lastItemForPoint;
483 delete series._lastStoreField;
484 delete series._lastStoreItem;
490 // @private handle mouse leave event.
491 onMouseLeave: function(e) {
494 me.mixins.mask.onMouseLeave.call(me, e);
496 me.series.each(function(series) {
497 delete series._lastItemForPoint;
501 // @private buffered refresh for when we update the store
502 delayRefresh: function() {
504 if (!me.refreshTask) {
505 me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
507 me.refreshTask.delay(me.refreshBuffer);
511 refresh: function() {
513 if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) {
514 if (me.fireEvent('beforerefresh', me) !== false) {
516 me.fireEvent('refresh', me);
522 * Changes the data store bound to this chart and refreshes it.
523 * @param {Store} store The store to bind to this chart
525 bindStore: function(store, initial) {
527 if (!initial && me.store) {
528 if (store !== me.store && me.store.autoDestroy) {
532 me.store.un('datachanged', me.refresh, me);
533 me.store.un('add', me.delayRefresh, me);
534 me.store.un('remove', me.delayRefresh, me);
535 me.store.un('update', me.delayRefresh, me);
536 me.store.un('clear', me.refresh, me);
540 store = Ext.data.StoreManager.lookup(store);
543 datachanged: me.refresh,
544 add: me.delayRefresh,
545 remove: me.delayRefresh,
546 update: me.delayRefresh,
551 if (store && !initial) {
556 // @private Create Axis
557 initializeAxis: function(axis) {
559 chartBBox = me.chartBBox,
561 h = chartBBox.height,
564 themeAttrs = me.themeAttrs,
569 config.axisStyle = Ext.apply({}, themeAttrs.axis);
570 config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft);
571 config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight);
572 config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop);
573 config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom);
574 config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft);
575 config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight);
576 config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop);
577 config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom);
579 switch (axis.position) {
614 Ext.apply(config, axis);
615 axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
618 Ext.apply(axis, config);
624 * @private Adjust the dimensions and positions of each axis and the chart body area after accounting
625 * for the space taken up on each side by the axes and legend.
627 alignAxes: function() {
631 edges = ['top', 'right', 'bottom', 'left'],
633 insetPadding = me.insetPadding,
637 bottom: insetPadding,
641 function getAxis(edge) {
642 var i = axes.findIndex('position', edge);
643 return (i < 0) ? null : axes.getAt(i);
646 // Find the space needed by axes and legend as a positive inset from each edge
647 Ext.each(edges, function(edge) {
648 var isVertical = (edge === 'left' || edge === 'right'),
649 axis = getAxis(edge),
652 // Add legend size if it's on this edge
653 if (legend !== false) {
654 if (legend.position === edge) {
655 bbox = legend.getBBox();
656 insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge];
660 // Add axis size if there's one on this edge only if it has been
662 if (axis && axis.bbox) {
664 insets[edge] += (isVertical ? bbox.width : bbox.height);
667 // Build the chart bbox based on the collected inset values
671 width: me.curWidth - insets.left - insets.right,
672 height: me.curHeight - insets.top - insets.bottom
674 me.chartBBox = chartBBox;
676 // Go back through each axis and set its length and position based on the
677 // corresponding edge of the chartBBox
678 axes.each(function(axis) {
679 var pos = axis.position,
680 isVertical = (pos === 'left' || pos === 'right');
682 axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x);
683 axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height);
684 axis.width = (isVertical ? chartBBox.width : chartBBox.height);
685 axis.length = (isVertical ? chartBBox.height : chartBBox.width);
689 // @private initialize the series.
690 initializeSeries: function(series, idx) {
692 themeAttrs = me.themeAttrs,
693 seriesObj, markerObj, seriesThemes, st,
694 markerThemes, colorArrayStyle = [],
698 seriesId: series.seriesId
701 seriesThemes = themeAttrs.seriesThemes;
702 markerThemes = themeAttrs.markerThemes;
703 seriesObj = Ext.apply({}, themeAttrs.series);
704 markerObj = Ext.apply({}, themeAttrs.marker);
705 config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]);
706 config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
707 config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
708 if (themeAttrs.colors) {
709 config.colorArrayStyle = themeAttrs.colors;
711 colorArrayStyle = [];
712 for (l = seriesThemes.length; i < l; i++) {
713 st = seriesThemes[i];
714 if (st.fill || st.stroke) {
715 colorArrayStyle.push(st.fill || st.stroke);
718 if (colorArrayStyle.length) {
719 config.colorArrayStyle = colorArrayStyle;
722 config.seriesIdx = idx;
724 if (series instanceof Ext.chart.series.Series) {
725 Ext.apply(series, config);
727 Ext.applyIf(config, series);
728 series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
730 if (series.initialize) {
736 getMaxGutter: function() {
739 me.series.each(function(s) {
740 var gutter = s.getGutters && s.getGutters() || [0, 0];
741 maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
742 maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
744 me.maxGutter = maxGutter;
747 // @private draw axis.
748 drawAxis: function(axis) {
752 // @private draw series.
753 drawCharts: function(series) {
754 series.triggerafterrender = false;
757 series.fireEvent('afterrender');
761 // @private remove gently.
762 destroy: function() {
763 this.surface.destroy();
764 this.bindStore(null);
765 this.callParent(arguments);