Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / chart / series / Series.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.series.Series
17  *
18  * Series is the abstract class containing the common logic to all chart series. Series includes
19  * methods from Labels, Highlights, Tips and Callouts mixins. This class implements the logic of handling
20  * mouse events, animating, hiding, showing all elements and returning the color of the series to be used as a legend item.
21  *
22  * ## Listeners
23  *
24  * The series class supports listeners via the Observable syntax. Some of these listeners are:
25  *
26  *  - `itemmouseup` When the user interacts with a marker.
27  *  - `itemmousedown` When the user interacts with a marker.
28  *  - `itemmousemove` When the user iteracts with a marker.
29  *  - `afterrender` Will be triggered when the animation ends or when the series has been rendered completely.
30  *
31  * For example:
32  *
33  *     series: [{
34  *             type: 'column',
35  *             axis: 'left',
36  *             listeners: {
37  *                     'afterrender': function() {
38  *                             console('afterrender');
39  *                     }
40  *             },
41  *             xField: 'category',
42  *             yField: 'data1'
43  *     }]
44  */
45 Ext.define('Ext.chart.series.Series', {
46
47     /* Begin Definitions */
48
49     mixins: {
50         observable: 'Ext.util.Observable',
51         labels: 'Ext.chart.Label',
52         highlights: 'Ext.chart.Highlight',
53         tips: 'Ext.chart.Tip',
54         callouts: 'Ext.chart.Callout'
55     },
56
57     /* End Definitions */
58
59     /**
60      * @cfg {Boolean/Object} highlight
61      * If set to `true` it will highlight the markers or the series when hovering
62      * with the mouse. This parameter can also be an object with the same style
63      * properties you would apply to a {@link Ext.draw.Sprite} to apply custom
64      * styles to markers and series.
65      */
66
67     /**
68      * @cfg {Object} tips
69      * Add tooltips to the visualization's markers. The options for the tips are the
70      * same configuration used with {@link Ext.tip.ToolTip}. For example:
71      *
72      *     tips: {
73      *       trackMouse: true,
74      *       width: 140,
75      *       height: 28,
76      *       renderer: function(storeItem, item) {
77      *         this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
78      *       }
79      *     },
80      */
81
82     /**
83      * @cfg {String} type
84      * The type of series. Set in subclasses.
85      */
86     type: null,
87
88     /**
89      * @cfg {String} title
90      * The human-readable name of the series.
91      */
92     title: null,
93
94     /**
95      * @cfg {Boolean} showInLegend
96      * Whether to show this series in the legend.
97      */
98     showInLegend: true,
99
100     /**
101      * @cfg {Function} renderer
102      * A function that can be overridden to set custom styling properties to each rendered element.
103      * Passes in (sprite, record, attributes, index, store) to the function.
104      */
105     renderer: function(sprite, record, attributes, index, store) {
106         return attributes;
107     },
108
109     /**
110      * @cfg {Array} shadowAttributes
111      * An array with shadow attributes
112      */
113     shadowAttributes: null,
114
115     //@private triggerdrawlistener flag
116     triggerAfterDraw: false,
117
118     /**
119      * @cfg {Object} listeners
120      * An (optional) object with event callbacks. All event callbacks get the target *item* as first parameter. The callback functions are:
121      *
122      *  - itemmouseover
123      *  - itemmouseout
124      *  - itemmousedown
125      *  - itemmouseup
126      */
127
128     constructor: function(config) {
129         var me = this;
130         if (config) {
131             Ext.apply(me, config);
132         }
133
134         me.shadowGroups = [];
135
136         me.mixins.labels.constructor.call(me, config);
137         me.mixins.highlights.constructor.call(me, config);
138         me.mixins.tips.constructor.call(me, config);
139         me.mixins.callouts.constructor.call(me, config);
140
141         me.addEvents({
142             scope: me,
143             itemmouseover: true,
144             itemmouseout: true,
145             itemmousedown: true,
146             itemmouseup: true,
147             mouseleave: true,
148             afterdraw: true,
149
150             /**
151              * @event titlechange
152              * Fires when the series title is changed via {@link #setTitle}.
153              * @param {String} title The new title value
154              * @param {Number} index The index in the collection of titles
155              */
156             titlechange: true
157         });
158
159         me.mixins.observable.constructor.call(me, config);
160
161         me.on({
162             scope: me,
163             itemmouseover: me.onItemMouseOver,
164             itemmouseout: me.onItemMouseOut,
165             mouseleave: me.onMouseLeave
166         });
167     },
168     
169     /**
170      * Iterate over each of the records for this series. The default implementation simply iterates
171      * through the entire data store, but individual series implementations can override this to
172      * provide custom handling, e.g. adding/removing records.
173      * @param {Function} fn The function to execute for each record.
174      * @param {Object} scope Scope for the fn.
175      */
176     eachRecord: function(fn, scope) {
177         var chart = this.chart;
178         (chart.substore || chart.store).each(fn, scope);
179     },
180
181     /**
182      * Return the number of records being displayed in this series. Defaults to the number of
183      * records in the store; individual series implementations can override to provide custom handling.
184      */
185     getRecordCount: function() {
186         var chart = this.chart,
187             store = chart.substore || chart.store;
188         return store ? store.getCount() : 0;
189     },
190
191     /**
192      * Determines whether the series item at the given index has been excluded, i.e. toggled off in the legend.
193      * @param index
194      */
195     isExcluded: function(index) {
196         var excludes = this.__excludes;
197         return !!(excludes && excludes[index]);
198     },
199
200     // @private set the bbox and clipBox for the series
201     setBBox: function(noGutter) {
202         var me = this,
203             chart = me.chart,
204             chartBBox = chart.chartBBox,
205             gutterX = noGutter ? 0 : chart.maxGutter[0],
206             gutterY = noGutter ? 0 : chart.maxGutter[1],
207             clipBox, bbox;
208
209         clipBox = {
210             x: chartBBox.x,
211             y: chartBBox.y,
212             width: chartBBox.width,
213             height: chartBBox.height
214         };
215         me.clipBox = clipBox;
216
217         bbox = {
218             x: (clipBox.x + gutterX) - (chart.zoom.x * chart.zoom.width),
219             y: (clipBox.y + gutterY) - (chart.zoom.y * chart.zoom.height),
220             width: (clipBox.width - (gutterX * 2)) * chart.zoom.width,
221             height: (clipBox.height - (gutterY * 2)) * chart.zoom.height
222         };
223         me.bbox = bbox;
224     },
225
226     // @private set the animation for the sprite
227     onAnimate: function(sprite, attr) {
228         var me = this;
229         sprite.stopAnimation();
230         if (me.triggerAfterDraw) {
231             return sprite.animate(Ext.applyIf(attr, me.chart.animate));
232         } else {
233             me.triggerAfterDraw = true;
234             return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), {
235                 listeners: {
236                     'afteranimate': function() {
237                         me.triggerAfterDraw = false;
238                         me.fireEvent('afterrender');
239                     }
240                 }
241             }));
242         }
243     },
244
245     // @private return the gutter.
246     getGutters: function() {
247         return [0, 0];
248     },
249
250     // @private wrapper for the itemmouseover event.
251     onItemMouseOver: function(item) {
252         var me = this;
253         if (item.series === me) {
254             if (me.highlight) {
255                 me.highlightItem(item);
256             }
257             if (me.tooltip) {
258                 me.showTip(item);
259             }
260         }
261     },
262
263     // @private wrapper for the itemmouseout event.
264     onItemMouseOut: function(item) {
265         var me = this;
266         if (item.series === me) {
267             me.unHighlightItem();
268             if (me.tooltip) {
269                 me.hideTip(item);
270             }
271         }
272     },
273
274     // @private wrapper for the mouseleave event.
275     onMouseLeave: function() {
276         var me = this;
277         me.unHighlightItem();
278         if (me.tooltip) {
279             me.hideTip();
280         }
281     },
282
283     /**
284      * For a given x/y point relative to the Surface, find a corresponding item from this
285      * series, if any.
286      * @param {Number} x
287      * @param {Number} y
288      * @return {Object} An object describing the item, or null if there is no matching item.
289      * The exact contents of this object will vary by series type, but should always contain the following:
290      * @return {Ext.chart.series.Series} return.series the Series object to which the item belongs
291      * @return {Object} return.value the value(s) of the item's data point
292      * @return {Array} return.point the x/y coordinates relative to the chart box of a single point
293      * for this data item, which can be used as e.g. a tooltip anchor point.
294      * @return {Ext.draw.Sprite} return.sprite the item's rendering Sprite.
295      */
296     getItemForPoint: function(x, y) {
297         //if there are no items to query just return null.
298         if (!this.items || !this.items.length || this.seriesIsHidden) {
299             return null;
300         }
301         var me = this,
302             items = me.items,
303             bbox = me.bbox,
304             item, i, ln;
305         // Check bounds
306         if (!Ext.draw.Draw.withinBox(x, y, bbox)) {
307             return null;
308         }
309         for (i = 0, ln = items.length; i < ln; i++) {
310             if (items[i] && this.isItemInPoint(x, y, items[i], i)) {
311                 return items[i];
312             }
313         }
314
315         return null;
316     },
317
318     isItemInPoint: function(x, y, item, i) {
319         return false;
320     },
321
322     /**
323      * Hides all the elements in the series.
324      */
325     hideAll: function() {
326         var me = this,
327             items = me.items,
328             item, len, i, j, l, sprite, shadows;
329
330         me.seriesIsHidden = true;
331         me._prevShowMarkers = me.showMarkers;
332
333         me.showMarkers = false;
334         //hide all labels
335         me.hideLabels(0);
336         //hide all sprites
337         for (i = 0, len = items.length; i < len; i++) {
338             item = items[i];
339             sprite = item.sprite;
340             if (sprite) {
341                 sprite.setAttributes({
342                     hidden: true
343                 }, true);
344             }
345
346             if (sprite && sprite.shadows) {
347                 shadows = sprite.shadows;
348                 for (j = 0, l = shadows.length; j < l; ++j) {
349                     shadows[j].setAttributes({
350                         hidden: true
351                     }, true);
352                 }
353             }
354         }
355     },
356
357     /**
358      * Shows all the elements in the series.
359      */
360     showAll: function() {
361         var me = this,
362             prevAnimate = me.chart.animate;
363         me.chart.animate = false;
364         me.seriesIsHidden = false;
365         me.showMarkers = me._prevShowMarkers;
366         me.drawSeries();
367         me.chart.animate = prevAnimate;
368     },
369
370     /**
371      * Returns a string with the color to be used for the series legend item.
372      */
373     getLegendColor: function(index) {
374         var me = this, fill, stroke;
375         if (me.seriesStyle) {
376             fill = me.seriesStyle.fill;
377             stroke = me.seriesStyle.stroke;
378             if (fill && fill != 'none') {
379                 return fill;
380             }
381             return stroke;
382         }
383         return '#000';
384     },
385
386     /**
387      * Checks whether the data field should be visible in the legend
388      * @private
389      * @param {Number} index The index of the current item
390      */
391     visibleInLegend: function(index){
392         var excludes = this.__excludes;
393         if (excludes) {
394             return !excludes[index];
395         }
396         return !this.seriesIsHidden;
397     },
398
399     /**
400      * Changes the value of the {@link #title} for the series.
401      * Arguments can take two forms:
402      * <ul>
403      * <li>A single String value: this will be used as the new single title for the series (applies
404      * to series with only one yField)</li>
405      * <li>A numeric index and a String value: this will set the title for a single indexed yField.</li>
406      * </ul>
407      * @param {Number} index
408      * @param {String} title
409      */
410     setTitle: function(index, title) {
411         var me = this,
412             oldTitle = me.title;
413
414         if (Ext.isString(index)) {
415             title = index;
416             index = 0;
417         }
418
419         if (Ext.isArray(oldTitle)) {
420             oldTitle[index] = title;
421         } else {
422             me.title = title;
423         }
424
425         me.fireEvent('titlechange', title, index);
426     }
427 });
428