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 * Charts provide a flexible way to achieve a wide range of data visualization capablitities.
17 * Each Chart gets its data directly from a {@link Ext.data.Store Store}, and automatically
18 * updates its display whenever data in the Store changes. In addition, the look and feel
19 * of a Chart can be customized using {@link Ext.chart.theme.Theme Theme}s.
21 * ## Creating a Simple Chart
23 * Every Chart has three key parts - a {@link Ext.data.Store Store} that contains the data,
24 * an array of {@link Ext.chart.axis.Axis Axes} which define the boundaries of the Chart,
25 * and one or more {@link Ext.chart.series.Series Series} to handle the visual rendering of the data points.
27 * ### 1. Creating a Store
29 * The first step is to create a {@link Ext.data.Model Model} that represents the type of
30 * data that will be displayed in the Chart. For example the data for a chart that displays
31 * a weather forecast could be represented as a series of "WeatherPoint" data points with
32 * two fields - "temperature", and "date":
34 * Ext.define('WeatherPoint', {
35 * extend: 'Ext.data.Model',
36 * fields: ['temperature', 'date']
39 * Next a {@link Ext.data.Store Store} must be created. The store contains a collection of "WeatherPoint" Model instances.
40 * The data could be loaded dynamically, but for sake of ease this example uses inline data:
42 * var store = Ext.create('Ext.data.Store', {
43 * model: 'WeatherPoint',
45 * { temperature: 58, date: new Date(2011, 1, 1, 8) },
46 * { temperature: 63, date: new Date(2011, 1, 1, 9) },
47 * { temperature: 73, date: new Date(2011, 1, 1, 10) },
48 * { temperature: 78, date: new Date(2011, 1, 1, 11) },
49 * { temperature: 81, date: new Date(2011, 1, 1, 12) }
53 * For additional information on Models and Stores please refer to the [Data Guide](#/guide/data).
55 * ### 2. Creating the Chart object
57 * Now that a Store has been created it can be used in a Chart:
59 * Ext.create('Ext.chart.Chart', {
60 * renderTo: Ext.getBody(),
66 * That's all it takes to create a Chart instance that is backed by a Store.
67 * However, if the above code is run in a browser, a blank screen will be displayed.
68 * This is because the two pieces that are responsible for the visual display,
69 * the Chart's {@link #cfg-axes axes} and {@link #cfg-series series}, have not yet been defined.
71 * ### 3. Configuring the Axes
73 * {@link Ext.chart.axis.Axis Axes} are the lines that define the boundaries of the data points that a Chart can display.
74 * This example uses one of the most common Axes configurations - a horizontal "x" axis, and a vertical "y" axis:
76 * Ext.create('Ext.chart.Chart', {
80 * title: 'Temperature',
83 * fields: ['temperature'],
97 * The "Temperature" axis is a vertical {@link Ext.chart.axis.Numeric Numeric Axis} and is positioned on the left edge of the Chart.
98 * It represents the bounds of the data contained in the "WeatherPoint" Model's "temperature" field that was
99 * defined above. The minimum value for this axis is "0", and the maximum is "100".
101 * The horizontal axis is a {@link Ext.chart.axis.Time Time Axis} and is positioned on the bottom edge of the Chart.
102 * It represents the bounds of the data contained in the "WeatherPoint" Model's "date" field.
103 * The {@link Ext.chart.axis.Time#cfg-dateFormat dateFormat}
104 * configuration tells the Time Axis how to format it's labels.
106 * Here's what the Chart looks like now that it has its Axes configured:
108 * {@img Ext.chart.Chart/Ext.chart.Chart1.png Chart Axes}
110 * ### 4. Configuring the Series
112 * The final step in creating a simple Chart is to configure one or more {@link Ext.chart.series.Series Series}.
113 * Series are responsible for the visual representation of the data points contained in the Store.
114 * This example only has one Series:
116 * Ext.create('Ext.chart.Chart', {
125 * yField: 'temperature'
130 * This Series is a {@link Ext.chart.series.Line Line Series}, and it uses the "date" and "temperature" fields
131 * from the "WeatherPoint" Models in the Store to plot its data points:
133 * {@img Ext.chart.Chart/Ext.chart.Chart2.png Line Series}
135 * See the [Simple Chart Example](doc-resources/Ext.chart.Chart/examples/simple_chart/index.html) for a live demo.
139 * The color scheme for a Chart can be easily changed using the {@link #cfg-theme theme} configuration option:
141 * Ext.create('Ext.chart.Chart', {
147 * {@img Ext.chart.Chart/Ext.chart.Chart3.png Green Theme}
149 * For more information on Charts please refer to the [Drawing and Charting Guide](#/guide/drawing_and_charting).
152 Ext.define('Ext.chart.Chart', {
154 /* Begin Definitions */
156 alias: 'widget.chart',
158 extend: 'Ext.draw.Component',
161 themeManager: 'Ext.chart.theme.Theme',
162 mask: 'Ext.chart.Mask',
163 navigation: 'Ext.chart.Navigation'
167 'Ext.util.MixedCollection',
168 'Ext.data.StoreManager',
170 'Ext.util.DelayedTask'
173 /* End Definitions */
179 * @cfg {String} theme
180 * The name of the theme to be used. A theme defines the colors and other visual displays of tick marks
181 * on axis, text, title text, line colors, marker colors and styles, etc. Possible theme values are 'Base', 'Green',
182 * 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes 'Category1' to 'Category6'. Default value
187 * @cfg {Boolean/Object} animate
188 * True for the default animation (easing: 'ease' and duration: 500) or a standard animation config
189 * object to be used for default chart animations. Defaults to false.
194 * @cfg {Boolean/Object} legend
195 * True for the default legend display or a legend config object. Defaults to false.
200 * @cfg {Number} insetPadding
201 * The amount of inset padding in pixels for the chart. Defaults to 10.
206 * @cfg {String[]} enginePriority
207 * Defines the priority order for which Surface implementation to use. The first one supported by the current
208 * environment will be used. Defaults to `['Svg', 'Vml']`.
210 enginePriority: ['Svg', 'Vml'],
213 * @cfg {Object/Boolean} background
214 * The chart background. This can be a gradient object, image, or color. Defaults to false for no
215 * background. For example, if `background` were to be a color we could set the object as
222 * You can specify an image by using:
225 * image: 'http://path.to.image/'
228 * Also you can specify a gradient by using the gradient object syntax:
248 * @cfg {Object[]} gradients
249 * Define a set of gradients that can be used as `fill` property in sprites. The gradients array is an
250 * array of objects with the following properties:
252 * - **id** - string - The unique name of the gradient.
253 * - **angle** - number, optional - The angle of the gradient in degrees.
254 * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values
285 * Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
287 * sprite.setAttributes({
288 * fill: 'url(#gradientId)'
293 * @cfg {Ext.data.Store} store
294 * The store that supplies data to this chart.
298 * @cfg {Ext.chart.series.Series[]} series
299 * Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
305 * 'afterrender': function() {
306 * console('afterrender');
309 * xField: 'category',
315 * @cfg {Ext.chart.axis.Axis[]} axes
316 * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
322 * title: 'Number of Hits',
324 * //one minor tick between two major ticks
328 * position: 'bottom',
330 * title: 'Month of the Year'
334 constructor: function(config) {
338 config = Ext.apply({}, config);
339 me.initTheme(config.theme || me.theme);
341 Ext.apply(config, { gradients: me.gradients });
344 Ext.apply(config, { background: me.background });
346 if (config.animate) {
351 if (Ext.isObject(config.animate)) {
352 config.animate = Ext.applyIf(config.animate, defaultAnim);
355 config.animate = defaultAnim;
358 me.mixins.mask.constructor.call(me, config);
359 me.mixins.navigation.constructor.call(me, config);
360 me.callParent([config]);
363 getChartStore: function(){
364 return this.substore || this.store;
367 initComponent: function() {
383 * @event beforerefresh
384 * Fires before a refresh to the chart data is called. If the beforerefresh handler returns false the
385 * {@link #refresh} action will be cancelled.
386 * @param {Ext.chart.Chart} this
391 * Fires after the chart data has been refreshed.
392 * @param {Ext.chart.Chart} this
404 me.maxGutter = [0, 0];
405 me.store = Ext.data.StoreManager.lookup(me.store);
407 me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
409 me.axes.addAll(axes);
412 me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
414 me.series.addAll(series);
416 if (me.legend !== false) {
417 me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
421 mousemove: me.onMouseMove,
422 mouseleave: me.onMouseLeave,
423 mousedown: me.onMouseDown,
424 mouseup: me.onMouseUp,
429 // @private overrides the component method to set the correct dimensions to the chart.
430 afterComponentLayout: function(width, height) {
432 if (Ext.isNumber(width) && Ext.isNumber(height)) {
434 me.curHeight = height;
437 this.callParent(arguments);
441 * Redraws the chart. If animations are set this will animate the chart too.
442 * @param {Boolean} resize (optional) flag which changes the default origin points of the chart for animations.
444 redraw: function(resize) {
446 chartBBox = me.chartBBox = {
449 height: me.curHeight,
453 me.surface.setSize(chartBBox.width, chartBBox.height);
454 // Instantiate Series and Axes
455 me.series.each(me.initializeSeries, me);
456 me.axes.each(me.initializeAxis, me);
457 //process all views (aggregated data etc) on stores
459 me.axes.each(function(axis) {
462 me.axes.each(function(axis) {
466 // Create legend if not already created
467 if (legend !== false) {
471 // Place axes properly, including influence from each other
474 // Reposition legend based on new axis alignment
475 if (me.legend !== false) {
476 legend.updatePosition();
479 // Find the max gutter
482 // Draw axes and series
483 me.resizing = !!resize;
485 me.axes.each(me.drawAxis, me);
486 me.series.each(me.drawCharts, me);
490 // @private set the store after rendering the chart.
491 afterRender: function() {
496 if (me.categoryNames) {
497 me.setCategoryNames(me.categoryNames);
500 if (me.tipRenderer) {
501 ref = me.getFunctionRef(me.tipRenderer);
502 me.setTipRenderer(ref.fn, ref.scope);
504 me.bindStore(me.store, true);
508 // @private get x and y position of the mouse cursor.
509 getEventXY: function(e) {
511 box = this.surface.getRegion(),
513 x = pageXY[0] - box.left,
514 y = pageXY[1] - box.top;
518 // @private wrap the mouse down position to delegate the event to the series.
519 onClick: function(e) {
521 position = me.getEventXY(e),
524 // Ask each series if it has an item corresponding to (not necessarily exactly
525 // on top of) the current mouse coords. Fire itemclick event.
526 me.series.each(function(series) {
527 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
528 if (series.getItemForPoint) {
529 item = series.getItemForPoint(position[0], position[1]);
531 series.fireEvent('itemclick', item);
538 // @private wrap the mouse down position to delegate the event to the series.
539 onMouseDown: function(e) {
541 position = me.getEventXY(e),
545 me.mixins.mask.onMouseDown.call(me, e);
547 // Ask each series if it has an item corresponding to (not necessarily exactly
548 // on top of) the current mouse coords. Fire mousedown event.
549 me.series.each(function(series) {
550 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
551 if (series.getItemForPoint) {
552 item = series.getItemForPoint(position[0], position[1]);
554 series.fireEvent('itemmousedown', item);
561 // @private wrap the mouse up event to delegate it to the series.
562 onMouseUp: function(e) {
564 position = me.getEventXY(e),
568 me.mixins.mask.onMouseUp.call(me, e);
570 // Ask each series if it has an item corresponding to (not necessarily exactly
571 // on top of) the current mouse coords. Fire mousedown event.
572 me.series.each(function(series) {
573 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
574 if (series.getItemForPoint) {
575 item = series.getItemForPoint(position[0], position[1]);
577 series.fireEvent('itemmouseup', item);
584 // @private wrap the mouse move event so it can be delegated to the series.
585 onMouseMove: function(e) {
587 position = me.getEventXY(e),
588 item, last, storeItem, storeField;
591 me.mixins.mask.onMouseMove.call(me, e);
593 // Ask each series if it has an item corresponding to (not necessarily exactly
594 // on top of) the current mouse coords. Fire itemmouseover/out events.
595 me.series.each(function(series) {
596 if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
597 if (series.getItemForPoint) {
598 item = series.getItemForPoint(position[0], position[1]);
599 last = series._lastItemForPoint;
600 storeItem = series._lastStoreItem;
601 storeField = series._lastStoreField;
604 if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) {
606 series.fireEvent('itemmouseout', last);
607 delete series._lastItemForPoint;
608 delete series._lastStoreField;
609 delete series._lastStoreItem;
612 series.fireEvent('itemmouseover', item);
613 series._lastItemForPoint = item;
614 series._lastStoreItem = item.storeItem;
615 series._lastStoreField = item.storeField;
620 last = series._lastItemForPoint;
622 series.fireEvent('itemmouseout', last);
623 delete series._lastItemForPoint;
624 delete series._lastStoreField;
625 delete series._lastStoreItem;
631 // @private handle mouse leave event.
632 onMouseLeave: function(e) {
635 me.mixins.mask.onMouseLeave.call(me, e);
637 me.series.each(function(series) {
638 delete series._lastItemForPoint;
642 // @private buffered refresh for when we update the store
643 delayRefresh: function() {
645 if (!me.refreshTask) {
646 me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
648 me.refreshTask.delay(me.refreshBuffer);
652 refresh: function() {
654 if (me.rendered && me.curWidth !== undefined && me.curHeight !== undefined) {
655 if (me.fireEvent('beforerefresh', me) !== false) {
657 me.fireEvent('refresh', me);
663 * Changes the data store bound to this chart and refreshes it.
664 * @param {Ext.data.Store} store The store to bind to this chart
666 bindStore: function(store, initial) {
668 if (!initial && me.store) {
669 if (store !== me.store && me.store.autoDestroy) {
670 me.store.destroyStore();
673 me.store.un('datachanged', me.refresh, me);
674 me.store.un('add', me.delayRefresh, me);
675 me.store.un('remove', me.delayRefresh, me);
676 me.store.un('update', me.delayRefresh, me);
677 me.store.un('clear', me.refresh, me);
681 store = Ext.data.StoreManager.lookup(store);
684 datachanged: me.refresh,
685 add: me.delayRefresh,
686 remove: me.delayRefresh,
687 update: me.delayRefresh,
692 if (store && !initial) {
697 // @private Create Axis
698 initializeAxis: function(axis) {
700 chartBBox = me.chartBBox,
702 h = chartBBox.height,
705 themeAttrs = me.themeAttrs,
710 config.axisStyle = Ext.apply({}, themeAttrs.axis);
711 config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft);
712 config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight);
713 config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop);
714 config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom);
715 config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft);
716 config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight);
717 config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop);
718 config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom);
720 switch (axis.position) {
755 Ext.apply(config, axis);
756 axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
759 Ext.apply(axis, config);
765 * @private Adjust the dimensions and positions of each axis and the chart body area after accounting
766 * for the space taken up on each side by the axes and legend.
768 alignAxes: function() {
772 edges = ['top', 'right', 'bottom', 'left'],
774 insetPadding = me.insetPadding,
778 bottom: insetPadding,
782 function getAxis(edge) {
783 var i = axes.findIndex('position', edge);
784 return (i < 0) ? null : axes.getAt(i);
787 // Find the space needed by axes and legend as a positive inset from each edge
788 Ext.each(edges, function(edge) {
789 var isVertical = (edge === 'left' || edge === 'right'),
790 axis = getAxis(edge),
793 // Add legend size if it's on this edge
794 if (legend !== false) {
795 if (legend.position === edge) {
796 bbox = legend.getBBox();
797 insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge];
801 // Add axis size if there's one on this edge only if it has been
803 if (axis && axis.bbox) {
805 insets[edge] += (isVertical ? bbox.width : bbox.height);
808 // Build the chart bbox based on the collected inset values
812 width: me.curWidth - insets.left - insets.right,
813 height: me.curHeight - insets.top - insets.bottom
815 me.chartBBox = chartBBox;
817 // Go back through each axis and set its length and position based on the
818 // corresponding edge of the chartBBox
819 axes.each(function(axis) {
820 var pos = axis.position,
821 isVertical = (pos === 'left' || pos === 'right');
823 axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x);
824 axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height);
825 axis.width = (isVertical ? chartBBox.width : chartBBox.height);
826 axis.length = (isVertical ? chartBBox.height : chartBBox.width);
830 // @private initialize the series.
831 initializeSeries: function(series, idx) {
833 themeAttrs = me.themeAttrs,
834 seriesObj, markerObj, seriesThemes, st,
835 markerThemes, colorArrayStyle = [],
839 seriesId: series.seriesId
842 seriesThemes = themeAttrs.seriesThemes;
843 markerThemes = themeAttrs.markerThemes;
844 seriesObj = Ext.apply({}, themeAttrs.series);
845 markerObj = Ext.apply({}, themeAttrs.marker);
846 config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]);
847 config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
848 config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
849 if (themeAttrs.colors) {
850 config.colorArrayStyle = themeAttrs.colors;
852 colorArrayStyle = [];
853 for (l = seriesThemes.length; i < l; i++) {
854 st = seriesThemes[i];
855 if (st.fill || st.stroke) {
856 colorArrayStyle.push(st.fill || st.stroke);
859 if (colorArrayStyle.length) {
860 config.colorArrayStyle = colorArrayStyle;
863 config.seriesIdx = idx;
865 if (series instanceof Ext.chart.series.Series) {
866 Ext.apply(series, config);
868 Ext.applyIf(config, series);
869 series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
871 if (series.initialize) {
877 getMaxGutter: function() {
880 me.series.each(function(s) {
881 var gutter = s.getGutters && s.getGutters() || [0, 0];
882 maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
883 maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
885 me.maxGutter = maxGutter;
888 // @private draw axis.
889 drawAxis: function(axis) {
893 // @private draw series.
894 drawCharts: function(series) {
895 series.triggerafterrender = false;
898 series.fireEvent('afterrender');
902 // @private remove gently.
903 destroy: function() {
904 Ext.destroy(this.surface);
905 this.bindStore(null);
906 this.callParent(arguments);