Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / chart / Chart.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.chart.Chart
17  * @extends Ext.draw.Component
18  *
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:
23  *
24   <pre><code>
25     Ext.create('Ext.chart.Chart', {
26         renderTo: Ext.getBody(),
27         width: 800,
28         height: 600,
29         animate: true,
30         store: store1,
31         shadow: true,
32         theme: 'Category1',
33         legend: {
34             position: 'right'
35         },
36         axes: [ ...some axes options... ],
37         series: [ ...some series options... ]
38     });
39   </code></pre>
40  *
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).
46  */
47 Ext.define('Ext.chart.Chart', {
48
49     /* Begin Definitions */
50
51     alias: 'widget.chart',
52
53     extend: 'Ext.draw.Component',
54     
55     mixins: {
56         themeManager: 'Ext.chart.theme.Theme',
57         mask: 'Ext.chart.Mask',
58         navigation: 'Ext.chart.Navigation'
59     },
60
61     requires: [
62         'Ext.util.MixedCollection',
63         'Ext.data.StoreManager',
64         'Ext.chart.Legend',
65         'Ext.util.DelayedTask'
66     ],
67
68     /* End Definitions */
69
70     // @private
71     viewBox: false,
72
73     /**
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'.
78      */
79
80     /**
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.
83      */
84     animate: false,
85
86     /**
87      * @cfg {Boolean/Object} legend (optional) true for the default legend display or a legend config object. Defaults to false.
88      */
89     legend: false,
90
91     /**
92      * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10.
93      */
94     insetPadding: 10,
95
96     /**
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.
100      */
101     enginePriority: ['Svg', 'Vml'],
102
103     /**
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.
106      *
107      * For example, if `background` were to be a color we could set the object as
108      *
109      <pre><code>
110         background: {
111             //color string
112             fill: '#ccc'
113         }
114      </code></pre>
115
116      You can specify an image by using:
117
118      <pre><code>
119         background: {
120             image: 'http://path.to.image/'
121         }
122      </code></pre>
123
124      Also you can specify a gradient by using the gradient object syntax:
125
126      <pre><code>
127         background: {
128             gradient: {
129                 id: 'gradientId',
130                 angle: 45,
131                 stops: {
132                     0: {
133                         color: '#555'
134                     }
135                     100: {
136                         color: '#ddd'
137                     }
138                 }
139             }
140         }
141      </code></pre>
142      */
143     background: false,
144
145     /**
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:
148      *
149      * <ul>
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
153      * as values</li>
154      * </ul>
155      *
156
157      For example:
158
159      <pre><code>
160         gradients: [{
161             id: 'gradientId',
162             angle: 45,
163             stops: {
164                 0: {
165                     color: '#555'
166                 },
167                 100: {
168                     color: '#ddd'
169                 }
170             }
171         },  {
172             id: 'gradientId2',
173             angle: 0,
174             stops: {
175                 0: {
176                     color: '#590'
177                 },
178                 20: {
179                     color: '#599'
180                 },
181                 100: {
182                     color: '#ddd'
183                 }
184             }
185         }]
186      </code></pre>
187
188      Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
189
190      <pre><code>
191         sprite.setAttributes({
192             fill: 'url(#gradientId)'
193         }, true);
194      </code></pre>
195
196      */
197
198
199     constructor: function(config) {
200         var me = this,
201             defaultAnim;
202         me.initTheme(config.theme || me.theme);
203         if (me.gradients) {
204             Ext.apply(config, { gradients: me.gradients });
205         }
206         if (me.background) {
207             Ext.apply(config, { background: me.background });
208         }
209         if (config.animate) {
210             defaultAnim = {
211                 easing: 'ease',
212                 duration: 500
213             };
214             if (Ext.isObject(config.animate)) {
215                 config.animate = Ext.applyIf(config.animate, defaultAnim);
216             }
217             else {
218                 config.animate = defaultAnim;
219             }
220         }
221         me.mixins.mask.constructor.call(me, config);
222         me.mixins.navigation.constructor.call(me, config);
223         me.callParent([config]);
224     },
225
226     initComponent: function() {
227         var me = this,
228             axes,
229             series;
230         me.callParent();
231         me.addEvents(
232             'itemmousedown',
233             'itemmouseup',
234             'itemmouseover',
235             'itemmouseout',
236             'itemclick',
237             'itemdoubleclick',
238             'itemdragstart',
239             'itemdrag',
240             'itemdragend',
241             /**
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
246                  */
247             'beforerefresh',
248             /**
249                  * @event refresh
250                  * Fires after the chart data has been refreshed.
251                  * @param {Chart} this
252                  */
253             'refresh'
254         );
255         Ext.applyIf(me, {
256             zoom: {
257                 width: 1,
258                 height: 1,
259                 x: 0,
260                 y: 0
261             }
262         });
263         me.maxGutter = [0, 0];
264         me.store = Ext.data.StoreManager.lookup(me.store);
265         axes = me.axes;
266         me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
267         if (axes) {
268             me.axes.addAll(axes);
269         }
270         series = me.series;
271         me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
272         if (series) {
273             me.series.addAll(series);
274         }
275         if (me.legend !== false) {
276             me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
277         }
278
279         me.on({
280             mousemove: me.onMouseMove,
281             mouseleave: me.onMouseLeave,
282             mousedown: me.onMouseDown,
283             mouseup: me.onMouseUp,
284             scope: me
285         });
286     },
287
288     // @private overrides the component method to set the correct dimensions to the chart.
289     afterComponentLayout: function(width, height) {
290         var me = this;
291         if (Ext.isNumber(width) && Ext.isNumber(height)) {
292             me.curWidth = width;
293             me.curHeight = height;
294             me.redraw(true);
295         }
296         this.callParent(arguments);
297     },
298
299     /**
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.
302      */
303     redraw: function(resize) {
304         var me = this,
305             chartBBox = me.chartBBox = {
306                 x: 0,
307                 y: 0,
308                 height: me.curHeight,
309                 width: me.curWidth
310             },
311             legend = me.legend;
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
317         //before rendering.
318         me.axes.each(function(axis) {
319             axis.processView();
320         });
321         me.axes.each(function(axis) {
322             axis.drawAxis(true);
323         });
324
325         // Create legend if not already created
326         if (legend !== false) {
327             legend.create();
328         }
329
330         // Place axes properly, including influence from each other
331         me.alignAxes();
332
333         // Reposition legend based on new axis alignment
334         if (me.legend !== false) {
335             legend.updatePosition();
336         }
337
338         // Find the max gutter
339         me.getMaxGutter();
340
341         // Draw axes and series
342         me.resizing = !!resize;
343
344         me.axes.each(me.drawAxis, me);
345         me.series.each(me.drawCharts, me);
346         me.resizing = false;
347     },
348
349     // @private set the store after rendering the chart.
350     afterRender: function() {
351         var ref,
352             me = this;
353         this.callParent();
354
355         if (me.categoryNames) {
356             me.setCategoryNames(me.categoryNames);
357         }
358
359         if (me.tipRenderer) {
360             ref = me.getFunctionRef(me.tipRenderer);
361             me.setTipRenderer(ref.fn, ref.scope);
362         }
363         me.bindStore(me.store, true);
364         me.refresh();
365     },
366
367     // @private get x and y position of the mouse cursor.
368     getEventXY: function(e) {
369         var me = this,
370             box = this.surface.getRegion(),
371             pageXY = e.getXY(),
372             x = pageXY[0] - box.left,
373             y = pageXY[1] - box.top;
374         return [x, y];
375     },
376
377     // @private wrap the mouse down position to delegate the event to the series.
378     onClick: function(e) {
379         var me = this,
380             position = me.getEventXY(e),
381             item;
382
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]);
389                     if (item) {
390                         series.fireEvent('itemclick', item);
391                     }
392                 }
393             }
394         }, me);
395     },
396
397     // @private wrap the mouse down position to delegate the event to the series.
398     onMouseDown: function(e) {
399         var me = this,
400             position = me.getEventXY(e),
401             item;
402
403         if (me.mask) {
404             me.mixins.mask.onMouseDown.call(me, e);
405         }
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]);
412                     if (item) {
413                         series.fireEvent('itemmousedown', item);
414                     }
415                 }
416             }
417         }, me);
418     },
419
420     // @private wrap the mouse up event to delegate it to the series.
421     onMouseUp: function(e) {
422         var me = this,
423             position = me.getEventXY(e),
424             item;
425
426         if (me.mask) {
427             me.mixins.mask.onMouseUp.call(me, e);
428         }
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]);
435                     if (item) {
436                         series.fireEvent('itemmouseup', item);
437                     }
438                 }
439             }
440         }, me);
441     },
442
443     // @private wrap the mouse move event so it can be delegated to the series.
444     onMouseMove: function(e) {
445         var me = this,
446             position = me.getEventXY(e),
447             item, last, storeItem, storeField;
448
449         if (me.mask) {
450             me.mixins.mask.onMouseMove.call(me, e);
451         }
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;
461
462
463                     if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) {
464                         if (last) {
465                             series.fireEvent('itemmouseout', last);
466                             delete series._lastItemForPoint;
467                             delete series._lastStoreField;
468                             delete series._lastStoreItem;
469                         }
470                         if (item) {
471                             series.fireEvent('itemmouseover', item);
472                             series._lastItemForPoint = item;
473                             series._lastStoreItem = item.storeItem;
474                             series._lastStoreField = item.storeField;
475                         }
476                     }
477                 }
478             } else {
479                 last = series._lastItemForPoint;
480                 if (last) {
481                     series.fireEvent('itemmouseout', last);
482                     delete series._lastItemForPoint;
483                     delete series._lastStoreField;
484                     delete series._lastStoreItem;
485                 }
486             }
487         }, me);
488     },
489
490     // @private handle mouse leave event.
491     onMouseLeave: function(e) {
492         var me = this;
493         if (me.mask) {
494             me.mixins.mask.onMouseLeave.call(me, e);
495         }
496         me.series.each(function(series) {
497             delete series._lastItemForPoint;
498         });
499     },
500
501     // @private buffered refresh for when we update the store
502     delayRefresh: function() {
503         var me = this;
504         if (!me.refreshTask) {
505             me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
506         }
507         me.refreshTask.delay(me.refreshBuffer);
508     },
509
510     // @private
511     refresh: function() {
512         var me = this;
513         if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) {
514             if (me.fireEvent('beforerefresh', me) !== false) {
515                 me.redraw();
516                 me.fireEvent('refresh', me);
517             }
518         }
519     },
520
521     /**
522      * Changes the data store bound to this chart and refreshes it.
523      * @param {Store} store The store to bind to this chart
524      */
525     bindStore: function(store, initial) {
526         var me = this;
527         if (!initial && me.store) {
528             if (store !== me.store && me.store.autoDestroy) {
529                 me.store.destroy();
530             }
531             else {
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);
537             }
538         }
539         if (store) {
540             store = Ext.data.StoreManager.lookup(store);
541             store.on({
542                 scope: me,
543                 datachanged: me.refresh,
544                 add: me.delayRefresh,
545                 remove: me.delayRefresh,
546                 update: me.delayRefresh,
547                 clear: me.refresh
548             });
549         }
550         me.store = store;
551         if (store && !initial) {
552             me.refresh();
553         }
554     },
555
556     // @private Create Axis
557     initializeAxis: function(axis) {
558         var me = this,
559             chartBBox = me.chartBBox,
560             w = chartBBox.width,
561             h = chartBBox.height,
562             x = chartBBox.x,
563             y = chartBBox.y,
564             themeAttrs = me.themeAttrs,
565             config = {
566                 chart: me
567             };
568         if (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);
578         }
579         switch (axis.position) {
580             case 'top':
581                 Ext.apply(config, {
582                     length: w,
583                     width: h,
584                     x: x,
585                     y: y
586                 });
587             break;
588             case 'bottom':
589                 Ext.apply(config, {
590                     length: w,
591                     width: h,
592                     x: x,
593                     y: h
594                 });
595             break;
596             case 'left':
597                 Ext.apply(config, {
598                     length: h,
599                     width: w,
600                     x: x,
601                     y: h
602                 });
603             break;
604             case 'right':
605                 Ext.apply(config, {
606                     length: h,
607                     width: w,
608                     x: w,
609                     y: h
610                 });
611             break;
612         }
613         if (!axis.chart) {
614             Ext.apply(config, axis);
615             axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
616         }
617         else {
618             Ext.apply(axis, config);
619         }
620     },
621
622
623     /**
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.
626      */
627     alignAxes: function() {
628         var me = this,
629             axes = me.axes,
630             legend = me.legend,
631             edges = ['top', 'right', 'bottom', 'left'],
632             chartBBox,
633             insetPadding = me.insetPadding,
634             insets = {
635                 top: insetPadding,
636                 right: insetPadding,
637                 bottom: insetPadding,
638                 left: insetPadding
639             };
640
641         function getAxis(edge) {
642             var i = axes.findIndex('position', edge);
643             return (i < 0) ? null : axes.getAt(i);
644         }
645
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),
650                 bbox;
651
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];
657                 }
658             }
659
660             // Add axis size if there's one on this edge only if it has been
661             //drawn before.
662             if (axis && axis.bbox) {
663                 bbox = axis.bbox;
664                 insets[edge] += (isVertical ? bbox.width : bbox.height);
665             }
666         });
667         // Build the chart bbox based on the collected inset values
668         chartBBox = {
669             x: insets.left,
670             y: insets.top,
671             width: me.curWidth - insets.left - insets.right,
672             height: me.curHeight - insets.top - insets.bottom
673         };
674         me.chartBBox = chartBBox;
675
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');
681
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);
686         });
687     },
688
689     // @private initialize the series.
690     initializeSeries: function(series, idx) {
691         var me = this,
692             themeAttrs = me.themeAttrs,
693             seriesObj, markerObj, seriesThemes, st,
694             markerThemes, colorArrayStyle = [],
695             i = 0, l,
696             config = {
697                 chart: me,
698                 seriesId: series.seriesId
699             };
700         if (themeAttrs) {
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;
710             } else {
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);
716                     }
717                 }
718                 if (colorArrayStyle.length) {
719                     config.colorArrayStyle = colorArrayStyle;
720                 }
721             }
722             config.seriesIdx = idx;
723         }
724         if (series instanceof Ext.chart.series.Series) {
725             Ext.apply(series, config);
726         } else {
727             Ext.applyIf(config, series);
728             series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
729         }
730         if (series.initialize) {
731             series.initialize();
732         }
733     },
734
735     // @private
736     getMaxGutter: function() {
737         var me = this,
738             maxGutter = [0, 0];
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]);
743         });
744         me.maxGutter = maxGutter;
745     },
746
747     // @private draw axis.
748     drawAxis: function(axis) {
749         axis.drawAxis();
750     },
751
752     // @private draw series.
753     drawCharts: function(series) {
754         series.triggerafterrender = false;
755         series.drawSeries();
756         if (!this.animate) {
757             series.fireEvent('afterrender');
758         }
759     },
760
761     // @private remove gently.
762     destroy: function() {
763         this.surface.destroy();
764         this.bindStore(null);
765         this.callParent(arguments);
766     }
767 });
768